Repository navigation
perf(agc): cache a draw key the second time it is seen - #5216
Conversation
A draw whose key has no entry captured its stages, built a variant per stage (a copy of every captured region) and inserted a draw entry. A key that never comes back paid for the variants and the entry for nothing, and its entry pushed a live one out of the cache. Draws whose user data changes every frame are such keys. The driver now remembers the keys it has seen once, in a table of four-way buckets holding four times the cache's size in keys. A key gets its entry at its second sight; a key that already has an entry still takes new variants at once. A full bucket replaces its oldest key, so keys that share a bucket do not push each other out until more than four of them come between two sights of one. Test: agc_driver_draw_cache_reuse draws three times. The first two draws find no entry, one entry is inserted, and the third draw hits it. A sighting table of one bucket keeps two keys drawn in turn and replaces its oldest key when full.
|
Data point from another title: Diablo II: Resurrected (PPSA03912), Linux, RTX 3080 Ti, in game. The draw cache hits about 0.1% there (46087 lookups per 10 s, 43 validated), the same pattern as described here. I ran this PR together with #5407 and #4947 (not isolated); no abort, and the window-title FPS went from about 2.4 to about 3.6 on average, noisy because the run swaps. Details on #5407. AI-assisted; the runs are from my machine. |
|
Holding this for review and a few more runs before it merges. What would help:
|
|
A/B for this PR alone, as requested above. Diablo II: Resurrected (PPSA03912, 01.039.000), Linux, RTX 3080 Ti (NVIDIA 615.71.09), RAM-limited machine (31 GB, the run swaps),
Mean 36.5 on A and 39.3 on B, about 8% more presents, and the two sets do not overlap. Within a run the windows are steady (within one present). The GPU busy time is the same, and the CPU idle gap per frame goes from about 120 ms to about 100 ms, so the gain is small. The hit rate stays at about 0.2% (the title's user data changes on almost every draw), so this PR saves the insert and eviction work and nothing more there. One thing to weigh: the title segfaulted while loading in 2 of the 5 launches with this PR (B1 and B2, both fine on retry) and 0 of 3 without it. Both crashes were a lookup in a hash table with a null table pointer inside the game's own code (same function, offsets 0x1b9d787 and 0x1b9d7a9 in the relinked executable), which looks like a race between game threads at load and not the driver, and the title has crashed at load before on builds without this PR. Five launches is too few to rule this PR out, so I would not read more into it than that. AI-assisted; the runs are from my machine and I can attach the |
…t is seen) into fex-upstream Admission is skipped while APS5_DRAW_RELOCATION is on: relocated draws must be cached at first sight to be found under their moved pointers.
|
Isolated A/B of this PR against
About +10%. No aborts. On top of #494 + #1193 + #1202 it is neutral: 6.33 / 5.72 / 7.64 FPS without it, 6.28 / 6.07 / 7.67 with it. It merges with #494 with three adjacent-line conflicts ( |
Since boykopovar#5216 a key gets its entry at its second sight, so a shape whose user words change on every draw never got an entry and its data-only hits never started. A key whose shape was seen before but has no entry is now admitted at its first sight. A shape that already has an entry serves data-only hits from it, and its new keys are still admitted at their second sight, so per-draw keys don't churn the cache. Shapes are remembered in a second sightings table of the same size. agc_driver_draw_cache_reuse draws three times with new pixel user words over a cached shape (three data-only hits, the vertex stages reused, no insert), then three keys of a new shape (the second one is admitted), and runs again with APS5_VERIFY_DRAW_DATA_HITS=1. Co-Authored-By: Claude <noreply@anthropic.com>
Upstream main now carries the final versions of PRs boykopovar#1193, boykopovar#1201, boykopovar#1202, boykopovar#5216 and boykopovar#4465, so their draft copies on this branch give way to main's: the snapshot ring, the pipeline identity memo, the code-hash memo and APS5_RESIDENT_LINEAR_TARGETS are gone with them. The port of boykopovar#4943 (draw-cache relocation across moved user-data pointers, not merged upstream) is dropped: the draw prefetch replaces it. Kept on top of main: the arm64 FEXCore runner and macOS libraries, the MoltenVK settings, rect-list clip and cull distances, GPU-ordered in-place vertex inputs, the draw pipeline's spin-first locks and lock-free overlap check, SharedVector binding flags, the result-memo admission, the draw prefetch (APS5_DRAW_PREFETCH) and the 1D T# sample modes. New for arm64: fegetenv from the runner's guest control words, and the direct-memory reuse map on macOS. On macOS the guest buffers keep one descriptor array (no buffers_coherent or buffers_u8 aliases): MoltenVK aliases a descriptor array only through Metal argument buffers, which the driver turns off, and Vampire Crawlers' first compute pipeline failed to compile.
What
A draw whose key has no entry in the draw cache captures its stages, builds a variant per stage (a copy of every captured region) and inserts a draw entry. A key that never comes back pays for the variants and the entry for nothing, and its entry pushes a live one out of the 4096-entry cache. Draws whose user data changes every frame are such keys: in a level of PPSA21564 almost no lookup finds an entry (152,112 lookups, 90 hits per 10 s), and
cacheDrawStages(the variant copies and the insert) is 4.3% of the queue-0 worker's samples, before the frees of the evicted entries.The driver now remembers the keys it has seen once (
DrawKeySightings): 16,384 keys in four-way buckets, four times the cache's size. A key gets its entry at its second sight. A key that already has an entry still takes new variants at once. A full bucket replaces its oldest key, so keys that share a bucket don't push each other out unless more than four keys of that bucket come between two sights of one of them.The cost is one more full draw for a key that does repeat: its third draw is the first hit, not its second. What a draw records is unchanged; only when an entry is made changes.
This is the idea of #3985, which was closed. Its table was direct-mapped, so two recurring keys in one slot evicted each other and were never cached; the buckets here are the fix discussed there. It was closed for no frame-time gain in a menu where few keys repeat; the scenes below where keys don't repeat do gain.
Tested
Linux (CachyOS, kernel 7.2, GCC 16.2, RTX 4090, NVIDIA 610.57.04), Release,
BUILD_TESTING=ON.agc_driver_draw_cache_reuseandagc_driver_draw_cache_reuse_offnow draw three times and require 3 lookups, 2 without an entry and 1 insert. With only that change to the test onmainboth fail:the three draws made 3 draw-cache lookups (1 without an entry), expected 3 lookups with 2 absent. The same test runs a one-bucket table: two keys drawn in turn are both admitted at their second sight, and a full bucket replaces its oldest key.ctest -R "agc|shader": 477 of 477 on this branch (main075169f).python3 tools/check_conventions.py --base origin/mainpasses.PPSA21564,
main2a2a3dc against the same commit with this change, same build options, warm shader and pipeline caches (warm-up runs not counted), runs alternated on an otherwise idle machine. Title and menu are frames per second over fixed windows. The levels advance a fixed step per frame below 30 fps, so they are milliseconds per present over the same level frame indices.mainAPS5_PROFILE_DRAW=1)In that later level, with the profile on, key, lookup and validate fall from 4.2 to 1.6 us per draw packet.
Summary lines of
APS5_TRACE_SECONDS=90:20in the same later level, withoutAPS5_PROFILE_DRAW:mainfirst, this change second (p50 400.5 to 372.2 ms per present). The builds were made from exported trees, socommitreadsunknown. Both traces filled the per-thread event buffer (trace_dropped); the frame times come from the presents and are complete.APS5_LOOP_GUARDandAPS5_DUMP_FRAMESare set by my test harness on both sides; the other three variables it sets are not read bymain. The trace files are not attached (the PR was opened from the command line); I can add them.{"commit":"unknown","dirty":null,"env":{"APS5_DUMP_FRAMES":"6000","APS5_INEXACT_SINGLE_LANE":"500597a00","APS5_LOOP_GUARD":"2000","APS5_PAD_PRESS":"","APS5_TOLERATE_DISPATCH_FAILURES":"1","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"90:20","APS5_TWO_LANE":"500597a00"},"gpu":"NVIDIA GeForce RTX 4090","driver":"NVIDIA 610.57.04","os":"Linux 7.2.3-1-cachyos","resolution":"3840x2160","window":{"spec":"seconds 90:20","first_present":1595,"ms":20037.491},"presents":50,"frame_ms":{"p50":400.485,"p90":415.494,"p99":431.600,"max":431.600},"gpu_busy_ms_per_present":118.908,"worker_busy_pct":{"queue worker 0x0":99.374,"queue worker 0x40":98.126,"queue worker 0x48":97.957,"queue worker 0x46":0.438},"top_phases_ms_per_frame":{"Driver.Packet":1185.391,"Driver.Packet.memory_wait":776.389,"Driver.Packet.draw":356.213,"Driver.Draw":355.285,"Driver.Draw.shader_compile_and_link":168.414,"Driver.Draw.draw_and_resource_release":153.188,"Graphics.Draw":152.218,"Shader.DrawStage":143.506},"trace_dropped":true,"trace":"aps5-trace-unknown-4117737.json.gz"} {"commit":"unknown","dirty":null,"env":{"APS5_DUMP_FRAMES":"6000","APS5_INEXACT_SINGLE_LANE":"500597a00","APS5_LOOP_GUARD":"2000","APS5_PAD_PRESS":"","APS5_TOLERATE_DISPATCH_FAILURES":"1","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"90:20","APS5_TWO_LANE":"500597a00"},"gpu":"NVIDIA GeForce RTX 4090","driver":"NVIDIA 610.57.04","os":"Linux 7.2.3-1-cachyos","resolution":"3840x2160","window":{"spec":"seconds 90:20","first_present":1592,"ms":20131.935},"presents":54,"frame_ms":{"p50":372.186,"p90":381.003,"p99":409.912,"max":409.912},"gpu_busy_ms_per_present":114.064,"worker_busy_pct":{"queue worker 0x0":99.285,"queue worker 0x40":98.232,"queue worker 0x48":98.188,"queue worker 0x46":0.469},"top_phases_ms_per_frame":{"Driver.Packet":1103.855,"Driver.Packet.memory_wait":723.301,"Driver.Packet.draw":328.267,"Driver.Draw":327.265,"Driver.Draw.draw_and_resource_release":148.226,"Graphics.Draw":147.226,"Driver.Draw.shader_compile_and_link":145.765,"Shader.DrawStage":137.433},"trace_dropped":true,"trace":"aps5-trace-unknown-4111272.json.gz"}Not tested: Windows, AMD.
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)