Skip to content

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

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

milescoviello wants to merge 2 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 is allocated in the same block as the page table, at 1/64 of its size: 3.9 MiB for the 1000 GiB arena. On Linux, as with the page table, only the pages that are touched get backed.

  • 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, which stored page by page before) stores the pages 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 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

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: Windows (the image table and VirtualAlloc path compile the same way; parl0v's Windows runs of Minecraft Dungeons II on this PR show no regression, but RunPageBlockTests is Linux-only), macOS, AMD/Intel.

#6184 changes record next to the line this replaces and adds forgetInaccessible, which stores single pages. Whichever merges second needs a small rebase: forgetInaccessible would call store(at, at + PageBytes, 0), which is what the MXGP build above does.

Checklist

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

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 1324 core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp
PR 1395 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
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/Execution/src/GuestMemory.cpp
PR 5441 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 5499 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 5500 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 5925 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6146 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6177 core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp
PR 6185 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6186 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6192 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6194 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6235 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6292 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6548 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6615 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6636 core/libs/prx/libSceAgcDriver/tests/BdaTests.hpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6683 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6701 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6741 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6808 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6868 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6946 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6947 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6960 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 6970 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6972 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7201 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7233 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7254 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7256 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7332 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7426 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 7432 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7505 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7604 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7642 core/libs/prx/libSceAgcDriver/Execution/src/GuestMemory.cpp
PR 7659 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 and others added 2 commits October 18, 2026 20:40
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.

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

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.

5 participants