Repository navigation
perf(agc): find a range's newest writer through a granule index instead of scanning the ring - #12264
Conversation
…ad of scanning the ring newestWriterLocked walked the 4096-entry written-buffer ring from the newest entry on every lookup, so a range without a recorded writer cost a full walk. The ring is now a WrittenBufferIndex that also files each entry under the 64 KiB granules it covers (entries wider than 16 granules on a short list of their own); a lookup walks only the query's granules and that list and returns the same entry as the walk. Queries wider than 16 granules still walk the ring. In GTA V (PPSA04264) cutscenes, newestWriterLocked went from 0.65 % to 0.21 % and from 0.39 % to 0.04 % of all CPU samples, and classifyPendingWrite with its callees from 0.60 % to 0.27 % and from 0.44 % to 0.07 %; keeping the index adds about 0.03 %. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This pull request is labeled |
The MinGW build takes the C++ runtime from libc, so the test failed to link there with an undefined __gxx_personality_v0. It now links libc, gets the Windows unwind section rename and libc's directory on PATH, like agc_thread_priority_tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Performance captures for this PR, as the form asks. Same machine (CachyOS Linux, kernel 7.3.0-4-cachyos, RTX 4070 SUPER, NVIDIA 615.78.08, Ryzen 7 5800X), same title and save (GTA V PPSA04264 01.011.001, Story mode from load), warm shader cache, log printed to the terminal, and only the capture switches Both builds are Summary lines of the trace window (75–105 s after the first present, in the first cutscene): base {"commit":"9f2d365268de","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_EVENTS":"4194304","APS5_TRACE_SECONDS":"75:30","APS5_TRACE_SUMMARY_EVERY":"10"},"gpu":"NVIDIA GeForce RTX 4070 SUPER","driver":"NVIDIA 615.78.08","os":"Linux 7.3.0-4-cachyos","cpu":"AMD Ryzen 7 5800X 8-Core Processor","cpu_threads":16,"memory_mib":64203,"title_id":"PPSA04264","title":"Grand Theft Auto V","content_version":"01.011.001","resolution":"3840x2160","window":{"spec":"seconds 75:30","first_present":2444,"ms":30014.412},"presents":403,"frame_ms":{"p50":73.748,"p90":79.862,"p99":110.775,"max":138.755},"gpu_busy_ms_per_present":36.963,"worker_busy_pct":{"queue worker 0x0":97.973},"top_phases_ms_per_frame":{"Driver.Packet":69.797,"Graphics.Draw":41.297,"Driver.Packet.draw":37.758,"Driver.Draw":37.408,"Graphics.Draw.draw_and_resource_release":35.681,"Driver.Packet.dispatch":24.439,"Driver.Draw.shader_compile_and_link":24.424,"Shader.DrawStage":18.971},"trace_dropped":false,"trace":"aps5-trace-9f2d365268de-1333741.json.gz"}with this PR {"commit":"e98000093a1a","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_EVENTS":"4194304","APS5_TRACE_SECONDS":"75:30","APS5_TRACE_SUMMARY_EVERY":"10"},"gpu":"NVIDIA GeForce RTX 4070 SUPER","driver":"NVIDIA 615.78.08","os":"Linux 7.3.0-4-cachyos","cpu":"AMD Ryzen 7 5800X 8-Core Processor","cpu_threads":16,"memory_mib":64203,"title_id":"PPSA04264","title":"Grand Theft Auto V","content_version":"01.011.001","resolution":"3840x2160","window":{"spec":"seconds 75:30","first_present":2741,"ms":30014.360},"presents":451,"frame_ms":{"p50":68.988,"p90":76.357,"p99":108.921,"max":588.045},"gpu_busy_ms_per_present":33.496,"worker_busy_pct":{"queue worker 0x0":93.744},"top_phases_ms_per_frame":{"Driver.Packet":59.260,"Graphics.Draw":33.805,"Graphics.Draw.draw_and_resource_release":29.403,"Driver.Packet.draw":29.328,"Driver.Draw":29.035,"Driver.Packet.dispatch":20.850,"Driver.Draw.shader_compile_and_link":18.695,"Recorder.Sync":16.659},"trace_dropped":false,"trace":"aps5-trace-e98000093a1a-1327440.json.gz"}The fixed window starts at the same time after the first present, but the first cutscene began about 15 s earlier in the base run (its 60–70 s interval against 80–90 s here), so the two windows cover different stretches of it. Aligned on the cutscene start instead, the The two traces are attached below (
|
| thread | busy | wait | untraced | in slices < 20 us | largest |
|---|---|---|---|---|---|
| title | 0.0 (0 %) | 0.0 (0 %) | 74.5 | 0.0 | Shader.ResolveGraphicsStagesAbi 0.0 |
| queue worker 0x0 | 57.6 (77 %) | 1.3 (2 %) | 15.6 | 80.0 | Driver.Packet 56.1, Driver.Rewind 1.6 |
| draw committer | 44.3 (60 %) | 0.0 (0 %) | 30.1 | 26.6 | Graphics.Draw 30.9, Recorder.Sync 13.2 |
| presenter | 0.4 (1 %) | 13.8 (18 %) | 60.3 | 0.1 | VideoOut.Flip 14.2 |
| gpu | 36.9 (50 %) | 0.0 (0 %) | 37.5 | 0.0 | batch 36.9 |
Largest slices (ms per frame, inclusive, all threads): Driver.Packet 56.1, batch 36.9, Graphics.Draw 30.9, Driver.Packet.draw 28.3, Driver.Draw 27.9, Graphics.Draw.draw_and_resource_release 25.8,
Longest waits
- Driver.Present.render_fence_wait (presenter) 16.4 ms at 14724 ms, present 2634
- Driver.Present.render_fence_wait (presenter) 16.3 ms at 10861 ms, present 2584
- Driver.Present.render_fence_wait (presenter) 16.1 ms at 17992 ms, present 2680
- Driver.Present.render_fence_wait (presenter) 15.8 ms at 7300 ms, present 2538
- Driver.Present.render_fence_wait (presenter) 15.8 ms at 10326 ms, present 2577
- Driver.Present.render_fence_wait (presenter) 15.8 ms at 17920 ms, present 2679
- Driver.Present.render_fence_wait (presenter) 15.6 ms at 6661 ms, present 2530
- Driver.Present.render_fence_wait (presenter) 15.5 ms at 16067 ms, present 2653
- Driver.Present.render_fence_wait (presenter) 15.5 ms at 27162 ms, present 2809
- Driver.Present.render_fence_wait (presenter) 15.5 ms at 11021 ms, present 2586
tools/trace_digest.py, this PR e98000093a
seconds 75:30 from present 2741: 451 presents, frame p50 69.0 / p90 76.4 / p99 108.9 / max 588.0 ms, GPU busy 33.5 ms per present
NVIDIA GeForce RTX 4070 SUPER, NVIDIA 615.78.08, Linux 7.3.0-4-cachyos, 3840x2160, commit e98000093a1a, dropped: no
{"commit":"e98000093a1a","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_EVENTS":"4194304","APS5_TRACE_SECONDS":"75:30","APS5_TRACE_SUMMARY_EVERY":"10"},"gpu":"NVIDIA GeForce RTX 4070 SUPERSlowest frames (largest slices inside each, inclusive)
- present 2798: 588.0 ms at 1877 ms: Driver.Packet (queue worker 0x0) 559.1, Graphics.Draw (draw committer) 307.0, Driver.Packet.draw (queue worker 0x0) 268.7, Driver.Draw (queue worker 0x0) 268.3
- present 2799: 221.4 ms at 2465 ms: batch (gpu) 186.9, Driver.Packet (queue worker 0x0) 147.3, Recorder.Sync (draw committer) 98.6, Driver.Packet.dispatch (queue worker 0x0) 85.1
- present 3164: 137.8 ms at 27998 ms: Driver.Packet (queue worker 0x0) 115.2, Driver.Packet.draw (queue worker 0x0) 76.6, Driver.Draw (queue worker 0x0) 76.3, Graphics.Draw (draw committer) 65.0
- present 3043: 113.6 ms at 18890 ms: Driver.Packet (queue worker 0x0) 87.5, Graphics.Draw (draw committer) 64.4, Driver.Packet.draw (queue worker 0x0) 57.8, Driver.Draw (queue worker 0x0) 57.4
- present 3087: 108.9 ms at 22183 ms: Driver.Packet (queue worker 0x0) 89.5, Driver.Packet.draw (queue worker 0x0) 63.4, Driver.Draw (queue worker 0x0) 63.0, Driver.Draw.shader_compile_and_link (queue
Threads (ms per frame)
| thread | busy | wait | untraced | in slices < 20 us | largest |
|---|---|---|---|---|---|
| title | 0.0 (0 %) | 0.0 (0 %) | 66.5 | 0.0 | Shader.ResolveGraphicsStagesAbi 0.0 |
| shader prepare | 0.0 (0 %) | 0.0 (0 %) | 66.6 | 0.0 | Vulkan.GraphicsPipelinePrebuild 0.0 |
| title | 0.0 (0 %) | 0.0 (0 %) | 66.5 | 0.0 | Recorder.Sync 0.0 |
| queue worker 0x0 | 49.1 (74 %) | 4.7 (7 %) | 12.8 | 64.5 | Driver.Packet 48.0, Driver.EndSubmission 1.5 |
| draw committer | 37.2 (56 %) | 0.1 (0 %) | 29.2 | 20.9 | Graphics.Draw 25.4, Recorder.Sync 11.6 |
| presenter | 0.5 (1 %) | 12.5 (19 %) | 53.6 | 0.1 | VideoOut.Flip 13.0 |
| gpu | 33.6 (51 %) | 0.0 (0 %) | 32.9 | 0.0 | batch 33.6 |
Largest slices (ms per frame, inclusive, all threads): Driver.Packet 48.0, batch 33.6, Graphics.Draw 25.4, Graphics.Draw.draw_and_resource_release 21.7, Driver.Packet.draw 21.4, Driver.Draw 21.1,
Longest waits
- Driver.Present.gpu_mutex_wait (presenter) 61.9 ms at 1877 ms, present 2798
- Driver.Present.render_fence_wait (presenter) 33.9 ms at 2465 ms, present 2799
- Driver.WorkerWait (queue worker 0x0) 33.3 ms at 234 ms, present 2747
- Driver.WorkerWait (queue worker 0x0) 33.0 ms at 640 ms, present 2759
- Driver.WorkerWait (queue worker 0x0) 32.7 ms at 370 ms, present 2751
- Driver.WorkerWait (queue worker 0x0) 32.7 ms at 404 ms, present 2752
- Driver.WorkerWait (queue worker 0x0) 32.7 ms at 1747 ms, present 2792
- Driver.WorkerWait (queue worker 0x0) 32.6 ms at 201 ms, present 2746
- Driver.WorkerWait (queue worker 0x0) 32.6 ms at 0 ms, present 2741
AI-assisted: yes (Claude Code)
|
Traces for the performance comment above, zipped because GitHub rejects
Unzip and open the |
|
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.
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.
|
|
Checked the 60 pull requests listed as not compared by hand (their diffs, and #5021's file list and #11439 is the one real overlap, already noted in the description: it changes the 🤖 Addressed by Claude Code |
|
@Tehshortbus thanks for the captures and the trace digests; the performance report is complete, and the Memory and cost bound. Please state in the description what the index can hold. The ring is 4096 entries, each non-wide entry takes up to 17 granule slots, and each distinct granule costs a hash node plus a |
|
@oneandonlydean The description has a new Memory section: the measured bound (about 44 MiB at worst, for 4096 distinct buffers of about 1 MiB, against 0.25 MiB for the ring on |
MarcZX10
left a comment
There was a problem hiding this comment.
Reviewed the index against the previous reverse linear scan. Each entry is indexed in every 64 KiB granule it overlaps (or the wide-entry list), eviction removes exactly the oldest sequence, and candidates are rechecked with the original half-open overlap predicate before choosing the newest sequence. The randomized differential test also covers empty and wide queries, eviction, and newest-wins behavior. The PR now quantifies index memory and reports paired title captures, trace summaries, same-GPU focused baseline/change tests, no new VUIDs, and validation-layer presence. Hosted Windows and conventions pass; the other product build jobs are skipped by eligibility. The reported modest lookup improvement is scoped appropriately and does not claim a frame-rate win. No remaining blocker found.
What
Driver::newestWriterLocked(used byclassifyPendingWritewhen a capture reads memory with pending GPU writes, and by write evidence) walked the 4096-entry written-buffer ring from the newest entry on every lookup, so a range without a recorded writer cost a full walk. In GTA V cutscenes it was the hottest function on the packet thread not touched by an open pull request.The ring is now a
WrittenBufferIndex(Execution/include/Driver/Memory/WrittenBufferIndex.hpp). It keeps the same deque, capacity and eviction order, and also files each entry's position under the 64 KiB granules it covers; entries wider than 16 granules (1 MiB) go on a short list of their own so a large buffer does not make a push expensive.Newest(begin, end)walks only the query's granules and that list, newest first, applies the same overlap test as before and returns the newest match, so it returns the same entry as the walk (an empty entry is filed under the granule of its address, since the old test also matches it). Queries wider than 16 granules, and empty queries, still walk the ring.WrittenBufferandWrittenBufferRingmove into the new header unchanged; the three push sites callPush, anddescribeWritersiteratesEntries().Memory. The entries stay in the same 4096-entry deque as before (64 bytes each, 0.25 MiB). On top of that, the index keeps one
unordered_mapnode with astd::deque<std::uint64_t>for each distinct 64 KiB granule that a non-wide entry covers. With its first 512-byte chunk, that is about 700 bytes per granule on libstdc++. A granule's 65th and later sequences cost 8 bytes each. A non-wide entry covers at most 16 granules, so the index files at most 4096 × 16 = 65,536 granules. Wider entries cost 8 bytes each on their own list. Measured with a countingoperator newaround the class (GCC 16.2.1, libstdc++), as the peak live heap of the class including its entries while pushing three times the ring capacity:So the upper bound is about 44 MiB, against 0.25 MiB for the ring on
main. It is reached only when the last 4096 recorded writes went to 4096 different buffers of about 1 MiB that share no granules. In GTA V (PPSA04264 01.011.001) the index stays far below that. I ran a 307 s session from the title through the save load, the first Story cutscene and 136 s of the second, with temporary counters sampled each second. The sample estimate counts the same allocations as the table and reads within 3 % of it, slightly high, for each row. The ring filled to 4096 entries within 66 s of the first recorded write. The peak was 2,360 granules and 639 wide entries, about 1.9 MiB, while the save loaded. During cutscene 1 the index held 25 to 65 granules (about 0.4 MiB), and during cutscene 2 at most 322 granules (about 0.5 MiB).Tested
CachyOS Linux (kernel 7.3.0-4-cachyos), GCC 16.2.1, Release, NVIDIA GeForce RTX 4070 SUPER (615.78.08).
agc_written_buffer_index(new): pushes 20,000 random entries (empty, small, medium and wider than 1 MiB) into a 64-entry index, so it evicts constantly, and after each push compares 8 random queries (also empty and wider than 16 granules) against the linear walk overEntries(); plus explicit newest-wins and eviction cases. With the wide-entry list left out ofNewest, it fails on the first differing query.Focused tests,
main0cd07dc4aband this change on it, withANYPS5_REQUIRE_VULKAN=1 ANYPS5_NO_SHADER_CACHE=1 VK_INSTANCE_LAYERS=VK_LAYER_KHRONOS_validation ctest --test-dir build-tests -V -R '^(agc_driver_copied_writer_copy|agc_driver_copied_writer_label_order|agc_driver_pending_exact_surface|agc_driver_indirect_arguments_pending_producer|agc_driver_gated_dispatch_capture|agc_space_pending_writes|agc_pending_write_snapshot|agc_written_buffer_index)$':main7/7 pass, this change 8/8 pass; noVUID-line and no missing-layer message in either log. Under gdb,WrittenBufferIndex::Pushis reached 65 times inagc_driver_gated_dispatch_captureand 85 times inagc_driver_indirect_arguments_pending_producer, andNewest4 times in the latter; the other tests do not reach the ring, so the lookup's equivalence rests mainly on the new test.GTA V (PPSA04264 01.011.001),
perf record -F 499 -e cpu-clock:u --call-graph dwarfover the first 20 s of the first two Story cutscenes, one run per build, both onmain86fb985abb+ fix(shader): load and store 16-bit data of converted packed images #11580 (which the title needs, GTA V (PPSA04264) stops when the save loads: compute shader reads or writes a converted float image with 16-bit data #11425) + chore(agc): log the storage writeback submit and done lines only with APS5_DEBUG_LOG #12244, the second with this change cherry-picked; same machine, warm shader cache. Share of all samples:newestWriterLocked(with callees)classifyPendingWrite(with callees)WrittenBufferIndex::Push+evictOldestThe saving is about 1 % of the packet thread, below the run-to-run variation of whole-thread and frame-time numbers, so I am not claiming a frame-rate change.
Performance captures (details, alignment and
trace_digest.pyoutput in the comment below; traces attached there):aps5-summary.jsonllines of the 75–105 s window, base9f2d365268then this PRe98000093a:{"commit":"9f2d365268de","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_EVENTS":"4194304","APS5_TRACE_SECONDS":"75:30","APS5_TRACE_SUMMARY_EVERY":"10"},"gpu":"NVIDIA GeForce RTX 4070 SUPER","driver":"NVIDIA 615.78.08","os":"Linux 7.3.0-4-cachyos","cpu":"AMD Ryzen 7 5800X 8-Core Processor","cpu_threads":16,"memory_mib":64203,"title_id":"PPSA04264","title":"Grand Theft Auto V","content_version":"01.011.001","resolution":"3840x2160","window":{"spec":"seconds 75:30","first_present":2444,"ms":30014.412},"presents":403,"frame_ms":{"p50":73.748,"p90":79.862,"p99":110.775,"max":138.755},"gpu_busy_ms_per_present":36.963,"worker_busy_pct":{"queue worker 0x0":97.973},"top_phases_ms_per_frame":{"Driver.Packet":69.797,"Graphics.Draw":41.297,"Driver.Packet.draw":37.758,"Driver.Draw":37.408,"Graphics.Draw.draw_and_resource_release":35.681,"Driver.Packet.dispatch":24.439,"Driver.Draw.shader_compile_and_link":24.424,"Shader.DrawStage":18.971},"trace_dropped":false,"trace":"aps5-trace-9f2d365268de-1333741.json.gz"} {"commit":"e98000093a1a","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_EVENTS":"4194304","APS5_TRACE_SECONDS":"75:30","APS5_TRACE_SUMMARY_EVERY":"10"},"gpu":"NVIDIA GeForce RTX 4070 SUPER","driver":"NVIDIA 615.78.08","os":"Linux 7.3.0-4-cachyos","cpu":"AMD Ryzen 7 5800X 8-Core Processor","cpu_threads":16,"memory_mib":64203,"title_id":"PPSA04264","title":"Grand Theft Auto V","content_version":"01.011.001","resolution":"3840x2160","window":{"spec":"seconds 75:30","first_present":2741,"ms":30014.360},"presents":451,"frame_ms":{"p50":68.988,"p90":76.357,"p99":108.921,"max":588.045},"gpu_busy_ms_per_present":33.496,"worker_busy_pct":{"queue worker 0x0":93.744},"top_phases_ms_per_frame":{"Driver.Packet":59.260,"Graphics.Draw":33.805,"Graphics.Draw.draw_and_resource_release":29.403,"Driver.Packet.draw":29.328,"Driver.Draw":29.035,"Driver.Packet.dispatch":20.850,"Driver.Draw.shader_compile_and_link":18.695,"Recorder.Sync":16.659},"trace_dropped":false,"trace":"aps5-trace-e98000093a1a-1327440.json.gz"}python3 tools/check_conventions.py --base origin/mainpasses.Overlap:
tools/pr_overlap.pyreports #11439, which changes theprogramargument of thewrittenBuffers.push_backcall inBufferCopy.cpp(L131) that this PR turns intowrittenBuffers.Push. Whichever merges second keeps #11439's argument inside the other call; no behaviour overlaps. The 66 pull requests it lists as not compared (they conflict withmain) shareCMakeLists.txtorDriver.hpp; I checked their diffs (#5021 by its file list and itsCMakeLists.txtpatch, since its full diff is too large to fetch): none adds, removes or edits thewrittenBuffersmember,WrittenBuffer,WrittenBufferRingornewestWriterLocked.Checklist
main; no other open PR implements the same functionsNotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file.mdfiles or images added to the repository (attach them to this PR)🤖 Generated with Claude Code