From 4b15b702ebd18500bbc452ea6567aeb7b5a389f4 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:35:39 -0700 Subject: [PATCH 1/3] refactor(agc): expose the guest read guard as GuestMemory::GuardRead Read, ReadCommitted and ReadCommittedUnsynced each loaded the installed read guard and called it inline. GuardRead does that once, so code that reads guest memory without going through those functions can run the same check. No behaviour change. --- .../libSceAgcDriver/Execution/include/GuestMemory.hpp | 1 + .../prx/libSceAgcDriver/Execution/src/GuestMemory.cpp | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp index 2388684997..916c9b3914 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp +++ b/core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp @@ -193,6 +193,7 @@ unsigned long long CodeOffset(const void* address); // calls FlushGpuWrites itself. void SetFlushHook(void (*hook)(std::uint64_t address, std::size_t bytes)); void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes)); +void GuardRead(std::uint64_t address, std::size_t bytes); void FlushGpuWrites(std::uint64_t address, std::size_t bytes); // Moves with every ForgetPages call (the libc invalidator: memory unmapped or re-registered): odd // while the call stores its page states, moved past it after, so a reader that loads an even diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp index d0859ab061..b069a06222 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp @@ -2082,13 +2082,17 @@ void SetReadGuard(void (*guard)(std::uint64_t address, std::size_t bytes)) { readGuard.store(guard, std::memory_order_release); } +void GuardRead(std::uint64_t address, std::size_t bytes) { + if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, bytes); +} + void FlushGpuWrites(std::uint64_t address, std::size_t bytes) { if (const auto hook = flushHook.load(std::memory_order_acquire)) hook(address, bytes); } void Read(std::uint64_t address, std::span destination, std::size_t alignment) { if (destination.empty()) return; - if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); + GuardRead(address, destination.size()); FlushGpuWrites(address, destination.size()); const TimedAccess timed(CounterRead, destination.size()); CountReadCaller(__builtin_return_address(0)); @@ -2118,7 +2122,7 @@ void copyCommitted(std::uint64_t address, std::span destination) { void ReadCommitted(std::uint64_t address, std::span destination) { if (destination.empty()) return; - if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); + GuardRead(address, destination.size()); FlushGpuWrites(address, destination.size()); const TimedAccess timed(CounterRead, destination.size()); CountReadCaller(__builtin_return_address(0)); @@ -2127,7 +2131,7 @@ void ReadCommitted(std::uint64_t address, std::span destination) { void ReadCommittedUnsynced(std::uint64_t address, std::span destination) { if (destination.empty()) return; - if (const auto guard = readGuard.load(std::memory_order_acquire)) guard(address, destination.size()); + GuardRead(address, destination.size()); const TimedAccess timed(CounterRead, destination.size()); CountReadCaller(__builtin_return_address(0)); copyCommitted(address, destination); From e01b1c46ecb25c8c4aabbf83c6223f228a94d566 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:39:51 -0700 Subject: [PATCH 2/3] fix(agc): run the read guard over a shader capture's lazily read pages Since #1193 (4651980eba) a capture copies a page in 64-byte blocks as it reads them, instead of reading the whole page with GuestMemory::Read the first time it touches it. The page now only gets FlushGpuWrites, which runs the flush hook but not the read guard, so a capture over a multisampled color target's memory, whose samples are only on the host, read the stale guest bytes silently where it threw before. ShaderMemory::page runs GuardRead over the page where the whole-page read stood, before the flush, as Read does. The check stays once per page a capture touches, as before #1193. The block copies need none: a pooled page is reset and goes through page() again, and later reads of a page were not checked again before #1193 either. The test captures a shader whose SRT lies in a live 4x target's memory, then one whose SRT lies just past it, then the first again on the pooled page. --- .../Execution/src/ShaderMemory.cpp | 1 + .../prx/libSceAgcDriver/tests/Recorder.cpp | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp index ffbecd5b70..f3f2881448 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp @@ -189,6 +189,7 @@ ShaderMemory::Page& ShaderMemory::page(std::uint64_t base) { return page; } const auto started = CaptureProfiled() ? std::chrono::steady_clock::now() : std::chrono::steady_clock::time_point{}; + GuestMemory::GuardRead(base, PageBytes); GuestMemory::FlushGpuWrites(base, PageBytes); if (CaptureProfiled()) CaptureTotals().pageReadNanoseconds += NanosecondsSince(started); page.lazy = true; diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index 040c2dd195..c7f5cfc3ff 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -36,6 +36,7 @@ #include "VertexRange_vert_spv.h" #include "VertexRangeProducer_frag_spv.h" #include "prx/libSceAgcDriver/Graphics/include/ColorTargetTransfer.hpp" +#include "BdaAbi.hpp" #include #ifdef _WIN32 #ifndef NOMINMAX @@ -6237,6 +6238,26 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { refused([&] { AgcDriver::GuestMemory::ReadCommitted(address + 256u, texels); }, "a CPU read", "a committed CPU read"); refused([&] { AgcDriver::GuestMemory::ReadCommittedUnsynced(address + 256u, texels); }, "a CPU read", "a committed CPU read without the flush hook"); refused([&] { RequireNoMultisampleTarget(address, 64u, "a DMA copy"); }, "a DMA copy", "a DMA copy"); + const auto captureSrt = [](std::uint64_t srt, AgcDriver::ShaderMemory::PendingWriteQuery pendingWrite) { + static constexpr std::array code{0xf4080100u, 0xfa000000u, 0xbf8cc07fu, 0xe0700000u, 0x80010000u, 0xbf810000u}; + const std::array userData{static_cast(srt), static_cast(srt >> 32u)}; + ShaderRecompiler::RecompileRequest request{}; + request.shader = {ShaderRecompiler::ShaderStage::Compute, 0x24000u, code, 0, {}}; + request.context.waveSize = 32u; + request.context.userDataBaseRegister = 0; + request.context.userData = userData; + request.context.compute = ShaderRecompiler::ShaderComputeStageInfo{{32u, 1u, 1u}, 0u, {false, false, false}, false, 1u}; + request.target.vulkanVersion = 0x00401000u; + request.target.spirvVersion = 0x00010300u; + request.target.subgroupSize = 32; + request.target.bdaAbiVersion = ShaderRecompiler::BdaAbi::Version; + request.layout.pushConstantSizeBytes = 128; + AgcDriver::ShaderMemory memory({}, pendingWrite); + static_cast(memory.Capture(request)); + }; + refused([&] { captureSrt(address + 256u, nullptr); }, "a CPU read", "a shader capture"); + captureSrt(address + 4u * 65536u + 256u, nullptr); + refused([&] { captureSrt(address + 256u, nullptr); }, "a CPU read", "a shader capture on a pooled page"); ShaderRecompiler::RecompileResult program; ShaderRecompiler::DescriptorBinding binding; binding.kind = ShaderRecompiler::DescriptorKind::SampledImage; From 07a4ac7e1501be9b1b92bb8828d95b57683978a7 Mon Sep 17 00:00:00 2001 From: Yunus Shaikh <75296019+EagleBearerYS@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:40:17 -0700 Subject: [PATCH 3/3] fix(agc): run the read guard on a shader capture's raw word reads On a page that recorded GPU work writes, a capture reads each dword on its own. Where the driver's evidence says that work leaves the dword unchanged (PendingWrite::Raw), the dword is copied straight from guest memory, without the read guard the other word reads get through GuestMemory::Read. The raw path (29b4601564) predates the guard (d109f3966f), so a capture whose raw word lies in a live multisampled target's memory has read stale bytes instead of throwing since then. The raw read now runs GuardRead over the dword first. With no target registered that is a few atomic loads, next to the pending-write query each raw word already makes. The test answers Raw for every page and word, so the capture's page is read word by word and raw, and Sync for the word reads that were already guarded. --- core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp | 1 + core/libs/prx/libSceAgcDriver/tests/Recorder.cpp | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp index f3f2881448..3501c5afc1 100644 --- a/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp +++ b/core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp @@ -250,6 +250,7 @@ bool ShaderMemory::read(void* context, std::uint64_t address, std::uint32_t* val word = waited; } } else if (policy == PendingWrite::Raw || policy == PendingWrite::VerifyRaw) { + GuestMemory::GuardRead(address, sizeof(word)); std::memcpy(&word, reinterpret_cast(address), sizeof(word)); ++CaptureTotals().wordsRaw; if (policy == PendingWrite::VerifyRaw) { diff --git a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp index c7f5cfc3ff..bd2436656a 100644 --- a/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp +++ b/core/libs/prx/libSceAgcDriver/tests/Recorder.cpp @@ -6258,6 +6258,11 @@ void multisampleGuestAccessTests(const Device& device, Recorder& recorder) { refused([&] { captureSrt(address + 256u, nullptr); }, "a CPU read", "a shader capture"); captureSrt(address + 4u * 65536u + 256u, nullptr); refused([&] { captureSrt(address + 256u, nullptr); }, "a CPU read", "a shader capture on a pooled page"); + const auto raw = [](std::uint64_t, std::size_t, std::span) { return AgcDriver::ShaderMemory::PendingWrite::Raw; }; + const auto synced = [](std::uint64_t, std::size_t, std::span) { return AgcDriver::ShaderMemory::PendingWrite::Sync; }; + refused([&] { captureSrt(address + 256u, raw); }, "a CPU read", "a shader capture's raw word read"); + refused([&] { captureSrt(address + 256u, synced); }, "a CPU read", "a shader capture's synced word read"); + captureSrt(address + 4u * 65536u + 256u, raw); ShaderRecompiler::RecompileResult program; ShaderRecompiler::DescriptorBinding binding; binding.kind = ShaderRecompiler::DescriptorKind::SampledImage;