Skip to content

fix(agc): wait for an image another thread is storing before a host read - #7769

Merged
oneandonlydean merged 2 commits into
boykopovar:mainfrom
jotitadev:fix/agc-flush-pending-wait-flushing
Oct 19, 2026
Merged

oneandonlydean merged 2 commits into
boykopovar:mainfrom
jotitadev:fix/agc-flush-pending-wait-flushing

Conversation

@jotitadev

@jotitadev jotitadev commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

What

Fixes the race reported in #7744. StorageTexture::FlushPending moves the overlapping images from pending.textures to pending.flushing, then stores them under the GPU mutex. A host read of the same range on another thread finds nothing in textures and returns without waiting, so it reads guest memory before the store lands. The queue-idle store (StoreIdleResults: GPU mutex, then FlushAllPending("idle")) is the usual other thread.

  • New StorageTexture::WaitForFlushing(address, bytes, except). When an image in flushing overlaps the range and this thread does not hold the GPU mutex, it takes the GPU mutex, releases it, and checks again until no overlapping image is left in flushing. It returns whether it waited.
  • The wait releases the mutex on every pass. A writer at depth 1 can release the mutex inside Recorder::SyncThrough. It also releases the mutex before it erases its images from flushing.
  • FlushPending calls it when it lists no image and returns true if it waited. That return is what tells the flush hook to sync the recorded store. Without it, the read still saw zeros after the wait.
  • The flush hook (FlushForAccess) calls it on its C4 path, which skips FlushPending when AccessKeptByCpu lists no pending image. That is the path a file write's host read takes by default. A fix in FlushPending alone left the repro at 200 of 200.
  • flushing is empty almost always. An atomic flushingCount, written under the registry mutex next to each change of flushing, keeps the hot path to one atomic load.

Lock order: WaitForFlushing never takes the GPU mutex while it holds the registry mutex. StoreIdleResults, FlushAllPending and the Unregister of FlushPending take the GPU mutex first and the registry mutex second, so the order stays the same. A thread that already holds the GPU mutex does not wait: the other writer may be blocked on that mutex. Readers on the hook path already take the GPU mutex (AccessKeptByCpu, the recorder sync), so the wait adds no new lock order.

agc_driver_storage_read_during_idle_store is a new test that reproduces #7744. A thread takes the GPU mutex and runs FlushAllPending. The main thread waits until FindPending returns null for the surface, so the image is in flushing, and then calls GuestMemory::Read. This runs for 20 rounds, and the test fails if any read sees the bytes from before the store. It covers the flush hook path, which a plain GuestMemory::Read takes.

agc_driver_storage_file_write also gets the concurrent flush case from #7757, by @MarcZX10 (his test only, with co-authorship in the commit). One thread's FlushPending stores the surface while another thread holds the GPU mutex, and a second thread calls FlushPending and then GuestMemory::Read on the same range. That covers a reader that calls FlushPending itself. Folded in at @oneandonlydean's suggestion in #7757.

Tested

Current head ba72cb78f merged onto main 118266aec (clean), same machine and drivers as below, ANYPS5_NO_SHADER_CACHE=1:

main + only the two test files this change
agc_driver_storage_file_write, lavapipe fails 5 of 5: concurrent guest read returned before an overlapping image was stored passes 5 of 5
agc_driver_storage_file_write, RTX 3070 fails 5 of 5, same message passes 5 of 5
agc_driver_storage_read_during_idle_store, lavapipe fails 5 of 5 (13 to 18 of 20 reads stale) passes 5 of 5
agc_driver_storage_read_during_idle_store, RTX 3070 fails 5 of 5 (20 of 20 reads stale) passes 5 of 5
  • ctest -R "^agc" (-j4) on main 118266aec: lavapipe 787 of 787, RTX 3070 782 of 782 (without agc_display_buffer_dcc, agc_driver_flip and agc_driver_lds_atomics64). With this change: 788 of 788 and 783 of 783, the same skips. agc_driver_bda_device now passes on main here too.
  • check_conventions.py --base origin/main and check_test_registration.py pass.
  • @oneandonlydean reproduced it on an RTX 4090 (Linux) in fix(agc): wait for overlapping storage flushes #7757, and @thuanquang on an RX 6700 XT (Windows) in this PR.

Earlier runs, before the #7757 test was added:

Intel i7-8700, Ubuntu 26.04, GCC 15.2, Release, BUILD_TESTING=ON, this branch on main 63e4a6380. Earlier runs on main 9f5d4bd92 gave the same results. lavapipe is Mesa 26.0.3 with GALLIUM_OVERRIDE_CPU_CAPS=avx.

main this change
New test, lavapipe fails 5 of 5 (12 to 17 of 20 reads stale) passes 20 of 20 (0 stale)
New test, RTX 3070 fails 5 of 5 (20 of 20 reads stale) passes 20 of 20 (0 stale)
#7744 repro, 200 reads, lavapipe 190 stale (in #7744) 0
#7744 repro, 200 reads, RTX 3070 191 stale (in #7744) 0
agc_driver_storage_file_write "wrote only zeros", lavapipe, SIGSTOP 70 ms every 20 ms, 100 runs 3 0 (twice)
Same, RTX 3070, 50 runs 2 0 (twice)

Not tested: Windows. The change uses only GpuMutex, std::atomic and std::this_thread::yield.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: Fixes agc: a host read on another thread does not wait for an image the queue-idle store is writing back #7744
  • Unblocks: none
  • GPUs tested: NVIDIA RTX 3070 (NVIDIA 595.99.02, Ubuntu 26.04), lavapipe (Mesa 26.0.3, Ubuntu 26.04)
  • AI-assisted: yes (Claude Code)

🤖 Generated with Claude Code

FlushPending moves the overlapping images from the pending list to the
flushing list and stores them under the GPU mutex. A FlushPending or a
flush hook on another thread for the same range found nothing pending and
returned while the store was still running, so the host read saw guest
memory before the store; the queue-idle store hits this with any host read.

When an overlapping image is being stored and this thread does not hold
the GPU mutex, take it and check again until no overlapping image is left
being stored, and report the range as stored so the hook syncs the
recorded store.

Fixes boykopovar#7744
@github-actions

github-actions Bot commented Oct 19, 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 6682 core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp L1058-1059
PR 7757 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp L1492, L3675-3677, L3690-3692

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/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 1395 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 4448 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 5431 core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 5609 core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6185 core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 6186 core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
PR 6187 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6192 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6197 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6202 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6209 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6280 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6292 core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6457 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6479 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6539 core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6615 core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6683 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6718 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6730 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6820 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6946 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6947 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6960 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6971 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6973 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6977 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6987 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6989 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6995 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7002 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7048 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7085 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7256 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7332 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7427 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7449 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
PR 7514 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7604 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7642 core/libs/prx/libSceAgcDriver/Graphics/include/Texture.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7800 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp

@github-actions github-actions Bot added fix error Closes an issue (Fixes #N) agc libSceAgc* labels Oct 19, 2026
@thuanquang

thuanquang commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

A Windows run, since the description lists Windows as not tested: RX 6700 XT (gfx1031, RDNA 2), AMD driver 32.0.21045.5002, Windows 11 26200, WinLibs GCC 15.2.0 r7, Release. main 7f596df48, then the same tree with this PR's head 5937ef03e merged (clean merge).

agc_driver_storage_read_during_idle_store runs failed
main + only the new test 10 9: idle store read: a host read did not wait for the image another thread was storing
main + this PR 10 0: 0 of 20 host reads saw the memory before the store

So the race reproduces on Windows with AMD's driver, and the PR closes it. The runs are also shorter with the PR: about 1.1 to 1.6 s each, against 4.9 to 6.7 s on main.

With the PR, and every related test executable rebuilt against it, the 33 tests matching storage|idle|flush|host_read|write_back pass in each of three ctest -j4 runs, and agc_driver_storage_file_write passes 20 of 20. (An earlier version of this comment mentioned two agc_driver_storage_file_write failures. They happened only in my first run, where that test's executable was still built against main's headers while the library had this PR. Rebuilt, it doesn't fail, so please disregard them.)

Add the concurrent flush case from boykopovar#7757 to agc_driver_storage_file_write:
while one thread's FlushPending stores the surface under a held GPU
mutex, a second thread calls FlushPending and then GuestMemory::Read on
the same range, and the read must see the stored bytes. Only the test
from boykopovar#7757 is taken here; agc_driver_storage_read_during_idle_store
keeps covering the flush hook path.

Co-authored-by: MarcZX10 <67475268+MarcZX10@users.noreply.github.com>
@jotitadev

Copy link
Copy Markdown
Contributor Author

Thanks @oneandonlydean for reproducing it on the 4090. I added the concurrent FlushPending reader case from #7757 to agc_driver_storage_file_write in ba72cb78f, keeping @MarcZX10's authorship (test only, co-authored commit).

Head ba72cb78f on main 118266aec, i7-8700, Ubuntu 26.04, RTX 3070 (595.99.02) and lavapipe (Mesa 26.0.3), 5 runs each:

main + only the test files this PR
agc_driver_storage_file_write fails 5 of 5 on both: concurrent guest read returned before an overlapping image was stored passes 5 of 5 on both
agc_driver_storage_read_during_idle_store fails 5 of 5 on both (13 to 20 of 20 reads stale) passes 5 of 5 on both

ctest -R "^agc" matches main apart from the new test (lavapipe 788 of 788, RTX 3070 783 of 783). Conventions and test registration pass. The description is updated.

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

@thuanquang

Copy link
Copy Markdown
Contributor

The new head ba72cb78f on Windows/AMD: RX 6700 XT (gfx1031), driver 32.0.21045.5002, Windows 11 26200, WinLibs GCC 15.2.0 r7, Release, main 7a36de66b. Full builds in both columns, so every test executable matches its tree.

10 runs each main + only this PR's two test files main + this PR
agc_driver_storage_read_during_idle_store 10 fail: a host read did not wait for the image another thread was storing 10 pass
agc_driver_storage_file_write (with the new concurrent FlushPending reader) 10 fail: concurrent guest read returned before an overlapping image was stored 10 pass
the 33 tests matching storage|idle|flush|host_read|write_back (-j4) those two fail, the other 31 pass 33 pass

@oneandonlydean
oneandonlydean merged commit 535587c into boykopovar:main Oct 19, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* fix error Closes an issue (Fixes #N)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

agc: a host read on another thread does not wait for an image the queue-idle store is writing back

3 participants