Skip to content

fix(agc): run the read guard over a shader capture's lazy pages and raw word reads - #6475

Merged
adriadam10 merged 3 commits into
boykopovar:mainfrom
EagleBearerYS:fix/agc-guard-lazy-capture-pages
Oct 19, 2026
Merged

adriadam10 merged 3 commits into
boykopovar:mainfrom
EagleBearerYS:fix/agc-guard-lazy-capture-pages

Conversation

@EagleBearerYS

@EagleBearerYS EagleBearerYS commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What

#1193 dropped a safety check: a shader capture no longer runs the multisample read guard over the guest memory it reads. @MichonGoddijn231849 confirmed it and asked for this fix with a test (comment).

The regression. Before #1193, ShaderMemory::page read a page with GuestMemory::Read the first time a capture touched it, and Read runs the read guard first. The guard #419 installs (guardReads in MultisampleTarget.cpp) makes a CPU read throw when it overlaps a multisampled color target, whose samples are only on the host, or a resident native multisample depth target, instead of returning bytes that were never written there. #1193 (4651980) replaced the whole-page read with FlushGpuWrites, and copies each 64-byte block as the capture reads it. FlushGpuWrites runs the flush hook but not the guard, so a capture over such a target read the stale guest bytes silently.

The fix (commits 1-2). GuestMemory::GuardRead exposes the guard, and Read, ReadCommitted and ReadCommittedUnsynced use it. ShaderMemory::page calls it over the page where the whole-page read stood, before the flush, as Read does. So the check runs once per page a capture touches, at the same point and over the same range as before #1193, and costs what it did then: with no target registered, a few atomic loads. The block copies don't need it:

A sibling gap (commit 3). On a page that recorded GPU work writes, a capture reads each dword on its own. The dwords that the driver's write evidence says that work leaves unchanged (PendingWrite::Raw and VerifyRaw) are copied straight from guest memory. That path (29b4601) predates the guard (#419) and never ran it, unlike the word reads through GuestMemory::Read. It now runs GuardRead over the dword first: a few atomic loads per raw word when nothing is registered, next to the pending-write query each raw word already makes. With it, every guest read a capture makes runs the guard: whole pages, lazy blocks (through their page), synced words and raw words. Known values (PendingWrite::KnownValue) read no guest memory.

The commits:

  1. refactor(agc): expose the guest read guard as GuestMemory::GuardRead. No behaviour change.
  2. fix(agc): run the read guard over a shader capture's lazily read pages, with its test.
  3. fix(agc): run the read guard on a shader capture's raw word reads, with its test.

Scope: the driver's other direct guest reads aren't shader captures, weren't guarded before #1193, and #1193 didn't touch them. They are buffer uploads and input snapshots (ShaderResources.cpp, GuestBufferMemory.cpp), the write-evidence copies taken before a hook read (VariantValidation.cpp, CaptureValidation.cpp, the recorder) and copy count records (BufferCopy.cpp). They lie outside the guard as #419 defines it (CPU reads through GuestMemory::Read and ReadCommitted, DMA_DATA copies, texture and storage image lookups), and this PR leaves them as they are.

Tested

  • agc_multisample_guest_access (multisampleGuestAccessTests in tests/Recorder.cpp), next to the existing refusals of plain reads of a live 4x target:
    • a capture of a shader whose SRT lies in the target's memory must throw "a CPU read"; one whose SRT lies just past the target must read normally; the first again, now on a pooled page, must throw;
    • with a pending-write query that answers Raw for every page and word, so the capture reads word by word and raw, the capture must throw, and one past the target must read normally; with Sync, the word reads that were already guarded must throw too.
  • GitHub's runners, on my fork:
    • Commit 2, on main 92e5041: with main's ShaderMemory.cpp and GuestMemory.cpp, agc_multisample_guest_access fails on Linux at "a shader capture of a resolved-only multisampled target's memory did not throw"; with the fix, the recorder binary's 39 tests pass 20 of 20 (run).
    • Commit 3: with commit 2's ShaderMemory.cpp, the test fails on Linux at "a shader capture's raw word read of a resolved-only multisampled target's memory did not throw"; with commit 3 the 39 tests pass 20 of 20 (run).
    • GitHub's Windows runner has no Vulkan device, so the recorder tests skip there (41 of 42); the change builds with WinLibs GCC 15.2 and the libc and libkernel suites pass, apart from guest_kqueue, which fails on main in the same runs. The suites pass on Linux too (75 and 143 tests).
    • The ^agc sweep at the head (run): 602 of 602 pass on Linux (42 skipped). On Windows 594 of 596 pass, and both failures also fail without these commits: agc_driver_pm4_replay_tool exits with 0xc0000139 in every Windows sweep on GitHub's runners today, including branches without this change, and agc_interpolant_mapping printed that its tests passed and then timed out at exit, a hang we've also seen without this change.
  • Windows 11, NVIDIA GeForce RTX 3070 Laptop GPU (driver 617.42), WinLibs GCC 15.2, ANYPS5_REQUIRE_VULKAN=1, at this head: agc_multisample_guest_access passes 5 of 5. With main's guard code (GuestMemory.hpp, GuestMemory.cpp and ShaderMemory.cpp at 92e5041, the new test kept) it fails 5 of 5 with "a shader capture of a resolved-only multisampled target's memory did not throw".
  • python3 tools/check_conventions.py --base origin/main: passed.

Checklist

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.
Since boykopovar#1193 (4651980) 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 boykopovar#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 boykopovar#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.
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 (29b4601) predates the guard
(d109f39), 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.
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 6771 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp L40

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 1368 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 1395 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 4448 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 5021 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 5925 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6185 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6186 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6192 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6273 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6486 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6615 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6701 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6718 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6747 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6759 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6763 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6820 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6868 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6946 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6947 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/Execution/src/ShaderMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6960 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6970 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6971 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7201 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7233 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7258 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7332 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7371 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7432 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7505 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7604 core/libs/prx/libSceAgcDriver/Execution/include/GuestMemory.hpp
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp

@MichonGoddijn231849

Copy link
Copy Markdown
Contributor

Thanks, this is what I asked for on #1193. I read the diff: the guard runs once per page at first touch, where the whole-page read used to be, and the raw-word path now has it too. That closes the gap #1193 opened, and the gap from 29b4601 that predates #419. The test covers a fresh page, a pooled page, raw words and synced words, and checks that a capture just past the 64 KiB target still reads normally, on its own page within the 5 x 64 KiB block. Good from my side.

@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

Thanks for reading it through, @MichonGoddijn231849.

@adriadam10 adriadam10 self-assigned this Oct 19, 2026
@adriadam10
adriadam10 merged commit 9d4382d into boykopovar:main Oct 19, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc*

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants