Skip to content

perf(agc): cross uniform runs of cached guest pages 64 pages per load - #6921

Open
milescoviello wants to merge 3 commits into
boykopovar:mainfrom
milescoviello:perf/page-run-blocks
Open

milescoviello wants to merge 3 commits into
boykopovar:mainfrom
milescoviello:perf/page-run-blocks

Conversation

@milescoviello

@milescoviello milescoviello commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

GuestMemory's page-state table keeps one byte of access flags per 4 KiB page of the guest arena (and of the main image on Windows), and describePages finds the end of a run of equal pages with PageSpan::runEnd, which reads that table eight pages per load. When a descriptor is bound over a whole GPU heap, GuestBufferMemory::addDescriptorRegion calls DescribeCommitted on hundreds of MiB for every dispatch, so the scan reads tens of thousands of table bytes each time.

In MXGP 26 (PPSA30839) on Linux, runEnd is the largest entry in a perf profile of the process (13.8% of all samples). The run uses the PRs this title needs to get this far (#6814, #6094, #6160, #6184, #6455, #6308) plus the lease change of #6917. A DWARF call graph puts 13% under Driver::run → dispatch → ShaderResources::addGuestBuffer → addDescriptorRegion → DescribeCommitted, on the submission thread. The draw-profile counters ([guestmem] verify) count about 8.5 million range checks per 130 s run, averaging 150 MiB each and 21 s of thread time in total.

Each PageSpan now has a second table with one byte per 64 pages (256 KiB of guest memory). A byte holds the pages' common value when all 64 hold the same nonzero value, and zero otherwise. It sits next to the page entries, at 1/64 of their size:

  • On Linux the page table is one calloc, and the summaries follow the pages in the same block (3.9 MiB for the 1000 GiB arena; only the pages that are touched get backed).
  • On Windows, fix(agc): allocate Windows page permissions on demand #8139 made the page table a directory of 65,536-page chunks allocated on first store. Each Chunk now also holds the 1,024 summaries of its own pages. A block never spans two chunks, and a missing chunk has no summaries: runEnd ends a run there as before, and stores of zero or PageInaccessible still don't allocate a chunk.
  • runEnd skips a marked block in one load, and at a 512-page boundary it compares eight marks in one 64-bit load (2 MiB per load). Unmarked blocks and run edges are read page by page as before.
  • store(first, last, value) (used by record, forgetInaccessible and the single-page store) stores the pages chunk by chunk and then settles the marks of the blocks it touched: a block whose 64 pages now agree is marked, and any other is cleared.
  • forget goes through the same store with zero, so it clears the pages, then the marks of the blocks it touched.

Marks are kept right under concurrency with fences, not locks. A store or forget writes its pages, issues a seq_cst fence, then reads and fixes the marks. Marking writes the mark, issues a fence, then reads the 64 pages again and clears the mark if they no longer agree. So when a page change and a mark race on one block, either the change's settle sees the mark and replaces it, or the mark's second read sees the change and takes the mark back. A stale mark therefore cannot outlive a change; while a change is in flight, readers see old or new state, as they already did with the page bytes. Writing zero is always safe, so only nonzero marks get the second check. The record generation check is unchanged and now also clears the marks through forget.

Tested

Current head (b50a5d5cea, rebased onto main 29896371ed, after #8139):

  • The rebase is a rework, not a hunk merge: fix(agc): allocate Windows page permissions on demand #8139's sparse Windows table replaced the dense array the summaries lived in, so they now live in each Chunk as agreed with @Izamaru on fix(agc): allocate Windows page permissions on demand #8139. The third commit adds @Izamaru's agc_page_state_runs fixture from lab/compat-8139-6921 (credited as co-author). It commits 1,536 pages across a chunk boundary, protects single pages at block, 512-page group and chunk edges, checks a decommit, and keeps two scanners running while pages flip. I added one case to it: a 512-page run that ends where an unallocated chunk begins must stop at its last page. The Linux table layout is the same as before the rework, so the MXGP measurements below still describe the Linux path.
  • Windows 11, NVIDIA GeForce RTX 4090 (driver 617.14, WinLibs GCC 15.2.0 r7, Release), run by @Izamaru under the Khronos layer (1.4.363) with the layer insertions logged: main 999e612 passes its 11 page-state, metadata, graphics, BDA and memory tests 3 of 3; this head (b50a5d5cea) and a local merge with that main pass the same 11 plus agc_page_state_runs, 12 of 12, 3 of 3. Skipping the summary settle when pages are stored as zero fails agc_page_state_runs 3 of 3. The metadata fixture reports the same 102,400 bytes of private commit on all three.
  • Linux (Ubuntu 26.04.1, GCC 15.2, Release, RTX 4070 Ti, NVIDIA 615.71.09), on 70740fcffb (the third commit only adds Windows tests): agc_driver_graphics (with RunPageBlockTests and the 256-round flip test), agc_driver_bda_device and agc_driver_store_over_untracked pass 3 of 3.
  • Windows 11 VM (GeForce GT 1030, driver 582.66, WinLibs GCC 15.2.0 r7, Release), main at the merge base against the head, 3 runs each: agc_page_states, agc_page_state_metadata, agc_driver_graphics, agc_driver_bda_device and agc_driver_store_over_untracked pass 5 of 5 on main; on the head (at 41468e10e9, the same three commits before the last rebase) those and agc_page_state_runs pass 6 of 6. As a negative control, a branch that leaves the summaries alone when pages are stored as zero (forget without the summary clear, test/6921-mut) fails agc_page_state_runs (a protected page retained writable access in a summarized run) and agc_driver_graphics in 3 of 3 runs.

Earlier heads (Ubuntu 24.04, GCC 13.3, Release, RTX 4070 Ti, NVIDIA 595.99.02):

  • Rebased onto main 6e27fc8 (f05f64a). Tamalero's 13a3902 and 4a96406 had changed PageStates::record meanwhile: inaccessible pages are now cached as PageInaccessible, an access serial forgets pages recorded across an arena access change, and forgetInaccessible clears inaccessible pages. The resolution keeps all of that and writes through this PR's range store, so the block summaries follow, including when forgetInaccessible clears a page. On the rebased head agc_driver_graphics (with RunPageBlockTests) passes 3 of 3. The 1,013 tests matching the regex below pass, except agc_driver_bda_device, which fails on main on this driver too (fix(shader): leave the BDA lookup functions through one return #7571).

  • New RunPageBlockTests in agc_driver_graphics (Linux, 1,536 arena pages aligned to 2 MiB). It changes one page through the registry in the middle of a marked block (700), at the start of a group of eight blocks (512), and at the last page (1535). Each time it checks Accessible, CommittedRanges (readable and writable) and DescribeCommitted(...).whole before and after the page is restored. Two threads then call DescribeCommitted on the whole run while the main thread toggles 128 pages through the registry 2,000 times, and the final state must be described exactly. Passes 3 of 3. With the mark clearing removed from forget, it fails (a page protected inside a long run of cached pages kept its old access). In 256 further rounds (8a2c242), two threads run DescribeCommitted over the range while one page of a marked block turns read-only, moving through 22 blocks and every offset in a block. After each thread finishes a pass started after the change, the writable ranges must stop at that page. Passes 3 of 3. With the recheck removed from settle it still passes 3 of 3: two scanners don't hit that window.

  • The 885 tests matching ^agc_driver_|^agc_vertex_barrier|recompiler|shader_cache|request_serializer|spirv|^agc_shader|bda|journal|^guest_|libc pass except agc_driver_store_over_untracked, which fails on main too.

  • Windows 11, AMD Radeon 780M (RDNA 3), AMD driver 25.10.20.01, WinLibs GCC 15.2.0 r7, Release, at 8a2c242: the 14 test targets this PR changes pass (run by @keravyn-ops).

  • Speed (Performance report protocol). MXGP 26, 100–110 s into a warm run with pad input, interleaved in one session. Only APS5_TRACE_SECONDS=100:10 (and the trace directory and pad input file) was set: no APS5_PROFILE_* switch. main does not reach this scene, so the base is the stack above plus perf(libc): keep the guest allocation lease snapshot until the registry changes #6917. The commits are local test merges: 5f469824dc57 is the stack (with perf(agc): time compute pipeline creation #6896's one-line compute pipeline trace timer) plus perf(libc): keep the guest allocation lease snapshot until the registry changes #6917's library change, and 84b92c0dc0dd adds this PR's GuestMemory.cpp change. That build differs from this branch only in comments removed before pushing, and in fix(agc): cache inaccessible guest pages #6184's forgetInaccessible calling store(at, at + PageBytes, 0).

Build commit presents frame p50 / p90 / p99 (ms)
base, run 1 5f469824dc57 44 230.5 / 239.5 / 247.3
this PR, run 1 84b92c0dc0dd 53 190.2 / 201.8 / 210.1
base, run 2 5f469824dc57 43 231.9 / 242.0 / 244.2
this PR, run 2 84b92c0dc0dd 51 198.4 / 205.9 / 212.3

tools/trace_digest.py:

base 1: **seconds 100:10** from present 477: 44 presents, frame p50 230.5 / p90 239.5 / p99 247.3 / max 247.3 ms, GPU busy 115.6 ms per present
PR 1:   **seconds 100:10** from present 472: 53 presents, frame p50 190.2 / p90 201.8 / p99 210.1 / max 210.1 ms, GPU busy 122.5 ms per present
base 2: **seconds 100:10** from present 441: 43 presents, frame p50 231.9 / p90 242.0 / p99 244.2 / max 244.2 ms, GPU busy 119.2 ms per present
PR 2:   **seconds 100:10** from present 525: 51 presents, frame p50 198.4 / p90 205.9 / p99 212.3 / max 212.3 ms, GPU busy 123.6 ms per present

Summary lines (aps5-summary.jsonl, base 1, PR 1, base 2, PR 2):

{"commit":"5f469824dc57","dirty":false,"env":{"APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_DIR":".","APS5_TRACE_SECONDS":"100:10"},"gpu":"NVIDIA GeForce RTX 4070 Ti","driver":"NVIDIA 595.99.02","os":"Linux 6.8.0-147-generic","resolution":"3840x2160","window":{"spec":"seconds 100:10","first_present":477,"ms":10188.146},"presents":44,"frame_ms":{"p50":230.477,"p90":239.478,"p99":247.269,"max":247.269},"gpu_busy_ms_per_present":115.561,"worker_busy_pct":{"queue worker 0x0":99.417,"queue worker 0x20":99.300},"top_phases_ms_per_frame":{"Driver.Packet":459.173,"Driver.Packet.dispatch":179.724,"Driver.Dispatch":178.887,"Driver.Packet.memory_wait":169.497,"Driver.Dispatch.capture_resources":70.947,"Resources.Prepare":50.093,"Driver.Packet.draw":48.342,"Driver.Draw":48.119},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":7.825,"events_lost":77892,"totals_full":false}},"trace":"aps5-trace-5f469824dc57-3.json.gz"}
{"commit":"84b92c0dc0dd","dirty":false,"env":{"APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_DIR":".","APS5_TRACE_SECONDS":"100:10"},"gpu":"NVIDIA GeForce RTX 4070 Ti","driver":"NVIDIA 595.99.02","os":"Linux 6.8.0-147-generic","resolution":"3840x2160","window":{"spec":"seconds 100:10","first_present":472,"ms":10136.123},"presents":53,"frame_ms":{"p50":190.164,"p90":201.774,"p99":210.060,"max":210.060},"gpu_busy_ms_per_present":122.500,"worker_busy_pct":{"queue worker 0x0":99.330,"queue worker 0x20":99.466},"top_phases_ms_per_frame":{"Driver.Packet":379.614,"Driver.Packet.memory_wait":138.089,"Driver.Packet.dispatch":136.793,"Driver.Dispatch":136.020,"Driver.Dispatch.capture_resources":72.500,"Driver.Packet.draw":44.223,"Driver.Draw":44.010,"Driver.Packet.prepare_memory":28.924},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":7.572,"events_lost":88121,"totals_full":false}},"trace":"aps5-trace-84b92c0dc0dd-3.json.gz"}
{"commit":"5f469824dc57","dirty":false,"env":{"APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_DIR":".","APS5_TRACE_SECONDS":"100:10"},"gpu":"NVIDIA GeForce RTX 4070 Ti","driver":"NVIDIA 595.99.02","os":"Linux 6.8.0-147-generic","resolution":"3840x2160","window":{"spec":"seconds 100:10","first_present":441,"ms":10005.818},"presents":43,"frame_ms":{"p50":231.870,"p90":241.967,"p99":244.206,"max":244.206},"gpu_busy_ms_per_present":119.206,"worker_busy_pct":{"queue worker 0x0":99.442,"queue worker 0x20":99.515},"top_phases_ms_per_frame":{"Driver.Packet":462.388,"Driver.Packet.memory_wait":177.861,"Driver.Packet.dispatch":177.430,"Driver.Dispatch":176.601,"Driver.Dispatch.capture_resources":69.747,"Resources.Prepare":49.194,"Driver.Packet.draw":46.248,"Driver.Draw":46.035},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":7.890,"events_lost":69526,"totals_full":false}},"trace":"aps5-trace-5f469824dc57-3.json.gz"}
{"commit":"84b92c0dc0dd","dirty":false,"env":{"APS5_PAD_INPUT_FILE":"<path>","APS5_TRACE_DIR":".","APS5_TRACE_SECONDS":"100:10"},"gpu":"NVIDIA GeForce RTX 4070 Ti","driver":"NVIDIA 595.99.02","os":"Linux 6.8.0-147-generic","resolution":"3840x2160","window":{"spec":"seconds 100:10","first_present":525,"ms":10127.634},"presents":51,"frame_ms":{"p50":198.376,"p90":205.886,"p99":212.263,"max":212.263},"gpu_busy_ms_per_present":123.567,"worker_busy_pct":{"queue worker 0x0":99.330,"queue worker 0x20":99.454},"top_phases_ms_per_frame":{"Driver.Packet":394.102,"Driver.Packet.dispatch":144.283,"Driver.Dispatch":143.497,"Driver.Packet.memory_wait":137.985,"Driver.Dispatch.capture_resources":77.539,"Driver.Packet.draw":48.411,"Driver.Draw":48.180,"Driver.Packet.prepare_memory":30.307},"trace_dropped":true,"trace_dropped_threads":{"queue worker 0x0":{"events_full_after_s":7.683,"events_lost":83184,"totals_full":false}},"trace":"aps5-trace-84b92c0dc0dd-3.json.gz"}

Traces: https://gist.github.com/milescoviello/336bdc50227a65c6ea50f86e84ffbe1d, files lease-5f469824dc57-run{1,2}.json.gz (base) and blocks-84b92c0dc0dd-run{1,2}.json.gz (this PR). They open in ui.perfetto.dev.

  • Mechanism. These are earlier runs with APS5_PROFILE_DRAW=1, which costs time, so they are not the speed claim. Three interleaved 130 s runs each: the [guestmem] verify time drops from 21.1 / 20.9 / 21.1 s (8.2–8.8 M range checks) to 9.9 / 9.5 / 9.4 s (10.0–10.6 M; more frames, more checks), about 2.4 µs to 0.9 µs per check. In perf record over 100–115 s, runEnd goes from 14.7% of all samples to 0.7%. All runs reached their limit.

Not checked by me: macOS, AMD/Intel on Linux. On Windows, RunPageBlockTests is Linux-only; the chunked path is covered by agc_page_states, agc_page_state_metadata and agc_page_state_runs above (parl0v's Windows runs of Minecraft Dungeons II on an earlier head showed no regression).

  • Khronos validation layer (LunarG SDK 1.4.363), NVIDIA RTX 4070 Ti (595.99.02), Ubuntu 24.04: VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ANYPS5_NO_SHADER_CACHE=1 ANYPS5_REQUIRE_VULKAN=1 ctest -R '^agc_' -V on main e2a2914 and on main + this PR (f05f64a, a local merge). main: 815 of 815 pass; with this PR: 815 of 815 pass. Neither log reports the layer missing. VUIDs on main: VUID-VkImageViewCreateInfo-usage-02275 ×6, VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 ×18, VUID-vkCmdCopyBuffer-srcBuffer-00118 ×2, VUID-vkCmdDraw-multisampledRenderToSingleSampled-07285 ×2, VUID-vkCmdWaitEvents-srcStageMask-01158 ×2, VUID-vkSetEvent-event-09543 ×2; with this PR: VUID-VkImageViewCreateInfo-usage-02275 ×6, VUID-VkPipelineColorBlendStateCreateInfo-pAttachments-00605 ×18, VUID-vkCmdCopyBuffer-srcBuffer-00118 ×2, VUID-vkCmdDraw-multisampledRenderToSingleSampled-07285 ×2, VUID-vkCmdWaitEvents-srcStageMask-01158 ×2, VUID-vkSetEvent-event-09543 ×2. None is new with this PR.

Checklist

🤖 Generated with Claude Code

@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 10575 core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp L26
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp L12090
core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp L330

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
PR 1395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 1896 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 2434 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 4040 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 4448 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 5021 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 5371 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5441 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 5925 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6186 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6187 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6188 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6192 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6194 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6209 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6235 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6273 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6280 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.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/tests/Graphics.cpp
PR 6548 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6642 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6808 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6960 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7048 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7201 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7223 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7256 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7275 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7410 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7457 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7604 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7642 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7668 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7677 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8014 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8212 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 8246 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 8484 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8592 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9193 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9356 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9387 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9401 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9489 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9508 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9675 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9700 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9852 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9892 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9897 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 9898 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 9969 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 10000 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 10046 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 10049 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 10193 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 10233 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 10348 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 10420 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 10472 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 10489 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 10515 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 10581 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp

@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 profile (runEnd at 13.8% of all samples in MXGP 26, 8.5 million range checks per 130 s averaging 150 MiB) justifies work here. I read the mark protocol (write pages, fence, settle; mark, fence, re-read and take the mark back) and could not find an interleaving that leaves a stale mark. Points:

  • This speeds up the scan, but the call count is the problem: addDescriptorRegion calls DescribeCommitted over hundreds of MiB for every dispatch that binds the same heap. The description counts 8.5 million of them. Caching the result per descriptor range against the registry generation (the generation the lease snapshot in #6917 already uses) removes the scan for repeated bindings instead of making each one cheaper. Say why that wasn't done, or measure both.
  • No before/after. A profile share before the change is not a result: please post the summary lines of main and this PR (plus #6917, as the run uses it) from the same scene in PPSA30839, no APS5_PROFILE_*, and the same perf share after.
  • RunPageBlockTests is #if defined(__linux__), but the allocation (VirtualAlloc of count + blockCount) and the main-image table on Windows change as well. Run the agc_* suite on Windows and say so, or add the Windows case.
  • The block tables are written with memory_order_relaxed byte stores plus seq_cst fences. Please add a short stress test that flips a page in a marked block from one thread while another calls runEnd over it, and requires that after the writer finishes runEnd stops at that page.

@parl0v

parl0v commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Data point from a CPU-bound scene on Windows: RTX 3060 12 GB (NVIDIA 617.42), i5-12400F, Windows 10 19045, MinGW GCC Release. Minecraft Dungeons II (PPSA16064 01.100.100), title screen, no input, 200 s rows, APS5_PROFILE_DRAW=1 APS5_PROFILE_GPU=1; main bc3c0fc vs main + this PR, interleaved with five other PRs in one batch, two rounds. FPS is the mean of the last 10 [present] windows, GPU busy and idle gaps per frame over the last 8. This scene is CPU-bound on this machine (45-47 ms of idle gaps per frame).

libs FPS GPU busy idle gaps
main 6.70 / 6.60 105 / 106 ms 45 / 46 ms
+ this PR (0e1eb57) 6.70 / 6.60 (0.0 %) 104 / 104 ms 46 / 47 ms

Within the run-to-run spread (about ±1.5 % here), no regression, 0 fatal.

AI-assisted: yes (Claude Code)

@milescoviello

Copy link
Copy Markdown
Contributor Author

Thanks for the review.

Memo per descriptor range vs. this change: I measured both. I made two measurement-only builds, neither of them a PR, on top of the stack plus #6917. In both, addDescriptorRegion keeps DescribeCommitted results per descriptor range against the registry generation:

  • 1d54da167430: a 64-entry per-thread table.
  • 384e7e8b1f6d: a per-thread hash map with no size limit, cleared when the generation changes.

Same scene and protocol as in the description: PPSA30839, 100–110 s, interleaved, two rounds, no APS5_PROFILE_* switch, perf record -F 499 over 100–112 s. Two sessions today:

Session Build commit presents frame p50 (ms) PageSpan::runEnd memo hits / misses
2 stack + #6917 5f469824dc57 41 / 42 244.1 / 241.5 14.2% / 14.5%
2 + 64-entry memo 1d54da167430 49 / 47 205.6 / 211.5 3.4% / 2.3% 3.15M / 1.11M, 3.15M / 1.30M
2 + this PR 84b92c0dc0dd 24 / 51 417.8 / 197.9 0.30% / 0.68%
3 stack + #6917 5f469824dc57 36 / 41 244.8 / 239.5 14.4% / 15.0%
3 + memo, no size limit 384e7e8b1f6d 43 / 48 231.8 / 207.9 1.9% / 1.9% 3.15M / 133k, 4.19M / 143k
3 + this PR 84b92c0dc0dd 42 / 38 200.2 / 227.7 0.56% / 0.45%
  • The generation barely moves during the race: 38 changes between two counter prints 2.8 million lookups apart. The 64-entry table missed for lack of room. Without a size limit the memo hits 96–97% of lookups.
  • This PR's first run in session 2 is an outlier. The title re-uploaded a storage texture about 500 times (StorageTexture upload submit), against 9 to 37 in the other runs today that reached the scene, and memmove is 29% of its samples. I haven't found the cause. The other three protocol runs of this build today, before session 3, have 51–53 presents at p50 190–198 ms.
  • Session 3 is noisier. Three of its runs (stack + perf(libc): keep the guest allocation lease snapshot until the registry changes #6917 run 1 and both runs of this PR) have one frame of about 1 s in which both queue workers spend the whole frame in Driver.Packet.
  • So in this scene I can't separate the unbounded memo from this PR. Both clearly beat the base, and the 64-entry memo trails them. They differ in what they cover:
    • With the memo, runEnd stays at about 1.9%, because Accessible, CommittedRanges, CheckRange and the verify reads still scan. The page table speeds up every caller.
    • The memo also keeps results for addresses outside the arena and image tables. The page table queries those every time on purpose, since nothing invalidates them.
  • The two compose. If you'd rather have the memo, I can open it as its own PR, on top of this one or instead of it.

Before/after. main does not reach this scene. main at 1b1f8483c exits at 103 s, on the unaligned BDA atomic (#6094) and then the unresolvable s_swappc_b64 target (#6160); details on #6917. So the base is the stack plus #6917, as in the table.

tools/trace_digest.py for the base and this PR (session 2, round 2):

base: **seconds 100:10** from present 440: 42 presents, frame p50 241.5 / p90 250.5 / p99 267.8 / max 267.8 ms, GPU busy 117.4 ms per present
PR:   **seconds 100:10** from present 517: 51 presents, frame p50 197.9 / p90 205.5 / p99 211.8 / max 211.8 ms, GPU busy 114.3 ms per present

Summary lines and traces for both sessions: https://gist.github.com/milescoviello/336bdc50227a65c6ea50f86e84ffbe1d, under session2-* and session3-*, each with its own *-summary.jsonl.

Windows. I did not build or run anything on Windows: there's no Windows machine here, and the supported MinGW (GCC 15.2) isn't installed. The only Windows data is parl0v's runs above: Minecraft Dungeons II, main vs this PR, no regression, 0 fatal. They allocate the table with VirtualAlloc(count + blockCount), but they don't run RunPageBlockTests. That test stays Linux-only because it maps and protects arena pages with mmap/mprotect.

Stress test. 8a2c242 adds it to RunPageBlockTests, 256 rounds. Each round:

  1. Describes the 1536-page range whole, so its uniform blocks are marked.
  2. Starts two threads that call DescribeCommitted over the range in a loop, which runs runEnd across the marked blocks.
  3. Makes one page read-only through Mutation::Protect while they run.
  4. Waits until each thread has finished a pass that started after the change, then stops them.
  5. Requires the writable ranges to stop exactly at that page, then makes the page writable again.

The flipped page moves through 22 blocks and every offset within a block ((1 + round % 22) * 64 + (round * 37) % 64). The check runs after the scanners stop rather than inside their loop: a scanner that queried before the change can store the old access and then forget it again after its generation check, and that short window exists on main too. agc_driver_graphics passes 3/3.

Negative controls, 3 runs each:

  • Without the mark clearing in forget: fails 3/3, at the existing check a page protected inside a long run of cached pages kept its old access, before the stress test runs.
  • Without the recheck in settle: passes 3/3, so the stress test does not catch it. The recheck covers a forget that lands between a scanner's common and its mark store, a window of a few loads, and two scanners over 256 rounds don't hit it. I don't see a way to widen that window without a test hook in GuestMemory.cpp.

AI-assisted: yes (Claude Code)

@milescoviello

Copy link
Copy Markdown
Contributor Author

An update on the outlier in my last comment (the run with about 500 StorageTexture uploads). It came back, and so far only with this PR. Please hold off merging until I've found the cause.

  • MXGP 26, 90–135 s runs today on the same machine, with and without APS5_PROFILE_DRAW: 5 of 35 runs on builds that contain this PR had it, against 0 of 36 runs without it (the stack, the stack + perf(libc): keep the guest allocation lease snapshot until the registry changes #6917, and the two measurement-only memo builds). By chance alone that split would be about 3% likely.
  • What the runs show:
    • About 220–500 storage-image uploads in a run, against 9–37 in the others.
    • memmove is about 29% of samples, and presents drop to about 24 in the 100–110 s window.
    • In a run with a log line per CPU-path upload, two 2560x1440 storage images (15 MiB each) upload alternately, dozens of times in the run, while normal runs upload each such image about once. In those storm runs they sit at 64 KiB-aligned addresses, where in normal runs they are 1 MiB-aligned. In a profiled storm run, uploads whose reason is a guest CPU write about doubled.
    • Every storage upload in MXGP takes the CPU path, in normal runs too. Host imports did not fail in a traced storm run (APS5_TRACE_HOST_IMPORT=1: 0 failures, about 4.4 GiB live).
  • A local build that checks every block skip in runEnd against the 64 page bytes, and logs any mark that disagrees, reported no stale mark in 4 runs. None of those 4 runs had the storm, though, so that doesn't clear the PR.
  • Next: runs with the check and the upload log together, until one of them hits the storm. I'll post what they show.

AI-assisted: yes (Claude Code)

@milescoviello

milescoviello commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor Author

Follow-up to my previous comment: the upload storm also happens without this PR, so I'm withdrawing the request to hold it. Two corrections to that comment are at the end.

I built the same CPU-upload log on the stack + #6917 build, which doesn't contain this PR (7b248fce30, local). I ran it interleaved with the log on the stack + #6917 + this PR, 6 rounds of MXGP 26, 130 s each, same session.

Build Runs with the storm (more than 100 storage uploads) Uploads in those runs
stack + #6917 + upload log (no #6921) 3 of 6 350, 348, 251
stack + #6917 + this PR + upload log 5 of 6 (8 of 14 with the earlier batch) 239–467
without the log: stack, stack + #6917, memo builds 0 of 36
without the log: builds with this PR 4 of 31 223–498

In every storm, the two 2560x1440 storage images upload alternately dozens of times through the CPU path, whichever build it is. So the storm doesn't need this PR. The log itself changes how often it happens: at each CPU-path upload it also calls HostImportFor and DescribeCommitted for the image's range. That points to timing or extra work tipping a behaviour that already exists. That fits the 4-of-31 split without the log, since this PR makes the scans much faster. I haven't found what makes the title re-upload those two images, and I'll open a separate issue once I have.

Corrections to my previous comment:

  • The storm runs' images are not consistently 64 KiB-aligned. In the control's storm runs they sit at 1 MiB-aligned addresses (0x1224000000, 0x1222c00000).
  • The earlier "5 of 35" included one run with the upload log. Without it the split is 4 of 31 against 0 of 36, as in the table, and with the control above it doesn't show that this PR causes the storm.

AI-assisted: yes (Claude Code)

@milescoviello

Copy link
Copy Markdown
Contributor Author

One more data point on the storm: with the block-mark check from my earlier comment and the CPU-upload log in the same build (stack + #6917 + this PR, local 98e408e988), the first MXGP run had the storm (342 storage uploads). Every block skip in runEnd was compared against the block's 64 page bytes, and no stale mark was logged.

AI-assisted: yes (Claude Code)

@keravyn-ops

Copy link
Copy Markdown
Contributor

AMD / Windows run of this PR at 8a2c242: Radeon 780M (RDNA 3, 0x15bf), AMD driver 25.10.20.01 (32.0.21020.1007), Windows 11 Home 10.0.26100, WinLibs GCC 15.2.0 r7, Release.

Every ctest entry built from the test targets this PR changes passes (14):

  • agc_driver_undithered_alpha_to_coverage_state (0.11 s): 282: Existing undithered alpha-to-coverage states passed
  • agc_driver_alpha_to_coverage_state (0.02 s): 283: Alpha-to-coverage state and refusal tests passed
  • agc_driver_prepared_graphics_prebuild_unset (0.19 s): 285: prebuild policy native readback passed
  • agc_driver_prepared_graphics_prebuild_0 (0.14 s): 286: prebuild policy native readback passed
  • agc_driver_prepared_graphics_prebuild_1 (0.15 s): 287: prebuild policy native readback passed
  • agc_driver_graphics (4.86 s): 320: Graphics validation tests passed
  • agc_driver_prepared_graphics (0.14 s): 580: prepared graphics ABI tests passed
  • agc_driver_prepared_unregistered_graphics (0.17 s): 583: prepared unregistered graphics tests passed
  • agc_sparse_buffer_holes (0.05 s): 700: sparse buffer hole tests passed
  • agc_pipeline_journal (0.07 s): 704: pipeline journal recipe, record and replay tests passed
  • agc_descriptor_pool_reset_failure (0.02 s)
  • agc_driver_address_space_teardown (0.02 s): 759: address-space device teardown releases cached guest allocations
  • agc_compute_descriptor_layout_flags (0.02 s): 788: Descriptor layout native and captured binding flags passed
  • agc_driver_covered_centroid_state (0.02 s): 789: Covered-centroid and multisample state checks passed

AI-assisted: yes (runs made on the device above).

@milescoviello

Copy link
Copy Markdown
Contributor Author

Thanks @keravyn-ops, that covers the AMD/Windows side of every test target this PR touches. I added your run to the Tested section.

AI-assisted: yes (Claude Code)

@milescoviello

Copy link
Copy Markdown
Contributor Author

Rebased onto main 6e27fc8 (f05f64a). The conflict was with Tamalero's 13a3902 and 4a96406 in PageStates::record: inaccessible pages are now cached, and there's an access serial. The resolution keeps their logic and stores through this PR's range store, including in the new forgetInaccessible, so the 64-page block summaries stay in step. On the rebased head agc_driver_graphics passes 3 of 3. The 1,013-test regex from the description passes, except agc_driver_bda_device, which fails on main here too (#7571).

AI-assisted: yes (Claude Code)

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
@oneandonlydean oneandonlydean self-assigned this Oct 19, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 23, 2026
milescoviello and others added 3 commits October 23, 2026 18:27
DescribeCommitted on a descriptor bound over a whole GPU heap walked the
page-state table one byte per page (eight per load), hundreds of MiB per
dispatch. Each table now keeps a byte per 64 pages with their common value;
runEnd skips a marked block in one load and eight of them in one 64-bit
load. Stores and forgets clear the marks they touch after their pages, and
a mark is checked again after a fence, so a racing change cannot leave a
stale mark.

On Windows the page table is a directory of lazily allocated 64Ki-page
chunks, so each chunk carries the marks of its own pages: a block never
spans two chunks, and an absent chunk has no marks to keep.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two threads describe a 1536-page arena range in a loop while the test
makes one page of a uniform (marked) block read-only; after each scanner
has finished a pass that started after the change, the writable ranges
must stop at that page. 256 rounds cover every page offset of a block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
agc_page_state_runs commits 1536 pages across a sparse chunk boundary and
protects single pages at block, group and chunk edges, checking the
committed and writable ranges around each and that a decommit clears the
summaries; two threads keep scanning while pages flip. A 512-page run
ending where an unallocated chunk begins must stop at its last page.

Co-Authored-By: Izamaru <machadochristiancris@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@milescoviello

Copy link
Copy Markdown
Contributor Author

Reworked for #8139's sparse Windows page table and rebased onto main ec5b89a. The new head is b50a5d5cea, three commits.

  • Layout: the 64-page summaries now live in each Chunk, as agreed with @Izamaru on fix(agc): allocate Windows page permissions on demand #8139. A chunk's 65,536 entries are a whole number of blocks, so it carries its own 1,024 summaries, and a missing chunk has none: runEnd ends a run there, and stores of zero or PageInaccessible still don't allocate one. Linux keeps the dense table with the summaries after the pages, so the MXGP measurements in the description still describe that path.
  • Tests: the third commit adds @Izamaru's agc_page_state_runs fixture from lab/compat-8139-6921, credited as co-author, plus one case of mine: a 512-page run that ends where an unallocated chunk begins.
  • Windows 11 VM (GT 1030, 582.66, WinLibs GCC 15.2.0 r7): main at the merge base passes the 5 page-state, graphics, BDA and untracked-store tests 3 of 3; this branch passes those and agc_page_state_runs, 6 of 6, 3 of 3. A mutant that skips the summary clear when pages are stored as zero (forget) fails agc_page_state_runs and agc_driver_graphics 3 of 3.
  • Linux (RTX 4070 Ti, 615.71.09): agc_driver_graphics (with RunPageBlockTests), agc_driver_bda_device and agc_driver_store_over_untracked pass 3 of 3.

The description's Tested section has the details.

AI-assisted: yes (Claude Code)

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 23, 2026
@Izamaru

Izamaru commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

@milescoviello I revalidated the published head after the sparse-table rework, and also tested a local integration with a newer main. These results support merging from the Windows/RTX 4090 compatibility side.

Pinned sources and results (3 runs of each matrix):

Source Commit Result in each run
Upstream main captured at the start 999e612599e75ce9e62d1acac058d38a5d725371 11/11 pass
Exact published PR head b50a5d5ceaf2f54aa4186b77aa3fa0105d14e62b 12/12 pass
Local merge of those two commits 2ad665c4688afe24ab87b91c5cff0921851a96b9 (not pushed) 12/12 pass

The head uses an older main base, so the third matrix checks the PR together with the newer main's code and tests. The local merge was conflict-free; its GuestMemory.cpp is byte-for-byte identical to the tested PR head. git diff --check and the conventions checker against the pinned main pass.

Environment: Windows 11 build 26200, NVIDIA GeForce RTX 4090, driver 617.14, WinLibs GCC 15.2.0 r7, Release, Khronos SDK 1.4.363.0; ANYPS5_ENABLE_SPIRV_TOOLS=OFF. All matrices used ANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation, with VK_LOADER_DEBUG=layer. Each of the nine matrix runs logged six actual Khronos instance-layer insertions, zero VUIDs, no missing-layer warning and no skips. The native GPU tests confirm the RTX 4090, BDA execution and preservation of pending storage results over untracked memory.

The 11 common tests were agc_page_states, agc_page_state_metadata, agc_driver_graphics, agc_sparse_buffer_holes, agc_copy_input_kept_bytes, agc_depth_copy_kept_bytes, agc_draw_inputs_copied, agc_space_pending_writes, agc_pending_write_snapshot, agc_driver_bda_device and agc_driver_store_over_untracked. The PR and local integration additionally ran agc_page_state_runs.

Negative control: on the exact PR head, skipping settle(begin, end) when the stored value is zero makes agc_page_state_runs fail 3/3 with “a protected page retained writable access in a summarized run”. Restoring the original code makes that test pass 3/3. This checks summary clearing; it does not independently prove every fence or all possible interleavings.

Metadata: the initialization fixture reported 102,400 bytes of private commit in all three sources, in all three runs. This preserves the sparse-table result in that fixture; it is not a whole-game RAM measurement or a claim that summaries have no cost (they add 1,024 bytes per allocated chunk).

Scope/limitations: page-state, metadata and run fixtures are CPU checks. RunPageBlockTests inside the graphics test remains Linux-only; our Windows concurrency coverage comes from agc_page_state_runs. We did not run Linux, AMD/Intel, the full suite or a game, and did not reproduce the earlier MXGP performance measurements. These results do not establish an FPS gain, reduced Yōtei loading time or Yōtei gameplay.

Thanks for adapting the summaries to #8139 and including our compatibility fixture with credit, plus your missing-chunk boundary case. No blocker appeared in this focused validation, so I support the merge within the scope above.

AI-assisted: yes (Codex; local builds and runs on the hardware listed above).

@milescoviello

Copy link
Copy Markdown
Contributor Author

@Izamaru thank you, that's the Windows check this needed, and on the exact head with the layer insertions logged. Your negative control is the same one I ran on the VM (skipping the summary settle for stored zeros), so it fails your fixture the same way on two machines. I added your run to the Tested section and the GPU list.

AI-assisted: yes (Claude Code)

This branch has not been deployed

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

Labels

agc libSceAgc* perf Performance change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants