Skip to content

fix(agc): order deferred write-backs after queued draws' index reads - #6914

Merged
adriadam10 merged 8 commits into
boykopovar:mainfrom
moong15:pr/queued-index-read-order
Oct 19, 2026
Merged

adriadam10 merged 8 commits into
boykopovar:mainfrom
moong15:pr/queued-index-read-order

Conversation

@moong15

@moong15 moong15 commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

A pipelined draw (the draw pipeline, depth 8) reads its index buffer from guest memory when it is committed, not when its packet is processed. By then the packets queued behind it may already have run, and a later dispatch's deferred storage-image write-back can land in the same bytes first. The draw then reads indices that are really image texels.

Stellar Blade hits this every time the main menu loads. Each frame it sets INDEX_BASE to the same allocation for a 6-index, 32-bit full-screen quad, then recycles that allocation for a 66x66 storage image (format 75). Once the image's write-back overtakes the queued quad, the quad's indices are garbage and the frame aborts:

[index-base] pipeline-failure serial=8486 queue=0x0 address=0x217e120000 count=6 elementBytes=4 error=AGC graphics: vertex fetch exceeds descriptor record count

Change:

  • DrawPipeline::HeldReads records the guest ranges each queued draw still reads at commit: its index range, and the extent of every guest buffer descriptor and vertex attribute its stages bind. A pipelined DMA copy holds its source range the same way.
  • A deferred storage-image write-back over a held range stays pending until the draws holding it have committed (Texture.cpp). A WAIT_REG_MEM over that range stores it when it polls (WaitMemory.cpp), so the wait is satisfied without waiting for another flush.
  • APS5_NO_QUEUED_DRAW_READ_ORDER=1 restores the old order.
  • The [draw] pipelined profile line reports read ranges and stores held, and the [storage] line counts the stores released after the reads committed.
  • New test agc_driver_queued_index_read_order, in three phases: a queued draw's index read and its vertex read must still see their original bytes when a later dispatch's write-back covers them, and a WAIT_REG_MEM over a held range, whose value only the deferred write-back produces, must be satisfied.

Tested

Windows 11, MinGW build. Stellar Blade (PPSA13197 v01.141), same path to the main menu each run:

Run Fix Result
1-5 not present index crash at the menu, every run
6 on menu reached, 27 stores held
7 (warm cache) on menu reached, 324 and 65 stores held
8 off (APS5_NO_QUEUED_DRAW_READ_ORDER=1) same crash at the menu

Holding the stores had no measurable cost on the [draw] pipelined line.

  • Full ctest -j 8 on this branch: 1348/1352. The four failures also fail on this machine without this change: agc_driver_quad_pixel_mask and agc_driver_layered_volume_vertex_no_gpl (fail on main), select_tests (needs symlink rights) and agc_driver_gpu_map_on_fiber.
  • agc_driver_queued_index_read_order passes. It fails with APS5_NO_QUEUED_DRAW_READ_ORDER=1, when only the index range is held (vertex phase), and without the write-back store from the wait (wait phase).
  • 160 runs of agc_driver_queued_index_read_order, 8 in parallel, with ANYPS5_NO_SHADER_CACHE=1: 0 failures.

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
  • No silent stubs or new unimplemented paths
  • No notes, investigation .md files or images added to the repository
  • No third-party code added
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: PPSA13197 (Stellar Blade) main menu
  • GPUs tested: AMD Radeon RX 9070 XT, driver 32.0.31041.1004, Windows 11 Pro 10.0.26300
  • AI-assisted: yes

🤖 Generated with Claude Code

@moong15
moong15 force-pushed the pr/queued-index-read-order branch from 408f564 to ec5175a Compare October 16, 2026 18:02
@github-actions

github-actions Bot commented Oct 16, 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 1896 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp L611
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp L129
PR 5431 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp L604-605
PR 6804 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp L604-605
PR 7427 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp L143
PR 7912 docs/dev/debt/libSceAgcDriver.md L147

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 1341 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 1368 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 1395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 4448 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 5021 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp
PR 5058 docs/dev/debt/libSceAgcDriver.md
PR 5371 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5404 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
PR 5426 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5441 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
PR 5499 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 5609 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 5656 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5925 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6044 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6146 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6185 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6186 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6187 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6188 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6192 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6194 docs/dev/debt/libSceAgcDriver.md
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
PR 6223 docs/dev/debt/libSceAgcDriver.md
PR 6252 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Synchronization/WaitMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6280 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6292 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6300 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6501 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp
PR 6539 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6548 docs/dev/debt/libSceAgcDriver.md
PR 6549 docs/dev/debt/libSceAgcDriver.md
PR 6588 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6611 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6615 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6636 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
docs/dev/ENVIRONMENT.md
docs/dev/debt/libSceAgcDriver.md
PR 6642 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6683 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6727 docs/dev/debt/libSceAgcDriver.md
PR 6741 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6747 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6788 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6808 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6867 docs/dev/debt/libSceAgcDriver.md
PR 6868 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6887 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6937 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6946 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/ENVIRONMENT.md
docs/dev/debt/libSceAgcDriver.md
PR 6947 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/ENVIRONMENT.md
docs/dev/debt/libSceAgcDriver.md
PR 6960 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Packets/PacketMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6965 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6971 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6973 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6977 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6987 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6989 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6995 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7002 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7048 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7085 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7201 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
PR 7223 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7246 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp
PR 7254 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7256 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7258 docs/dev/debt/libSceAgcDriver.md
PR 7369 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
PR 7390 docs/dev/debt/libSceAgcDriver.md
PR 7395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7417 docs/dev/ENVIRONMENT.md
PR 7426 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7432 docs/dev/debt/libSceAgcDriver.md
PR 7446 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7449 docs/dev/debt/libSceAgcDriver.md
PR 7484 docs/dev/ENVIRONMENT.md
PR 7505 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
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
docs/dev/ENVIRONMENT.md
docs/dev/debt/libSceAgcDriver.md
PR 7622 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp
PR 7642 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/include/Driver/Draw/DrawPipeline.hpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawPipeline.cpp
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Memory/WrittenBuffers.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/ENVIRONMENT.md
docs/dev/debt/libSceAgcDriver.md
PR 7659 docs/dev/debt/libSceAgcDriver.md
PR 7683 docs/dev/ENVIRONMENT.md
docs/dev/debt/libSceAgcDriver.md
PR 7800 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md

@Dextroid17

Copy link
Copy Markdown
Contributor

@moong15 thanks for this. The new test agc_driver_queued_index_read_order is flaky on lavapipe when it runs with no shader cache next to other tests, which is how CI runs it (CONTRIBUTING.md: parallel ctest, first run with no shader cache). Its last check fails:

AGC graphics: queued index read: the image's results never reached the memory they cover, so the run above proves nothing

Cause, from my runs: after WaitForCommits() nothing is queued, so the queue worker's idle store (StoreIdleResults → FlushAllPending("idle"), after the 50 ms quiet period in QueueWorker.cpp) can take the pending image first. In a traced failing run, the idle flush appears after WaitForCommits(), and at line 284 StorageTexture::AnyPending() was already false with the indices still intact. FlushGpuWrites(Pool, PoolBytes) + AgcDriverWaitIdle_nid_postfix() then return before that write-back shows up in Pool. With a probe that polled IndicesIntact() after the final check, the bytes landed 2–9 ms later in all 9 hits out of 60 runs. So the write-back arrives late; nothing is lost. The order check at line 273 passed in every run, so the production change looks fine here.

I did not check whether FlushGpuWrites should also wait for an idle write-back that is already in flight. If it should, that is a separate issue in code this PR does not change.

Test-side fix for core/libs/prx/libSceAgcDriver/tests/execution/QueuedIndexReadOrder.cpp, replacing lines 284–286. I built and ran this exact code (results below):

        const auto landBy = std::chrono::steady_clock::now() + std::chrono::seconds(5);
        do {
            AgcDriver::GuestMemory::FlushGpuWrites(reinterpret_cast<std::uint64_t>(reinterpret_cast<std::uintptr_t>(Pool)), PoolBytes);
            AgcDriverWaitIdle_nid_postfix();
        } while (IndicesIntact() && std::chrono::steady_clock::now() < landBy);
        Require(!IndicesIntact(), "queued index read: the image's results never reached the memory they cover, so the run above proves nothing");
tree / condition result
head, ctest -V -R '^agc_driver_queued_index_read_order$', 3 runs 3/3 pass (0.40–0.75 s)
head, test binary, warm Mesa cache, 12 copies at once, 5 × 60 runs 300/300 pass
head, test binary, fresh MESA_SHADER_CACHE_DIR per run, one at a time, 60 runs 60/60 pass
head, test binary, fresh MESA_SHADER_CACHE_DIR per run, 12 copies at once, 3 × 60 runs 28, 37, 36 of 60 fail (message above)
head + the fix above, same 12-copy fresh-cache loop, 3 × 60 runs 180/180 pass
head + the fix above + APS5_NO_QUEUED_DRAW_READ_ORDER=1, 10 runs 10/10 fail with "the queued draw's commit stored the image's results over its index buffer" (expected)
production files reverted to main, test kept, ctest, 3 runs 3/3 fail with that same expected message

The negative control works. check_conventions.py passes. The head merges cleanly with main at c842491 (13 commits after your base, none touching the files this PR changes), but I did not run the test on the merged tree.

Setup: Linux, lavapipe Mesa 26.2.4 (LLVM 23.1.1), default CMake build (CMAKE_BUILD_TYPE empty), clean env -i, ANYPS5_NO_SHADER_CACHE=1 APS5_NO_PIPELINE_JOURNAL=1 for the direct binary loops, head ec5175a.

AI-assisted: yes

@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 cause is well shown (a queued draw reads its indices at commit, a later write-back lands in the same bytes first, Stellar Blade reproduces it on every menu load, 5 of 5 runs fail without the fix and run 8 with the switch off fails again). The test fails with the switch set. Two questions and one measurement.

  • HeldReads records only the index range of a queued draw. What else does a queued draw read from guest memory at commit rather than at prepare? Indirect argument buffers, vertex data fetched through a BDA range, and descriptor words that are re-read in commitDraw would be held to the same bug. Please list what commitDraw reads from guest memory and show that the others are snapshotted at prepare, so this is the whole class and not one instance.
  • A write-back that stays pending until the draws holding the range have committed moves a store later. What happens when the draws never commit because the pipeline is blocked behind a label that waits on this write-back (a store over the held range that a WAIT_REG_MEM depends on)? The hold must not be able to wait on itself; a test with a label wait over a held range would show it.
  • "No measurable cost on the [draw] pipelined line" is one line from one run. Post the summary lines of main (switch off) and this PR at the menu, same scene, no APS5_PROFILE_*, since the write-backs it delays are on the path to the flip.

@moong15
moong15 force-pushed the pr/queued-index-read-order branch from ec5175a to a436481 Compare October 17, 2026 02:18
@moong15

moong15 commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Updated in 7471618.

What a queued draw reads at commit. commitDraw (Execution/src/Driver/Draw/Draw.cpp) runs VulkanDevice::Draw -> Graphics::Draw -> prepareDrawInputs. Every guest read on that path:

# Guest read Where Prepare or commit Held?
1 draw registers (context, shader, uconfig) -> decode->state, the draw key Draw.cpp:37, :99, :177 prepare (moved into the commit lambda) n/a
2 shader user data words (descriptor words in VGPRs) Draw.cpp:162, :181, :510, stages.push_back :407 prepare (in the RecompileResult) n/a
3 push constants Draw.cpp:406-407 prepare n/a
4 shader memory regions (BDA/SRT constants) Draw.cpp:250, :424-426, snapshot :458-463 prepare (Graphics::GuestMemorySnapshot byte copies) n/a
5 vertex-stage decode reads Draw.cpp:243 -> decodeReads -> :424-426 -> snapshot prepare n/a
6 indirect argument records Draw.cpp:496-498 Pm4::ReadDrawArguments prepare, and indirect draws never take the pipelined branch (Draw.cpp:577) n/a
7 index address/count/size Draw.cpp:37 ResolveDraw prepare n/a
8 mesh index buffer descriptor words Draw.cpp:183-191 prepare n/a
9 deferred label bytes PacketMemory.cpp:361-372 (copied into the queued item) prepare n/a
10 pipelined DMA store source bytes PacketMemory.cpp:279 store->Bytes() prepare n/a
11 index buffer, highest-index scan of live memory Graphics/src/Draw.cpp:1088 -> :957/:967 HighestDrawIndex (and the cached :963) commit yes (index range)
12 index buffer copy Graphics/src/Draw.cpp:1084 -> :910 GuestMemory::Read commit yes
13 index buffer, zero-fetch clamp copy Graphics/src/Draw.cpp:1105-1108 commit yes
14 index bytes, mesh restart table Graphics/src/Draw.cpp:1123-1124 commit yes
15 vertex buffer, in-place store trigger Graphics/src/Draw.cpp:1194 -> :938 FlushPending(..., "in-place vertex input") commit yes (new)
16 vertex buffer copies Graphics/src/Draw.cpp:1200 and :1210 -> :910 / :920 GuestMemory::Read commit was not; now yes
17 pipelined DMA copy source PacketMemory.cpp:311 GuestMemory::Read(copy.source, ...) inside the commit commit was not; now yes
18 colour/cmask original texels for register-clear materialization Graphics/src/Draw.cpp:127, :216, :237 via :140/:266 <- :1245/:1252 refreshResidentTarget commit, flush hook consulted no (see below)
19 texture surfaces compared against a descriptor ShaderResources.cpp:663, :677, :738-739, :2529, :2651 commit, but stamps/hook-consulted compares, not descriptor words n/a
20 mapping checks (CheckRange) Graphics/src/Draw.cpp:1042, :1085, :1193, :2506 commit, no data read n/a

Registers, user data, push constants, BDA/SRT constants, indirect arguments and descriptor words are read or snapshotted at prepare. Indirect draws never take the pipelined branch. At commit, the index buffer (11-14) was already held. The vertex buffer reads (16) and the pipelined DMA copy source (17) were not; both are held now. QueuedDrawReadRanges returns the index range plus the extent of every guest buffer descriptor and vertex attribute the draw's stages bind, and enqueueDmaPacket passes the copy source the same way. Every other commit-time read goes through GuestMemory::Read, whose flush hook reaches HeldReads, so the hold covers it as well. One read is not held: the colour/cmask texels a register clear materializes from the draw's own target (18). It goes through the hook, and it is the same aliasing the debt note already records for a surface read as index data, so it is recorded there.

Can the hold wait on itself. The draws always commit: the commit thread pops items without waiting on any label or drain, and the GPU mutex is always taken before the pipeline mutex, so there is no cycle. The real gap was in the other direction, as you suspected: a store that the hold refused was never retried. A WAIT_REG_MEM on that store's bytes would then spin, because waitMemory does not rerun FlushPending, and StoreIdleResults needs all queues idle, which is false while that worker waits. The refused range is now recorded, and the commit thread stores it as soon as no queued item reads it (DrawPipeline::storeDeferred). The [storage] line counts those stores.

Tests. agc_driver_queued_index_read_order now has three phases: the index read, the vertex read, and a WAIT_REG_MEM over a held range whose reference value only the deferred write-back produces, which must be satisfied within 10 s while the draw still reads its own bytes. Each phase fails without its part of the fix:

  • with APS5_NO_QUEUED_DRAW_READ_ORDER=1, the index phase fails
  • holding only the index range, the vertex phase fails ("the queued draw holds no read of its vertex range")
  • without the retry, the wait phase fails ("the wait over the held range was not satisfied ... after 10.0 s")

The test's vertex data is reached through a buffer descriptor, so the separate vertex-attribute loop has no case of its own.

160 runs, 8 in parallel, with ANYPS5_NO_SHADER_CACHE=1: 0 failures. Full ctest -j 8: 1348/1352. The 4 failures also fail without this change on this machine: agc_driver_quad_pixel_mask, agc_driver_layered_volume_vertex_no_gpl, select_tests (needs symlink rights) and agc_driver_gpu_map_on_fiber.

The menu summary lines, main against this PR, follow in a separate comment.

@moong15

moong15 commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my earlier reply: the retry I described there is gone. In game runs it stored a refused write-back after the holding draws committed, which could land after the title had already reused that memory for the next frame's indices, and the draw then read texels as indices. The hold now stays as it was, and a WAIT_REG_MEM over a held range stores the pending write-backs itself when it polls (WaitMemory.cpp), so the bytes appear only when something waits for them.

agc_driver_queued_index_read_order still covers it: without that flush the wait phase fails ("the wait over the held range was not satisfied ... after 10.0 s").

Same machine and scene, from start to the in-game scene with Enter pressed every 20 s:

  1. Fix on: 323 s, no vertex fetch errors.
  2. Fix off (APS5_NO_QUEUED_DRAW_READ_ORDER=1): "vertex fetch exceeds descriptor record count", then abort at 150 s, loading from the menu.
  3. Repeat of 1 and 2: the same result (fix on 380 s clean; fix off aborts at the menu, 165 s).

@TheSovietWaffle

Copy link
Copy Markdown
Contributor

NVIDIA and Windows data point at the current head 8d26c406a: Release, Windows 10 Pro 22H2 (19045), WinLibs GCC 15.2.0 r7, libraries rebuilt from the head. Device chosen with ANYPS5_GPU.

  • agc_driver_queued_index_read_order: NVIDIA RTX 3080 (617.42) 5 of 5 (device selected in the log, not skipped); AMD Radeon Graphics (Raphael iGPU, 1002:164e, AMD Windows driver) 3 of 3.
  • agc_driver_draw_cache_reuse, agc_driver_draw_cache_reuse_off, agc_driver_draw_cache_reuse_verify, agc_driver_draw_cache_reuse_inline, agc_driver_pipelined_dispatch and agc_driver_pipelined_dispatch_item pass on the RTX 3080.

The earlier head a436481ee gave the same results on the RTX 3080.

Not checked: whether the new test fails on main without the change on these GPUs, and titles.

AI-assisted: yes (Claude Code).

@jotitadev jotitadev 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.

A Linux run on NVIDIA and lavapipe, since the description has Windows on AMD only.

Machine: Ubuntu 26.04, i7-8700, GCC 15.2, Release, BUILD_TESTING=ON. NVIDIA GeForce RTX 3070 (driver 595.99.02) and lavapipe (Mesa 26.0.3, LLVM 21.1.8) on the same host. PR merged onto main 12a71f3f5 (clean). ANYPS5_NO_SHADER_CACHE=1, -j1 --timeout 30.

Run RTX 3070 lavapipe
agc_driver_queued_index_read_order pass 3/3 pass
same, --repeat until-fail:30, serial 30/30 30/30
same, APS5_NO_QUEUED_DRAW_READ_ORDER=1 fails: queued index read: the queued draw's commit stored the image's results over its index buffer same failure
^agc (637 tests), PR vs main no new failures (635/637; agc_driver_f16_fma_denormal_modes and agc_driver_fma_mix_f16_denormal_modes fail on main on this GPU too) 637/637 on both

I ran the repeats serially only, so this does not cover the parallel, cold-cache case @Dextroid17 reported.

Not run: Windows, AMD, Stellar Blade or any title.

GPUs tested: NVIDIA GeForce RTX 3070 (595.99.02) and lavapipe (Mesa 26.0.3), Ubuntu 26.04

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

@marco-scheffler

Copy link
Copy Markdown

Linux NVIDIA data point: Ubuntu 24.04.4, RTX 4000 SFF Ada Generation, driver 595.99.02, GCC 13.3.0, Release. This PR at 8d26c406a merged onto main c9e6c45de, full ctest on the RTX (ANYPS5_GPU=RTX, Xvfb with -DANYPS5_VIDEO_OUT_TEST_DRIVER=x11): no new failures. The only failures, video_out_flip_eop{,_main,_aftergpu} (#6616) and agc_driver_{f16_fma,fma_mix_f16}_denormal_modes, fail the same way on that main (0/5 serial with and without this PR).

@moong15
moong15 force-pushed the pr/queued-index-read-order branch from 8d26c40 to ce35d8f Compare October 17, 2026 14:22
@moong15

moong15 commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main 47a266c with no conflicts. After the rebase, agc_driver_queued_index_read_order failed in about 1 of 7 runs while the machine was busy, with "the queued draw holds no read of its vertex range". The cause is a race in the test. In the vertex phase, the gate item let the draw commit as soon as it was queued, so the commit thread could run it before the test thread checked HeldReads. The gate now stays closed until the test has checked the hold. No production code changed. With that change the test passed 30 of 30 runs one at a time, and every repeat passed under ctest -j12 --repeat until-fail:8 next to the other pipelined and draw-cache tests with ANYPS5_NO_SHADER_CACHE=1. It still fails with APS5_NO_QUEUED_DRAW_READ_ORDER=1, with "the queued draw's commit stored the image's results over its index buffer". check_conventions.py passes.

@CnRJay, about your third point, the menu summary lines. These come from one build (the game branch carrying this fix) with the fix on and with APS5_NO_QUEUED_DRAW_READ_ORDER=1, which puts back what main does. No APS5_PROFILE_*. Same scene: the title menu, from the first menu window until loading starts. Windows are 10 s. Windows, RX 9070 XT, 3840x2160.

Pair Window Fix on: presents, p50 / p90 ms, Driver.Packet ms/frame Fix off: presents, p50 / p90 ms, Driver.Packet ms/frame
1 (runs 1 / 2) 10-20 s 217, 40.3 / 44.6, 91.6 242, 37.4 / 40.9, 82.6
1 20-30 s 247, 40.4 / 41.7, 80.6 127 (loading starts at ~25 s), 38.1 / 40.8
2 (runs 3 / 4) 10-20 s 193, 42.2 / 47.8, 108.8 191, 42.1 / 48.6, 110.5

The two pairs disagree. Pair 1 has the fix about 3 ms slower at p50, and pair 2 shows no difference. Two pairs can't settle a 3 ms gap at the menu. After the menu there is nothing to compare: with the fix off, every run aborts on the index read while loading from the menu (150 s and 165 s), and with it on, the game reaches the in-game scene (323 s and 380 s).

If a 3 ms menu cost would matter here, I can run another pair on this rebased head and post it.

@nicolasfalesy

Copy link
Copy Markdown
Contributor

Tested on Linux (Arch), c++ (GCC) 16.2.1 20260810, Release, BUILD_TESTING=ON, main at e3838332ee31af787ad311b7eadbe53d8d012e95. GPU tests ran on lavapipe (llvmpipe, LLVM 22.1.8).

#6656, #6914 and #6249 together fail two tests that #6249 adds, and #6656, #6914, #6249 and #6146 together fail one that #6146 adds. Each is a three-way interaction: no pair of the PRs fails (2 of 2 for every row, main has none of these tests).

PRs merged on main Tests run Result
#6249 agc_driver_copy_surface_bytes, agc_driver_copy_surface_bytes_no_alias pass
#6249 + #6656 same pass
#6249 + #6914 same pass
#6249 + #6656 + #6914 same both fail
#6146 agc_driver_depth_expand_out_of_place pass
#6146 + #6656 + #6914 same pass
#6146 + #6249 + #6656 same pass
#6146 + #6249 + #6914 same pass
#6146 + #6249 + #6656 + #6914 same fails
agc_driver_copy_surface_bytes:           AGC graphics: surface byte copy: image alias did not preserve the raw byte copy
agc_driver_copy_surface_bytes_no_alias:  AGC graphics: surface byte copy: image alias did not preserve the raw byte copy
agc_driver_depth_expand_out_of_place:    AGC graphics: depth expand: a DMA_DATA copy of the write base does not hold the read surface's depth

(CopySurfaceBytes.cpp:257 on #6249's head and DepthExpandOutOfPlace.cpp:900 on #6146's head.)

What I found: the copy holds back its own source. #6656 makes enqueueDmaPacket queue DMA_DATA copies on the draw pipeline by default (PacketMemory.cpp:271). #6914 lists the copy's source as a held read of that queued item (PacketMemory.cpp:313), and DrawPipeline::HeldReads makes StorageTexture::FlushPending return without storing anything while any queued item lists a read over the range (Texture.cpp:3218). The committer runs the item while it is still items.front() with its reads listed (DrawPipeline.cpp:157, popped only at :173). For a copy over 64 KiB, VulkanDevice::CopyBuffer then calls FlushPending(source, ..., "buffer copy source") (VulkanDevice.cpp:2660 on #6249's head) to store the GPU-produced image bytes before the buffer copy reads them. That call now sees the copy's own read, returns false, and the copy moves the stale guest bytes. Debug print on the three-PR merge: FlushPending held 0x7f..c0000 +0x20000 reason=buffer copy source, then incorrect_bytes=121865 for the 128x120 cases. The tests are #6249's because they are the only ones that copy a GPU-produced source through this path; the problem needs no #6249 code. #6146's test fails the same way: it passes with the fix below.

Evidence on the three-PR merge: APS5_NO_QUEUED_DRAW_READ_ORDER=1 (turns off HeldReads): both pass. APS5_NO_PIPELINE_DMA=1: both pass. Neither alone is the bug, the pair is.

Fix tried (two small hunks on top of #6914): DrawPipeline::ReleaseRunningReads() (committer thread: locks mutex, clears items.front().reads), called first thing in the DMA copy commit lambda in enqueueDmaPacket. The held read still protects the copy's source against stores queued ahead of it; it just stops holding it against the copy's own flush. Skipping items.front() inside HeldReads for every item does not work: it breaks #6914's own agc_driver_queued_index_read_order, because a draw's commit is meant to be held against its own reads. With the two hunks: agc_driver_copy_surface_bytes, agc_driver_copy_surface_bytes_no_alias, agc_driver_depth_expand_out_of_place, agc_driver_depth_expand_out_of_place_inline and agc_driver_queued_index_read_order pass 5 of 5, twice, on the merge of all four PRs.

Who should act: #6914's author (the fix belongs next to their held read; the read hold is what is new). #6656 only turned the path on by default.

AI-assisted: yes

@Dextroid17

Copy link
Copy Markdown
Contributor

A second host for @nicolasfalesy's #6249 result, on a different main and LLVM: CachyOS x86-64, GCC 16.2.1, Release, env -i, lavapipe (llvmpipe, LLVM 23.1.1), 3 runs per arm via ctest. Main f1af6c591, merged in order with #6914 ce35d8fec, #6249 bdf65336d and #6656 82119a8b8:

Not run: #6146 or the four-PR merge, the #6249 + #6656 and #6914 + #6656 pairs, the APS5_NO_PIPELINE_DMA=1 arm, Windows, NVIDIA/AMD, or a PS5.

AI-assisted: yes

@moong15

moong15 commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

@nicolasfalesy, @Dextroid17, thanks. Confirmed and fixed in 6647ccd, rebased onto main 09127ea.

The self-hold doesn't need #6656 or #6249: on main it shows with APS5_PIPELINE_DMA=1, and it covers every queued copy, not just those over 64 KiB. A copy at or under 64 KiB reads its source with GuestMemory::Read, whose flush hook reaches HeldReads, so its own listed read refused the store just as CopyBuffer's flush did for the large ones.

The fix is the one you described. DrawPipeline::ReleaseRunningReads() clears items.front().reads under mutex, and the DMA copy commit lambda in enqueueDmaPacket calls it first. While the copy waits in the queue, its source stays held against write-backs. Draws never call it, so a draw is still held against its own reads.

Test: agc_driver_queued_index_read_order has a fourth phase. It leaves an image's results pending over the pool, queues a 4 KiB DMA_DATA copy out of it with APS5_PIPELINE_DMA=1, checks that the copy holds its source while queued, and requires the copied bytes to match the image's results once they land.

  • With the fix: passes 3 of 3, and 10 repeats of it and the six agc_driver_pipelined_* tests under ctest -j8 with ANYPS5_NO_SHADER_CACHE=1 all pass.
  • With the ReleaseRunningReads() call removed: fails with "the queued copy of the image: 4089 of 4096 copied bytes miss the image's results over the copy source".
  • With APS5_NO_QUEUED_DRAW_READ_ORDER=1: the index phase fails as before.

I left the large copy out of this test. On main, a copy over 64 KiB out of a pending image loses the image's bytes (120192 of 131072 here) even with APS5_NO_QUEUED_DRAW_READ_ORDER=1 and without APS5_PIPELINE_DMA, which looks like what #6249 fixes. The fix above releases the read before either copy path runs, so with #6249 merged the large copy no longer refuses its own flush; your four-PR runs cover that.

Windows 11, AMD Radeon RX 9070 XT, AMD 26.8.1 (LLPC). check_conventions.py passes.

@Dextroid17

Copy link
Copy Markdown
Contributor

@moong15 I re-ran this on 6647ccd11, with the same host and settings as my run above (CachyOS, GCC 16.2.1, Release, env -i, lavapipe, 3 runs per arm):

Not run: #6146 or the four-PR merge, APS5_PIPELINE_DMA=1 on bare main, the over-64 KiB copy on main, the pairs, the APS5_NO_PIPELINE_DMA=1 arm, your 10-repeat ctest -j8 run, Windows, NVIDIA/AMD, or a PS5.

AI-assisted: yes

@moong15
moong15 force-pushed the pr/queued-index-read-order branch 2 times, most recently from 2ee52e7 to 845d587 Compare October 18, 2026 18:39
@github-actions github-actions Bot added the unblocks title Names a title ID on the Unblocks line label Oct 18, 2026
@moong15
moong15 force-pushed the pr/queued-index-read-order branch from 845d587 to 4ffb7f6 Compare October 19, 2026 01:50
@adriadam10 adriadam10 self-assigned this Oct 19, 2026
moong15 and others added 8 commits October 19, 2026 12:57
A pipelined draw read its index buffer from guest memory at commit time,
after the packets behind it had run, so a later dispatch's deferred
storage-image write-back could land in the bytes first. Stellar Blade
recycles its full-screen quad's index buffer for a 66x66 image when the
menu loads, and aborted with "vertex fetch exceeds descriptor record count".
Deferred write-backs now wait while a queued draw still holds a read of the
range. APS5_NO_QUEUED_DRAW_READ_ORDER=1 restores the old order; [draw]
reports held read ranges and stores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ndex read

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e stores they refused

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s checked

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erence

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adriadam10
adriadam10 force-pushed the pr/queued-index-read-order branch from 4ffb7f6 to 45d012e Compare October 19, 2026 11:11
@adriadam10
adriadam10 merged commit 3efab5e into boykopovar:main Oct 19, 2026
10 checks passed
@adriadam10

Copy link
Copy Markdown
Collaborator

Checked the held-read ranges (index, buffer descriptors, vertex attributes, DMA source), the FlushPending/WAIT_REG_MEM ordering, the self-hold fix for queued copies and the test; the cross-platform runs in the thread cover the test on NVIDIA, AMD and lavapipe.

Added two commits on your branch: a rebase onto current main, and a docs commit folding the new docs/dev/debt/libSceAgcDriver.md entry into the existing "Storage image write-back" line instead of leaving two, and fixing it to match the code (no retry of refused stores; the [storage] line counts the listings the hold turned away, the [draw] pipelined line the stores held); merged.

@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.

Thanks for the pairs, and for fixing the race in the test.

I agree two pairs can't settle 3 ms, but the PR already has a counter that can. The hold only changes behaviour when a write-back overlaps an index range a queued draw still has to read. The earlier runs held 27 stores in one run and 324 and 65 in another, whole-run totals. At about 250 frames per 10 s window that is at most a store held every few frames, which is too little work to cost 3 ms of Driver.Packet on every frame. If the [draw] pipelined lines for the same windows show the same small numbers of held reads and stores on the fix-on runs, the pair-1 difference is run-to-run spread; if a window shows hundreds of holds, it isn't. Please post the held read ranges and held stores per 10 s window next to the table.

Pair 1's fix-off run also lost its second window to the start of loading at about 25 s (127 presents), so only the 10-20 s window of that pair is comparable. A third pair run in the opposite order (off first) would remove the ordering effect.

@mugurc mugurc mentioned this pull request Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build docs unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants