From 15ef08b3edd9d3110a52de510a4d6c27edfb6d84 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Sun, 1 Nov 2026 22:54:17 -0500 Subject: [PATCH 1/5] fix(agc): keep guest writes over a stale stencil plane A draw that writes stencil marks the surface's stencil plane stale; the plane reaches guest memory later, when a GPU write retires the surface or something reads the plane. That store wrote the stencil over the plane even when the guest had written it after the draw, typically after freeing the memory and reusing it. The mirror upload inside StoreStencilPlane takes the guest bytes at store time, so the guest's write looked like original data, and on Windows the driver no longer write-watches memory it uses as a surface. When the stencil goes stale the driver now records the tracker generation and, for a plane that is not write-watched, a copy of its guest bytes. Both store paths skip the store, and clear the stale state, when the guest wrote the plane since: UnchangedSinceCollected on watched memory, a compare with the copy otherwise. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/DepthSurface.cpp | 25 ++++++++++++++++--- .../tests/execution/StencilPlaneMemory.cpp | 9 +++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 78de805344cf..d5472aa1be85 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -857,9 +857,18 @@ class DepthSurface { void MarkStencilStale() { if (!StencilMirrorable()) return; + const auto [begin, end] = staleStencilRange(*this); + if (!stencilStale) { + const auto bytes = static_cast(end - begin); + stencilStaleSnapshot.clear(); + if (!GuestMemory::Watched(begin, bytes) && GuestMemory::Accessible(reinterpret_cast(begin), bytes)) { + stencilStaleSnapshot.resize(bytes); + GuestMemory::Read(begin, stencilStaleSnapshot); + } + } stencilStale = true; + stencilStaleGeneration = GuestMemory::TrackerGeneration(); stencilMark = nextPlaneSerial(); - const auto [begin, end] = staleStencilRange(*this); noteStalePlane(begin, end); } @@ -1422,6 +1431,8 @@ class DepthSurface { bool stencilStale = false; std::uint64_t stencilGeneration = 0; + std::uint64_t stencilStaleGeneration = 0; + std::vector stencilStaleSnapshot; std::vector staleUnits; std::size_t staleCount = 0; @@ -2823,12 +2834,17 @@ void retireStencil(DepthSurface& surface) { forgetStaleStencil(surface); } +bool stencilWrittenSinceStale(const DepthSurface& surface, std::uint64_t begin, std::size_t bytes) { + if (GuestMemory::Watched(begin, bytes)) return !GuestMemory::UnchangedSinceCollected(begin, bytes, surface.stencilStaleGeneration); + return surface.stencilStaleSnapshot.size() == bytes && !GuestMemory::EqualsCommittedUnsynced(begin, surface.stencilStaleSnapshot); +} + void storeStaleStencil(DepthSurface& surface) { if (!surface.stencilStale || surface.syncing || !surface.StencilMirrorable()) return; const auto [begin, end] = staleStencilRange(surface); const auto bytes = static_cast(end - begin); if (GuestMemory::CommittedRanges(begin, bytes).empty()) return; - if (unmappedSince(begin, end, surface.stencilMark)) { + if (unmappedSince(begin, end, surface.stencilMark) || stencilWrittenSinceStale(surface, begin, bytes)) { retireStencil(surface); return; } @@ -3221,7 +3237,10 @@ std::vector> FlushDepthPlanes(std::uint6 if (surface->stencilStale && !surface->syncing) { const auto [stencilBegin, stencilEnd] = staleStencilRange(*surface); if (stencilBegin < end && address < stencilEnd) { - if (GuestMemory::CommittedRanges(stencilBegin, static_cast(stencilEnd - stencilBegin)).empty() || unmappedSince(stencilBegin, stencilEnd, surface->stencilMark)) { + const auto stencilBytes = static_cast(stencilEnd - stencilBegin); + if (GuestMemory::CommittedRanges(stencilBegin, stencilBytes).empty() || + unmappedSince(stencilBegin, stencilEnd, surface->stencilMark) || + stencilWrittenSinceStale(*surface, stencilBegin, stencilBytes)) { surface->stencilStale = false; } else { auto mirror = surface->StoreStencilPlane(stencilResource(surface->target)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp index 3969668dba1b..c78535d40056 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp @@ -167,12 +167,21 @@ int main() { Require(Holding(stencilPlane, 0x2au) == StencilBytes, "retiring the surface for a write over its depth plane only dropped the stencil 0x2a a draw wrote"); Require(AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "a read of the stencil plane stored after its surface retired stored it again"); + Require(draw({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, 0x3cu)}) == PlaneTexels, "the draw writing stencil 0x3c did not cover the plane"); + device->WaitIdle(); + Fill(stencilPlane, 0x77u); + AgcDriver::Graphics::RetireDepthSurfaces(device->graphicsContext().device, reinterpret_cast(depthPlane), DepthBytes); + device->WaitIdle(); + Require(Holding(stencilPlane, 0x77u) == StencilBytes, "retiring the surface stored its stale stencil 0x3c over the guest's later write of 0x77"); + Require(AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "a read of the stencil plane the guest overwrote stored the stale stencil"); + if (!AgcDriver::GuestMemory::WriteWatched() || !AgcDriver::GuestMemory::Watched(stencilBase, StencilBytes)) { std::puts("guest memory is not write-watched here: a guest write into the stencil plane is not exercised"); } else { Fill(stencilPlane, 0x05u); Require(draw({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_EQUAL, VK_STENCIL_OP_KEEP, 0x05u)}) == PlaneTexels, "a draw testing stencil EQUAL 0x05 after the guest wrote 0x05 into the stencil plane did not pass everywhere"); Require(draw({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_EQUAL, VK_STENCIL_OP_KEEP, 0x18u)}) == 0u, "a draw testing stencil EQUAL 0x18 passed after the guest wrote 0x05 over the stored stencil plane"); + } Require(readOnlyFailures == 0, "read-only stencil draws dirtied the stored plane"); From 16f8c5e3e94e01281279e07e83f434110eb532c7 Mon Sep 17 00:00:00 2001 From: JJenkx Date: Mon, 2 Nov 2026 03:23:04 -0500 Subject: [PATCH 2/5] fix(agc): refresh the stale stencil snapshot when a later draw follows a guest write On an unwatched plane the snapshot was taken only when the stencil first went stale, so a guest write followed by another stencil draw made the store compare against the older bytes and keep the guest write over the later draw. Each stencil draw now refreshes the snapshot when the plane's bytes no longer match it. The test covers a second draw after the guest write through retirement and through a read, and a guest write after the last draw through a read. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/DepthSurface.cpp | 11 ++++---- .../tests/execution/StencilPlaneMemory.cpp | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index d5472aa1be85..97810b20f866 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -858,13 +858,12 @@ class DepthSurface { void MarkStencilStale() { if (!StencilMirrorable()) return; const auto [begin, end] = staleStencilRange(*this); - if (!stencilStale) { - const auto bytes = static_cast(end - begin); + const auto bytes = static_cast(end - begin); + if (GuestMemory::Watched(begin, bytes) || !GuestMemory::Accessible(reinterpret_cast(begin), bytes)) { stencilStaleSnapshot.clear(); - if (!GuestMemory::Watched(begin, bytes) && GuestMemory::Accessible(reinterpret_cast(begin), bytes)) { - stencilStaleSnapshot.resize(bytes); - GuestMemory::Read(begin, stencilStaleSnapshot); - } + } else if (!stencilStale || stencilStaleSnapshot.size() != bytes || !GuestMemory::EqualsCommittedUnsynced(begin, stencilStaleSnapshot)) { + stencilStaleSnapshot.resize(bytes); + GuestMemory::Read(begin, stencilStaleSnapshot); } stencilStale = true; stencilStaleGeneration = GuestMemory::TrackerGeneration(); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp index c78535d40056..a746db7d395f 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp @@ -175,6 +175,33 @@ int main() { Require(Holding(stencilPlane, 0x77u) == StencilBytes, "retiring the surface stored its stale stencil 0x3c over the guest's later write of 0x77"); Require(AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "a read of the stencil plane the guest overwrote stored the stale stencil"); + Require(draw({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, 0x3cu)}) == PlaneTexels, "the draw writing stencil 0x3c before a guest write and a second draw did not cover the plane"); + device->WaitIdle(); + Fill(stencilPlane, 0x44u); + Require(draw({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, 0x55u)}) == PlaneTexels, "the draw writing stencil 0x55 after the guest write did not cover the plane"); + device->WaitIdle(); + AgcDriver::Graphics::RetireDepthSurfaces(device->graphicsContext().device, reinterpret_cast(depthPlane), DepthBytes); + device->WaitIdle(); + Require(Holding(stencilPlane, 0x55u) == StencilBytes, "retiring the surface kept the guest's 0x44 over the stencil 0x55 a later draw wrote"); + + Require(draw({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, 0x3cu)}) == PlaneTexels, "the draw writing stencil 0x3c before a guest write and a read did not cover the plane"); + device->WaitIdle(); + Fill(stencilPlane, 0x77u); + Require(AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "a read of the stencil plane the guest overwrote after the last draw stored the stale stencil"); + device->WaitIdle(); + Require(Holding(stencilPlane, 0x77u) == StencilBytes, "a read of the stencil plane replaced the guest's write after the last draw"); + + Require(draw({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, 0x3cu)}) == PlaneTexels, "the draw writing stencil 0x3c before a guest write, a second draw and a read did not cover the plane"); + device->WaitIdle(); + Fill(stencilPlane, 0x66u); + Require(draw({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, 0x55u)}) == PlaneTexels, "the draw writing stencil 0x55 after the guest write and before a read did not cover the plane"); + device->WaitIdle(); + Require(!AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "a read of the stencil plane a draw wrote after the guest's write stored nothing"); + device->WaitIdle(); + Require(Holding(stencilPlane, 0x55u) == StencilBytes, "a read of the stencil plane kept the guest's 0x66 over the stencil 0x55 a later draw wrote"); + AgcDriver::Graphics::RetireDepthSurfaces(device->graphicsContext().device, reinterpret_cast(depthPlane), DepthBytes); + device->WaitIdle(); + if (!AgcDriver::GuestMemory::WriteWatched() || !AgcDriver::GuestMemory::Watched(stencilBase, StencilBytes)) { std::puts("guest memory is not write-watched here: a guest write into the stencil plane is not exercised"); } else { From 811f2d92e488fb8b335be520ef9bc74d87074d68 Mon Sep 17 00:00:00 2001 From: JJenkx <31495916+JJenkx@users.noreply.github.com> Date: Tue, 3 Nov 2026 16:12:04 -0500 Subject: [PATCH 3/5] docs(agc): record stale stencil partial-write and comparison costs --- .../prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp | 1 - docs/dev/debt/libSceAgcDriver.md | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp index a746db7d395f..8cef9a99d4c5 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp @@ -208,7 +208,6 @@ int main() { Fill(stencilPlane, 0x05u); Require(draw({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_EQUAL, VK_STENCIL_OP_KEEP, 0x05u)}) == PlaneTexels, "a draw testing stencil EQUAL 0x05 after the guest wrote 0x05 into the stencil plane did not pass everywhere"); Require(draw({.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_EQUAL, VK_STENCIL_OP_KEEP, 0x18u)}) == 0u, "a draw testing stencil EQUAL 0x18 passed after the guest wrote 0x05 over the stored stencil plane"); - } Require(readOnlyFailures == 0, "read-only stencil draws dirtied the stored plane"); diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 0da81517922f..5297fc0d91cd 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -122,6 +122,8 @@ ### Functional +- [Stale stencil guest writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a CPU write after the last stencil draw drops the whole pending stencil store. Untouched bytes therefore retain their previous guest value instead of the drawn stencil; on write-watched memory a write anywhere in an overlapping tracked page can drop the store. The unwatched path compares the entire stencil plane on each subsequent stencil-writing bind or stale-marking clear/fill while the graphics locks are held, and copies it again when guest bytes changed. Bytewise merging and console ordering for partial CPU writes are not implemented. + - [Native multisampled depth import](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - refuses a depth HTILE that is not pipe-aligned (`0x2af` bit 18 is zero). Sonic Superstars (PPSA06888) uses a layout with iterate-256 also off (`DB_Z_INFO` bit 20 is zero) on its 2x depth targets, and the refusal stops presentation before its title screen. Not verified: whether the clear-proof is correct for a non-pipe-aligned layout, since the import reads the HTILE words as a flat array. - [Written staging window on discrete GPUs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written buffer element of a dispatch that is not swept is staged device-local up to 8 MiB on a discrete GPU (`Context::discreteGpu`) and up to 2 MiB otherwise; `APS5_WRITTEN_SHADOW_MAX_KIB` replaces either bound. The 8 MiB figure comes from one title on one NVIDIA Windows system (PPSA09911: a 2.5 and a 3.3 MiB skin-cache output written in place cost 55 ms of GPU time per frame, 1.8 ms of extra staging copies once staged); the point where the two copies of a larger element cost more than its in-place stores, and whether AMD or Linux drivers move it, has not been measured. The window is per element and per use, so a kernel that streams once through an element inside the window pays the copy-in and the copy-back on every dispatch. - [FS table calls on macOS](../../../core/libs/prx/libSceAgcDriver/Execution/include/Driver/DriverMemory.hpp) (libSceAgcDriver) - macOS keeps the console's 0xfe0000000 for itself, so the driver block, and the FS table at 0xfe0040000 inside it, moves to 0xee0000000 there; `sceAgc*StaticBuffer` and `AgcDriverStaticTable_nid_postfix` use the moved address. A shader that builds the table address from two `s_mov_b32` literals still loads from 0xfe0040000: `ReadTableCallees` (`Driver/Shaders/ShaderRegistry.cpp`) reads that address unchanged, and the shader also loads from it at run time, so those dispatches cannot link on macOS. #6160 lists titles that build this address. `ComputeFsTableCalls` is skipped on macOS as it is under `ANYPS5_ENABLE_ASAN`. Closes when: the driver rewrites that pointer in the shader code for both the callee lookup and the code the GPU runs, or the console address can be mapped on macOS. From c1fe457b32fbd69c5dea801bad46743028d6979f Mon Sep 17 00:00:00 2001 From: JJenkx Date: Wed, 4 Nov 2026 14:55:44 -0500 Subject: [PATCH 4/5] fix(agc): compare a stale stencil plane with its last store, not its last mark A stale stencil plane is dropped instead of stored when the guest wrote its memory. The comparison was against the memory at the plane's latest stale mark, and HTILE clears and rebinds re-mark a plane without writing it, so a guest write before the latest mark was taken as the plane's own bytes and the stale plane was stored over it. The baseline is now the last store, or a mark that comes from a stencil write or a stencil plane fill. On write-watched memory the store checks WrittenSince against the generation of the last sync or store. Co-Authored-By: Claude Opus 5.5 --- .../Graphics/src/DepthSurface.cpp | 39 +++++++++++-------- .../tests/execution/StencilPlaneMemory.cpp | 30 ++++++++++++++ docs/dev/debt/libSceAgcDriver.md | 2 +- 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp index 97810b20f866..ac36520f959a 100644 --- a/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp +++ b/core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp @@ -855,18 +855,22 @@ class DepthSurface { if ((aspects & VK_IMAGE_ASPECT_STENCIL_BIT) != 0) stencilFill.reset(); } - void MarkStencilStale() { - if (!StencilMirrorable()) return; + void TakeStencilBaseline() { const auto [begin, end] = staleStencilRange(*this); const auto bytes = static_cast(end - begin); if (GuestMemory::Watched(begin, bytes) || !GuestMemory::Accessible(reinterpret_cast(begin), bytes)) { - stencilStaleSnapshot.clear(); - } else if (!stencilStale || stencilStaleSnapshot.size() != bytes || !GuestMemory::EqualsCommittedUnsynced(begin, stencilStaleSnapshot)) { - stencilStaleSnapshot.resize(bytes); - GuestMemory::Read(begin, stencilStaleSnapshot); + stencilBaseline.clear(); + return; } + stencilBaseline.resize(bytes); + GuestMemory::Read(begin, stencilBaseline); + } + + void MarkStencilStale(bool written = false) { + if (!StencilMirrorable()) return; + const auto [begin, end] = staleStencilRange(*this); + if (written || stencilBaseline.size() != static_cast(end - begin)) TakeStencilBaseline(); stencilStale = true; - stencilStaleGeneration = GuestMemory::TrackerGeneration(); stencilMark = nextPlaneSerial(); noteStalePlane(begin, end); } @@ -885,7 +889,7 @@ class DepthSurface { NoteDepthWork(); } else { stencilFill = stencil; - MarkStencilStale(); + MarkStencilStale(true); } } @@ -1426,12 +1430,12 @@ class DepthSurface { void NoteStencilStored(std::uint64_t generation) { stencilGeneration = generation; + if (StencilMirrorable()) TakeStencilBaseline(); } bool stencilStale = false; std::uint64_t stencilGeneration = 0; - std::uint64_t stencilStaleGeneration = 0; - std::vector stencilStaleSnapshot; + std::vector stencilBaseline; std::vector staleUnits; std::size_t staleCount = 0; @@ -2833,9 +2837,12 @@ void retireStencil(DepthSurface& surface) { forgetStaleStencil(surface); } -bool stencilWrittenSinceStale(const DepthSurface& surface, std::uint64_t begin, std::size_t bytes) { - if (GuestMemory::Watched(begin, bytes)) return !GuestMemory::UnchangedSinceCollected(begin, bytes, surface.stencilStaleGeneration); - return surface.stencilStaleSnapshot.size() == bytes && !GuestMemory::EqualsCommittedUnsynced(begin, surface.stencilStaleSnapshot); +bool stencilWrittenSinceBaseline(const DepthSurface& surface, std::uint64_t begin, std::size_t bytes) { + if (GuestMemory::Watched(begin, bytes)) { + GuestMemory::CollectWritesUncached(begin, bytes); + return surface.stencilGeneration != 0u && GuestMemory::WrittenSince(begin, bytes, surface.stencilGeneration); + } + return surface.stencilBaseline.size() == bytes && !GuestMemory::EqualsCommittedUnsynced(begin, surface.stencilBaseline); } void storeStaleStencil(DepthSurface& surface) { @@ -2843,7 +2850,7 @@ void storeStaleStencil(DepthSurface& surface) { const auto [begin, end] = staleStencilRange(surface); const auto bytes = static_cast(end - begin); if (GuestMemory::CommittedRanges(begin, bytes).empty()) return; - if (unmappedSince(begin, end, surface.stencilMark) || stencilWrittenSinceStale(surface, begin, bytes)) { + if (unmappedSince(begin, end, surface.stencilMark) || stencilWrittenSinceBaseline(surface, begin, bytes)) { retireStencil(surface); return; } @@ -3153,7 +3160,7 @@ VkImageView DepthSurfaceViewWithStencilWriteIntent(const Context& context, const surface->MarkAllStale(planeBytes); noteStalePlane(plane, plane + planeBytes); } - if (stencilWritten || created || stencilRebound) surface->MarkStencilStale(); + if (stencilWritten || created || stencilRebound) surface->MarkStencilStale(stencilWritten); drawnDepth = surface; return surface->view; } @@ -3239,7 +3246,7 @@ std::vector> FlushDepthPlanes(std::uint6 const auto stencilBytes = static_cast(stencilEnd - stencilBegin); if (GuestMemory::CommittedRanges(stencilBegin, stencilBytes).empty() || unmappedSince(stencilBegin, stencilEnd, surface->stencilMark) || - stencilWrittenSinceStale(*surface, stencilBegin, stencilBytes)) { + stencilWrittenSinceBaseline(*surface, stencilBegin, stencilBytes)) { surface->stencilStale = false; } else { auto mirror = surface->StoreStencilPlane(stencilResource(surface->target)); diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp index 8cef9a99d4c5..b83c2ad95f83 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp @@ -118,6 +118,35 @@ void CheckConsumedStencilClear(AgcDriver::VulkanDevice& device, const DepthFastC Require(AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "the consumed stencil clear was stored twice"); } +void CheckGuestWriteBeforeRepeatedClears(AgcDriver::VulkanDevice& device, const DepthFastClearHarness::Shaders& shaders, std::uint32_t swizzle) { + auto* blocks = static_cast(AllocateBlocks(DepthBytes + StencilBytes + Block)); + auto* depth = reinterpret_cast(blocks); + auto* stencil = reinterpret_cast(blocks + DepthBytes); + auto* htile = reinterpret_cast(blocks + DepthBytes + StencilBytes); + const auto htileBytes = AgcDriver::Graphics::HtileMetadataBytes(PlaneExtent); + std::fill(depth, depth + PlaneTexels, 1.0f); + Fill(stencil, 0u); + std::fill(htile, htile + htileBytes / sizeof(*htile), 0xffffffffu); + AgcDriver::GuestMemory::BumpCollectEpoch(); + const auto stencilBase = reinterpret_cast(stencil); + const Surface surface{reinterpret_cast(depth), stencilBase, reinterpret_cast(htile), true, VK_FORMAT_D32_SFLOAT_S8_UINT, 0, 0, swizzle}; + Require(Draw(device, shaders, surface, {.depthTest = true, .depthWrite = true, .depthCompare = VK_COMPARE_OP_ALWAYS, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, 0x18u)}, PlaneExtent, Colors) == PlaneTexels, "the draw before the guest write did not cover the plane"); + Require(!AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "the stencil drawn before the guest write was not stored"); + device.WaitIdle(); + Require(Holding(stencil, 0x18u) == StencilBytes, "the stencil drawn before the guest write did not reach guest memory"); + Fill(stencil, 0x77u); + AgcDriver::GuestMemory::BumpCollectEpoch(); + for (int clear = 0; clear < 2; ++clear) { + std::fill(htile, htile + htileBytes / sizeof(*htile), 0u); + AgcDriver::GuestMemory::BumpCollectEpoch(); + AgcDriver::Graphics::NoteDepthMetadataFill(surface.htile, htileBytes, 0u); + } + static_cast(AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes)); + device.WaitIdle(); + Require(Holding(stencil, 0x77u) == StencilBytes, "stale stencil planes re-marked by HTILE clears were stored over the guest's write after their last store"); + Require(AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "a stencil plane the guest wrote after its last store stayed stale"); +} + } int main() { @@ -145,6 +174,7 @@ int main() { CheckConsumedStencilClear(*device, shaders, swizzle, true); CheckConsumedStencilClear(*device, shaders, swizzle, false); + CheckGuestWriteBeforeRepeatedClears(*device, shaders, swizzle); std::size_t readOnlyFailures = 0; const auto checkReadOnly = [&](const DrawOptions& options, const char* name) { diff --git a/docs/dev/debt/libSceAgcDriver.md b/docs/dev/debt/libSceAgcDriver.md index 5297fc0d91cd..125289acb048 100644 --- a/docs/dev/debt/libSceAgcDriver.md +++ b/docs/dev/debt/libSceAgcDriver.md @@ -122,7 +122,7 @@ ### Functional -- [Stale stencil guest writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a CPU write after the last stencil draw drops the whole pending stencil store. Untouched bytes therefore retain their previous guest value instead of the drawn stencil; on write-watched memory a write anywhere in an overlapping tracked page can drop the store. The unwatched path compares the entire stencil plane on each subsequent stencil-writing bind or stale-marking clear/fill while the graphics locks are held, and copies it again when guest bytes changed. Bytewise merging and console ordering for partial CPU writes are not implemented. +- [Stale stencil guest writes](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver): a CPU write after the plane's last store, stencil draw or stencil plane fill drops the whole pending stencil store. HTILE clears and rebinds that mark the plane stale again do not move that point, so a write before them still drops the store. Untouched bytes therefore retain their previous guest value instead of the drawn stencil; on write-watched memory a write anywhere in an overlapping tracked page can drop the store. A CPU write followed by an HTILE stencil clear keeps the guest bytes, although a console decompress would write the clear value over them. The unwatched path copies the entire stencil plane after each store and each stencil-writing bind or plane fill, and compares it on each store, while the graphics locks are held. Bytewise merging and console ordering for partial CPU writes are not implemented. - [Native multisampled depth import](../../../core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp) (libSceAgcDriver) - refuses a depth HTILE that is not pipe-aligned (`0x2af` bit 18 is zero). Sonic Superstars (PPSA06888) uses a layout with iterate-256 also off (`DB_Z_INFO` bit 20 is zero) on its 2x depth targets, and the refusal stops presentation before its title screen. Not verified: whether the clear-proof is correct for a non-pipe-aligned layout, since the import reads the HTILE words as a flat array. - [Written staging window on discrete GPUs](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) (libSceAgcDriver) - a written buffer element of a dispatch that is not swept is staged device-local up to 8 MiB on a discrete GPU (`Context::discreteGpu`) and up to 2 MiB otherwise; `APS5_WRITTEN_SHADOW_MAX_KIB` replaces either bound. The 8 MiB figure comes from one title on one NVIDIA Windows system (PPSA09911: a 2.5 and a 3.3 MiB skin-cache output written in place cost 55 ms of GPU time per frame, 1.8 ms of extra staging copies once staged); the point where the two copies of a larger element cost more than its in-place stores, and whether AMD or Linux drivers move it, has not been measured. The window is per element and per use, so a kernel that streams once through an element inside the window pays the copy-in and the copy-back on every dispatch. From ed4ae4f33aca3c6775f7bfefab00b8269450210f Mon Sep 17 00:00:00 2001 From: JJenkx Date: Fri, 6 Nov 2026 10:44:34 -0500 Subject: [PATCH 5/5] test(agc): cover a reused, write-watched stencil plane on Linux Back the planes with a shared memfd, imported as a dma-buf and write-watched, so the watched branch runs on Linux. Co-authored-by: hugodearaujo <175634979+hugodearaujo@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 --- .../tests/execution/StencilPlaneMemory.cpp | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp b/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp index b83c2ad95f83..9af5921e8492 100644 --- a/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/execution/StencilPlaneMemory.cpp @@ -13,7 +13,9 @@ #endif #include #else +#include #include +#include #endif #include #include @@ -147,6 +149,101 @@ void CheckGuestWriteBeforeRepeatedClears(AgcDriver::VulkanDevice& device, const Require(AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "a stencil plane the guest wrote after its last store stayed stale"); } +#ifdef __linux__ +struct SharedPlanes { + int file = -1; + std::uintptr_t begin = 0; + std::size_t bytes = 0; + std::uint64_t offset = 0; +}; +SharedPlanes sharedPlanes; + +bool ResolveSharedPlanes(std::uintptr_t address, std::size_t bytes, int* file, std::uint64_t* offset) { + if (sharedPlanes.file < 0 || bytes == 0 || address < sharedPlanes.begin || address - sharedPlanes.begin >= sharedPlanes.bytes || bytes > sharedPlanes.bytes - (address - sharedPlanes.begin)) return false; + *file = fcntl(sharedPlanes.file, F_DUPFD_CLOEXEC, 0); + *offset = sharedPlanes.offset + (address - sharedPlanes.begin); + return *file >= 0; +} +#endif + +void* AllocateSharedBlocks(std::size_t bytes) { +#ifdef __linux__ + const int file = memfd_create("stencil-plane-memory", MFD_CLOEXEC | MFD_ALLOW_SEALING); + Require(file >= 0, "cannot create the shared depth and stencil planes"); + Require(ftruncate(file, static_cast(bytes + Block)) == 0 && fcntl(file, F_ADD_SEALS, F_SEAL_SHRINK) == 0, "cannot size the shared depth and stencil planes"); + void* raw = mmap(nullptr, bytes + Block, PROT_READ | PROT_WRITE, MAP_SHARED, file, 0); + Require(raw != MAP_FAILED, "cannot map the shared depth and stencil planes"); + const auto begin = reinterpret_cast(raw); + const auto aligned = (begin + Block - 1) & ~static_cast(Block - 1); + if (aligned != begin) munmap(raw, aligned - begin); + if (aligned + bytes != begin + bytes + Block) munmap(reinterpret_cast(aligned + bytes), begin + Block - aligned); + sharedPlanes = {file, aligned, bytes, aligned - begin}; + GuestArena::GuestArenaSetSharedBacking_nid_postfix(ResolveSharedPlanes); + void* block = reinterpret_cast(aligned); + GuestWriteWatch::GuestWriteWatchRegister_nid_postfix(block, bytes); + GuestAllocations::Mutation().Add(block, bytes, true, true, true); + return block; +#else + return AllocateBlocks(bytes); +#endif +} + +void CheckReusedStencilPlane(AgcDriver::VulkanDevice& device, const DepthFastClearHarness::Shaders& shaders, std::uint32_t swizzle) { + auto* blocks = static_cast(AllocateSharedBlocks(DepthBytes + StencilBytes)); + auto* depth = reinterpret_cast(blocks); + auto* stencil = reinterpret_cast(blocks + DepthBytes); + std::fill(depth, depth + DepthBytes / sizeof(float), 1.0f); + Fill(stencil, 0u); + const auto stencilBase = reinterpret_cast(stencil); + const Surface surface{reinterpret_cast(depth), stencilBase, 0, false, VK_FORMAT_D32_SFLOAT_S8_UINT, 0, 0, swizzle}; + const auto draw = [&](std::uint8_t reference) { + Require(Draw(device, shaders, surface, {.depthTest = false, .stencilTest = true, .stencil = Stencil(VK_COMPARE_OP_ALWAYS, VK_STENCIL_OP_REPLACE, reference)}, PlaneExtent, Colors) == PlaneTexels, "a draw writing stencil over the reused plane did not cover it"); + }; + draw(0x18u); + Require(!AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "a read of the shared stencil plane after a draw stored nothing"); + device.WaitIdle(); + Require(Holding(stencil, 0x18u) == StencilBytes, "the stencil drawn into the shared plane did not reach guest memory"); + if (!AgcDriver::GuestMemory::Watched(stencilBase, StencilBytes)) { + std::puts("the shared stencil plane is not write-watched here: a guest reusing the plane's memory is not exercised"); + return; + } + constexpr std::size_t RecordBytes = 0x30; + constexpr std::array RecordOffsets{0x1000, 0x7dd0, StencilBytes - RecordBytes}; + const auto writeRecords = [&](std::uint8_t value) { + for (const auto offset : RecordOffsets) std::fill(stencil + offset, stencil + offset + RecordBytes, value); + AgcDriver::GuestMemory::BumpCollectEpoch(); + }; + const auto recordsHold = [&](std::uint8_t value) { + return std::all_of(RecordOffsets.begin(), RecordOffsets.end(), [&](std::size_t offset) { return std::all_of(stencil + offset, stencil + offset + RecordBytes, [&](std::uint8_t byte) { return byte == value; }); }); + }; + + draw(0x66u); + Fill(stencil, 0x77u); + const bool storedOverFill = !AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(); + device.WaitIdle(); + Require(Holding(stencil, 0x77u) == StencilBytes, "a read of a stencil plane the guest wrote over after a draw stored the stencil drawn before the write over the guest's bytes"); + Require(!storedOverFill, "a read of a stencil plane the guest wrote over after a draw stored the stencil drawn before the write"); + + draw(0x2bu); + writeRecords(0x7au); + AgcDriver::Graphics::RetireDepthSurfaces(device.graphicsContext().device, surface.depth, DepthBytes); + device.WaitIdle(); + Require(recordsHold(0x7au), "retiring the surface stored the stencil drawn before the guest wrote records into the plane over them"); + Require(AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "a read of the stencil plane whose retired surface's stencil was dropped stored it"); + + draw(0x4du); + writeRecords(0x3cu); + static_cast(AgcDriver::Graphics::FlushDepthPlanes(surface.depth, DepthBytes + StencilBytes, AgcDriver::Graphics::DepthPlaneScope::Whole)); + device.WaitIdle(); + Require(recordsHold(0x3cu), "a whole read of the planes stored the stencil drawn before the guest wrote records into the plane over them"); + + writeRecords(0x5eu); + draw(0x18u); + Require(!AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "a read of the stencil plane drawn after the guest's records stored nothing"); + device.WaitIdle(); + Require(Holding(stencil, 0x18u) == StencilBytes, "the stencil drawn after the guest wrote records did not reach guest memory over them"); +} + } int main() { @@ -255,6 +352,8 @@ int main() { Require(!AgcDriver::Graphics::FlushDepthPlanes(stencilBase, StencilBytes).empty(), "a read of the stencil plane drawn after the unmap stored nothing"); device->WaitIdle(); Require(Holding(stencilPlane, 0x18u) == StencilBytes, "the stencil drawn after the unmap did not reach guest memory"); + + CheckReusedStencilPlane(*device, shaders, swizzle); std::puts("stencil plane memory tests passed"); return 0; } catch (const std::exception& error) {