Skip to content

fix(agc): wait for the idle flush's unit-shadow publish before returning - #7984

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
fisherjoey:fix/idle-publish-wait
Oct 21, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
fisherjoey:fix/idle-publish-wait

Conversation

@fisherjoey

@fisherjoey fisherjoey commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

What

The idle flush now waits for its unit-shadow publish before it returns.

VulkanDevice::StoreIdleResults runs StorageTexture::FlushAllPending("idle") and then SubmitRecorded, which does not wait. The flush ends in PublishShadow, or in PublishShadowsOnly when no image is pending. Both go through publishUnits, which decides on the CPU which units to store, and then recordCopies, which records the slab to import vkCmdCopyBuffer into the open batch. The bytes reach guest memory only when the GPU runs that copy. A guest store made between the decision and the copy is overwritten with the older shadow bytes, and since no reader goes through the flush hook at that point, nothing waits for the pending write.

With this change, FlushPending and PublishShadowsOnly sync the recorder under the GPU mutex they already hold, after an idle publish that stored at least one unit. Other flush reasons are unchanged.

The memory-access hook already waits right after its flush (FlushForAccess calls SyncThrough before the accessing thread goes on), so this PR leaves it alone. Our Demon's Souls runs below waited in both places. On the hook path the only difference was that the wait ran inside the lock, so I kept it out of this change.

Why

Demon's Souls never starts the level load after New Game in 3 of 4 runs on our test set (#7983). Each stalled run had one large idle or hook publish at New Game over memory the title was writing. With the publish waited, 4 of 4 runs loaded, at the same frame rate.

Related, not overlapping:

Tested

  • New test agc_idle_publish_wait (tests/Recorder.cpp): two units are retiled into a unit shadow, FlushAllPending("idle") runs, then the CPU stores into both units and the recorder syncs. The stores have to survive, and a "label" flush's publish has to stay recorded. Without the Texture.cpp change it fails with the idle flush returned before its publish's copies completed; with it, it passes 5 of 5 repeats. Both results hold on lavapipe and on the RTX 5070 Ti, on this branch at main dbac9fa.
  • The other unit-shadow, storage and pending-write tests (agc_unit_shadow, agc_driver_unit_shadow, agc_driver_unit_shadow_budget, agc_shadowed_key_stores, agc_pending_key_stores, agc_pending_write_snapshot, agc_storage_*, agc_space_pending_writes): pass on the RTX 5070 Ti; on lavapipe the four unit-shadow ones skip as before (no watched imports) and the rest pass. tools/check_conventions.py --base origin/main passes.
  • Demon's Souls PPSA01341 v1.07, Linux (Nobara, kernel 7.2), RTX 5070 Ti, driver 595.91, 900 s per run from boot through New Game. The test base was main 93e54ce plus local patches: a stencil-export workaround, fix(agc): keep texture images inside their extent's mip chain #6280, a cherry-pick of fix(agc): reuse a recycled descriptor set only for a table of its own size #6567, a sampled-texture snapshot sharing patch, an ImportWatched page-digest fix, KytyPS5's job-worker idle sleep (on in every run), and a diagnostic that logs CPU stores into blocks a GPU-timeline store just covered. Without a wait, 1 of 4 runs loaded the level. In the 3 stalled runs, one publish at New Game stored 67 to 73 64 KiB blocks that the CPU wrote afterwards. With the idle and hook publishes waited, 4 of 4 loaded. The New Game burst (50 to 62 blocks) was still there, and the largest waited publish was 1034 units (64.6 MiB) in 8.8 ms. A measuring pair gave 12.34 presents per 10 s with the wait and 12.34 without.
  • Plain main dbac9fa with only this branch, no local patches: 3 of 3 runs started the level load after New Game with no crash (one with a cold shader cache, one reached the level). I have no unpatched plain-main runs to compare stall rates against, so this shows the change is safe on main, not that it removes a stall there.

Fixes #7983

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 in TechnicalDebt; this change adds none
  • 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; none added
  • Generic behaviour, not specific to one title
  • Depends on: none
  • GPUs tested: lavapipe (Mesa), Linux, for the tests; NVIDIA GeForce RTX 5070 Ti, driver 595.91, Linux (Nobara, kernel 7.2), for the tests and the game runs
  • AI-assisted: yes

AI-assisted: yes

@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 6182 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1607
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp L10173
PR 6185 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1607
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp L10173
PR 6642 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1573, L1607
PR 6682 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1573
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp L3676-3677, L3769, L3935
PR 7457 core/libs/prx/libSceAgcDriver/CMakeLists.txt L1607
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp L4679

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/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 1395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 2434 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 4448 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 5021 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 5371 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5431 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5499 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5609 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 5925 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6044 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6186 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6187 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6188 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6192 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6197 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6202 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6209 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6280 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6300 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6412 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6481 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6501 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6539 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6747 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6788 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6808 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6868 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6946 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6947 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6960 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6971 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7048 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7201 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7256 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7275 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7310 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7505 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7508 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7514 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7604 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7642 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7800 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 8014 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8246 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 8484 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 8527 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 8552 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8564 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8592 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8672 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 9020 core/libs/prx/libSceAgcDriver/CMakeLists.txt

@github-actions github-actions Bot added the fix error Closes an issue (Fixes #N) label Oct 19, 2026
@MarcZX10

Copy link
Copy Markdown
Contributor

One integration detail from the overlap check: open #6642 adds the full recorder CTest entry and Windows environment list in CMakeLists.txt; open #7457 adds import-miss tests in that file and Recorder.cpp. Their behavior is separate from this idle-publish fix, so I do not see a dependency, but the registration/list hunks will need careful combination if they land in a different order. Could you include them in the related/rebase notes so none of the test entries get dropped?

@fisherjoey

Copy link
Copy Markdown
Contributor Author

Thanks, added both to the related notes: #6642 and #7457 register tests in the same CMakeLists.txt lists (and #7457 in Recorder.cpp), so whichever lands second has to keep every entry when the lists are combined.

AI-assisted: yes

@fisherjoey

Copy link
Copy Markdown
Contributor Author

Smoke runs on plain main dbac9fa with only this PR applied (no local patches at all), Demon's Souls, RTX 5070 Ti, 900 s each:

Run Shader cache New Game -> level load started Crash
235 cold yes (t+774) none
236 warm title yes (t+188) none
237 warm yes, reached the level none

So the wait is safe on main. These runs do not show the stall rate on plain main without the change, because I have no unpatched plain-main runs to compare against; the 3 in 4 stall figure in the description is from our test set. One related data point: a current-main-based run without this change (run226, base main 6762694 + #6280 + #6926 + #7844 + a local stencil workaround) died with a guest SIGSEGV during the level load, and its stack shares four guest frames with the two heap-overwrite crashes described in #7983. That fits the same mechanism but does not prove it.

AI-assisted: yes

@MarcZX10

Copy link
Copy Markdown
Contributor

Thanks for the plain-main smoke runs. Three runs reaching the level without a crash are useful safety evidence, and the caveat that there is no unpatched plain-main baseline keeps the comparison appropriately scoped. The PR description still says the current-main smoke run is queued; please replace that line with this result and its limitation so the summary reflects the latest evidence.

@fisherjoey

Copy link
Copy Markdown
Contributor Author

Done, the queued line is replaced with the three plain-main runs and the no-baseline caveat.

AI-assisted: yes

The idle flush (VulkanDevice::StoreIdleResults) publishes the unit shadows over guest memory into
the host import and only submits the copies. publishUnits picks the units to store on the CPU,
and the slab -> import copy lands when the GPU runs it, so a guest store made between the two is
overwritten with the older shadow bytes. The memory-access hook waits through SyncThrough right
after its flush; the idle flush has no reader that waits.

FlushPending and PublishShadowsOnly now sync the recorder under the GPU mutex after an idle
publish that stored units. Other flush reasons are unchanged.

Test: agc_idle_publish_wait (a CPU store made after the idle flush returns survives; a label
flush's publish stays recorded).

@CnRJay CnRJay left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The window it closes is clear (the idle flush records copies of a unit-shadow publish into the open batch and returns before the GPU runs them, so a guest store in between is overwritten), and the evidence is good: 1 of 4 runs loaded without the wait, 4 of 4 with it, at 12.34 presents per 10 s in both arms.

One point on where the wait happens. The PR syncs the recorder "under the GPU mutex they already hold", and notes that it left the hook path alone because there "the wait ran inside the lock". The same objection applies here: while the idle publish waits, every other queue that needs the GPU mutex waits with it. The largest waited publish was 1,034 units (64.6 MiB) in 8.8 ms, which is fine in the measured run, but the wait grows with the amount published (the New Game burst was 50-62 blocks, other titles will publish more), and the time is spent with the packet worker, the draw committer and any compute queue blocked.

  • #6926 releases the GPU mutex for the wait when the acquisition is the outermost, as for any CPU access. If the idle flush can do the same (publish under the lock, release it, wait for the copies, and let the callers that need ordering take the lock again), the stall disappears from the other queues without changing what the flush guarantees. Please say why that is not possible here.
  • Whichever way it ends up, add the waited publish time to the [storage] line (units published, time waited, longest) so the cost is visible on titles other than Demon's Souls, and post it for the New Game burst.
  • The 12.34 against 12.34 pair is a single measuring pair on a base of main plus local patches. A repeat on plain main, as the last bullet of the description says is missing, would also show that the wait does not cost frame time where there is no stall to remove.

@adriadam10
adriadam10 merged commit eb69286 into boykopovar:main Oct 21, 2026
10 checks passed
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked that the wait is scoped to the "idle" reason and skipped inside a completion, that it follows the existing sync-under-GpuMutex pattern, and that agc_idle_publish_wait fails without the Texture.cpp change (pending write left after the idle flush) and is registered in ctest and the Windows path list; merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

agc: the idle flush does not wait for its unit-shadow publish, so a CPU store made before the copy runs is lost (Demon's Souls New Game stall)

4 participants