Skip to content

perf(agc): insert a draw into the draw cache on the second sighting of its key - #3985

Closed
XairRrR wants to merge 1 commit into
boykopovar:mainfrom
XairRrR:perf/agc-draw-cache-admission
Closed

XairRrR wants to merge 1 commit into
boykopovar:mainfrom
XairRrR:perf/agc-draw-cache-admission

Conversation

@XairRrR

@XairRrR XairRrR commented Oct 11, 2026

Copy link
Copy Markdown
Contributor

What

The draw key covers the user data the stages read. In the menu of Uncharted: Legacy of Thieves Collection (PPSA05684) that user data holds per-draw buffer addresses, so a key almost never repeats: of about 45,000 draw-cache lookups per 10 s, none hit. Yet every draw built its stage variants (the captured words, copied and sorted) and inserted them, evicting the 4096-entry cache over and over.

A key with no entry is now only remembered the first time it is seen, in 16384 slots of key hashes, and its draw is inserted when the key comes again. Titles whose keys repeat are cached one draw later. The [draw-cache] line counts the first sightings left out. APS5_DRAW_CACHE_ADMIT_ALL=1 inserts every draw as before.

The draw-cache reuse test now issues its draw three times and expects the third to hit, plus one first sighting left out.

#494 attacks the same waste from the other side: draws that differ only in user data share their entry. This PR does not depend on it.

Tested

Linux, RTX 4070 Ti SUPER. Full ctest on this branch: all tests pass, check_conventions.py passes.
Uncharted does not run on plain main (it needs the image tables of #757/#758). On a local branch that carries them, in the menu: 7,688 first sightings left out and 209 inserts per 10 s, and the key/lookup/validate phase went from 5.4 to 2.4 µs per draw.

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed in TechnicalDebt
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes

…f its key

The draw key covers the user data the stages read, which in the menu
of Uncharted: Legacy of Thieves Collection (PPSA05684) holds per-draw
buffer addresses: of about 45,000 lookups per 10 s none hit, yet every
draw built its stage variants (the captured words copied and sorted)
and inserted them, evicting a 4096-entry cache over and over.

A key that has no entry is now only remembered (16384 slots of key
hashes) the first time it is seen, and its draw is inserted when it
comes again; titles whose keys repeat are cached one draw later. The
[draw-cache] line counts the first sightings left out.
APS5_DRAW_CACHE_ADMIT_ALL=1 inserts every draw as before. The draw-cache
reuse test now issues its draw three times: the third one hits.

Menu: 7,688 first sightings left out and 209 inserts per 10 s; the
key, lookup and validate phase went from 5.4 to 2.4 us per draw.
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Two keys that land in the same sighting slot overwrite each other (DrawCache.cpp:66-68): with A and B each drawn once a frame, neither ever finds slot == key, so neither is inserted and both rebuild their variants every frame. With a couple of thousand recurring keys in 16384 slots that is a few hundred draws that main caches today.

The early return at DrawCapture.cpp:88 also skips phaseTiming.Phase(DrawRowKeyLookupValidate), so 5.4 → 2.4 µs isn't like for like. A frame time before and after would show the gain (#218).

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

Adding to @oneandonlydean's comment above rather than repeating it. Both of their points are correct, and I reproduced the second from outside: with only DrawCapture.cpp's call site and DrawCache.cpp's admitDrawEntry reverted (headers kept so the test still compiles), agc_driver_draw_cache_reuse_tests exits 1 on the test's own message the three draws made 3 draw-cache lookups (1 without an entry), expected 3 lookups with 2 absent, while the PR head passes on the CI device (lavapipe). What I can add is the cause, the fix, and three things neither comment covers.

1. Cause of the colliding keys: the index reads 28 of the 64 key bits. (key ^ (key >> 29)) % 16384 mixes bits 0–13 with bits 29–42; bits 14–28 and 43–63 can never change the slot. Flipping each bit of a key one at a time: 28 bits can move the slot, 36 cannot, and the collision is therefore systematic for a whole key class rather than birthday chance:

0x0000000000001111 -> slot 4369
0x000000001fffd111 -> slot 4369     (differs only in bits 14..28)
0x0010000000001111 -> slot 4369     (differs only in bits 43..63)

A whole-word mix before the mask removes that class: k *= 0x9E3779B97F4A7C15ull; slot = (k >> 50) % SightingSlots; — measured, it takes the count of slot-influencing bits from 28 to 64. It is one multiply, and it does not remove birthday collisions (thousands of live keys in 16384 slots still collide), so it fixes the systematic class, not all of them.

2. On "never inserted" — a refinement, not a disagreement. For the strictly alternating pair in the example, neither is ever admitted, exactly as stated: each sighting re-arms the slot to the other key. With a less regular interleaving the re-arm makes it a delay rather than a loss — at the rate the description measures (7,688 first sightings per 10 s ≈ 769/s, assuming the writes spread across the slots) the expected slot lifetime is 16384/769 ≈ 21 s, so a key whose sightings are much further apart than that is usually re-armed before it is seen again. That matters for sizing the regression: a hard "never cached" is the degenerate case, not the general one.

3. The one-line fix for the phase. DrawPhaseTiming::Phase(X) charges the elapsed lap to X and restarts it (core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/DrawTiming.cpp:35-40); cacheDrawStages opens the lap at DrawCapture.cpp:87 and closes it at :130, and on the skipped path the next Phase() call is in Draw.cpp (:236/:268), so the slice lands under "vectors". Adding phaseTiming.Phase(DrawRowKeyLookupValidate); immediately before the early return makes before/after per-phase numbers comparable, and the figure in the description is that phase by name (DrawTiming.hpp:13), so it is the line to add before quoting it publicly a second time.

4. A test nit neither comment has. The new firstSightings == 1 expectation assumes the APS5_DRAW_CACHE_ADMIT_ALL override is unset; with it exported the same test fails on its own absent == 2 assertion (1 without an entry), which reads as a regression rather than as the override working. Either skip those two expectations when the variable is set, or pin it as a test property the way agc_shader_frontend_pair_off pins its flag.

5. Merge order, and explicitly no claim of a clash. Open #494 edits five of these same six files (DrawCache.hpp, Driver.hpp, DrawCache.cpp, DrawCacheStatistics.cpp, DrawCapture.cpp — not the test). The overlap bot has flagged nothing on this PR, so treat that as a rebase-when-it-lands note, not as a conflict.

Not verified, so it is not claimed. The perf figures need the title and a tree carrying #757/#758, so nothing here speaks to 5.4 → 2.4 µs beyond the accounting point above; and rendering equivalence is not claimed — no title on this machine. What I did check on that side: the only consumer of fresh past the call is Draw.cpp:362, and :363 clears an all-nullptr vector, so a first-sighting draw takes the full draw path with no recipe, which is also what it did before (an entry created in that same call could never have had a recipe).

Hermes Agent 🤖 at your service

@XairRrR

XairRrR commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @oneandonlydean and @Dextroid17, both points were right. I fixed them locally: sightings now go in 4-way buckets indexed by a multiplicative mix of the whole key, the early return is charged to DrawRowKeyLookupValidate, the reuse test no longer depends on APS5_DRAW_CACHE_ADMIT_ALL, and a unit test covers the colliding keys from the review. Then I measured frame time as asked (#218), and that decides it.

Uncharted: Legacy of Thieves Collection (PPSA05684) menu, same build, alternating unprofiled runs, fps / median frame over the last 120 s:

  • every draw inserted (main's behaviour, APS5_DRAW_CACHE_ADMIT_ALL=1): 7.71 / 116.6 ms, 7.13 / 124.3 ms
  • second-sighting admission: 7.24 / 126.6 ms, 6.90 / 124.3 ms

There's no gain beyond noise: the per-phase saving doesn't show up at the frame level. So I'm closing this rather than asking for more review time on it. Thanks again for the careful review.

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

Labels

agc libSceAgc*

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants