Skip to content

perf(agc): fast revalidation over untracked memory proves a surface by its stamp until the next ordering point - #6192

Open
TheAggie wants to merge 4 commits into
boykopovar:mainfrom
TheAggie:perf/agc-untracked-fast-revalidate
Open

TheAggie wants to merge 4 commits into
boykopovar:mainfrom
TheAggie:perf/agc-untracked-fast-revalidate

Conversation

@TheAggie

@TheAggie TheAggie commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What

Stacked on #6187; only the last two commits are this PR.

A cached resource template is reused only after ShaderResources::Revalidate proves its textures and storage images current. The fast proof (fastRevalidate) answers from write stamps; a surface in memory the write watch does not cover (on Windows, every host-pointer import of the guest mapping, #1441) has none, so CollectWrites returns 0, the fast proof fails with collect, and the full walk repeats every texture lookup.

In RetroArch's Flycast core (PPSA99169) on Windows: 2.06 million of 2.2 million revalidations per 10 s took the full walk for that reason, and revalidation took 15.2 s of thread time per 10 s in all.

  • When a full walk proves a template (captureValidation), each surface over untracked memory also records a stamp: the collect epoch, NewestStamp (driver stores into the range), ForgetStamp (perf(agc): an untracked comparison memo holds until memory it covers is forgotten #6187) and the recorder's newest pending write note over the range. Storage images get the same, in storageUntracked.
  • fastRevalidate accepts such a surface while its stamp is unchanged, instead of failing with collect, and leaves it out of the UnchangedSinceAll query. Everything else in the fast proof is as before: the keys, the pending-image scan and the cache checks still run.
  • The rule is the one compareUntracked already uses for untracked memory: a CPU store there is seen at the next ordering point (docs/dev/ARCHITECTURE.md, "Untracked storage image comparisons"). A collect epoch of 0 or a forget in progress gives no stamp, so the full walk runs.
  • APS5_VERIFY_UNTRACKED_PROOFS=1 runs the full walk beside every fast proof that involved an untracked surface and reports how often it disagreed.

Tested

Windows 11, NVIDIA GeForce RTX 5070 Ti, MinGW-w64 GCC 15.2.0, on main 8cd937e29 with #6187; rebased onto main 92e50418f (only a CMake test list conflicted), where the full suite passes 1277 of 1296, 14 skipped, failing only the known tests below.

  • ctest: 1229 of 1249 pass, 14 skipped. guest_call_trace_abort and windows_backtrace_report fail, and fail the same way on plain main here. agc_driver_prepared_graphics_prebuild_0/_1 and undeliverable_host_exception fail under my test runner because it skips their environment changes and pass pattern; run with those, they pass. agc_driver_multisample_fill_host_only failed once in the parallel run and passed three times out of three run alone.
  • New test agc_untracked_revalidate (agc_driver_recorder_tests --untracked-revalidate-only): a sampled template over an untracked allocation proves on the fast path twice in one collect epoch; after a CPU store and a new epoch it takes the full walk. Without this change the test fails with "a sampled template over untracked memory took the full walk in the same collect epoch".
  • RetroArch (PPSA99169), Flycast, Crazy Taxi attract demo, APS5_VERIFY_UNTRACKED_PROOFS=1: 1,600,000 fast proofs over untracked memory checked by the full walk, 0 disagreed.
  • Same demo, ANYPS5_PRESENT_MODE=immediate, unprofiled, on the local stack described in perf(agc): an untracked comparison memo holds until memory it covers is forgotten #6187 with perf(agc): an untracked comparison memo holds until memory it covers is forgotten #6187 included: two runs each, audio produced at 63.1% and 62.6% of real time without this change against 65.3% and 66.9% with it. Fast proofs per 10 s went from 0.13 million of 2.2 million revalidations to 1.96 million of 2.48 million; the committer's lookup phase went from 9.75 to 5.75 µs per draw. I have no measurement of main alone.
  • Linux is untested; on Linux most surfaces are write-watched and keep the existing path.

Related: #6060 keeps Windows imports of private write-watched memory on GetWriteWatch when a start-up probe allows it; memory it keeps watched never reaches this path, so this PR matters for what stays untracked (imports through views, devices whose probe fails). I have not measured how much of the Flycast workload #6060 would cover.

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: perf(agc): an untracked comparison memo holds until memory it covers is forgotten #6187
  • AI-assisted: yes. Written with Claude Code under my direction.

🤖 Generated with Claude Code

@fdfawef324

Copy link
Copy Markdown
Contributor

@TheAggie I had a variant of this running locally when #6192 went up, and measured it at Astro Bot's title (PPSA21567, the collect case from #357). The design difference and the numbers are below; the numbers say the collect failures are not where Astro Bot's revalidation time goes.

Design difference. Instead of a stamp taken in captureValidation, the fast proof reuses the keys the lookups already decide with:

  • Snapshots: cachedTexture puts its untrackedKey into the lookup record. That key is the write-watch hint, NewestStamp, ForgetSerial and the newest write note, taken before the bytes are read or compared. The proof accepts the surface while the key is unchanged and !SnapshotWriteOverlaps, which is the kept() rule.
  • Views and storage images: the proof accepts while the image's comparedGuestBytes equals the stamp compareUntracked would take now. It also requires that the saved bytes hold, that no save is in flight, that no unit kept at an unwatch holds, and that no recorded write is pending over the surface (Refresh would flush that write first).
  • Across ordering points: on Windows the hint replaces the epoch for hinted ranges, so the proof survives ordering points until a CPU store marks the range.
  • Why the keys come from the lookups: a stamp read at capture time is read after the lookup read the bytes. A CPU store landing in between, whose mark another hint walk consumed, would then be missed. With the epoch in the stamp, as in perf(agc): fast revalidation over untracked memory proves a surface by its stamp until the next ordering point #6192, that cannot happen, which is why I took the lookups' keys rather than extending the capture stamp.

On a microbenchmark (16 untracked sampled surfaces) this took Revalidate from 86–93 µs to 37–41 µs at one proof per collect epoch, and from 52–56 µs to 10–13 µs at eight per epoch.

At the title it saved nothing. Setup:

  • RTX 3060 (591.86), Ryzen 9 5900X, Windows Server 2025, main e858dbd78 plus the change, APS5_PROFILE_DRAW=1.
  • Same build for both runs; "off" sets APS5_NO_UNTRACKED_PROOF=1.
  • Both columns are the same scene (the title screen after the first button press), 7 windows each, aligned by present count: off is presents 829–1400, on is presents 346–916.
  • Mean per 10 s:
off (= main's rule) on
presents 81.6 81.4
GPU ms per present 44.3 44.6
draws 1531 1530
µs per draw 2329 2340
lookup / record / readTarget ms 1087 / 77 / 2359 1062 / 76 / 2397
revalidations 5898 5874
revalidate ms 3461 3424
µs per revalidation 587 583
fast / full 567 / 5330 4111 / 1763
fast-fail collect / untracked memo / pending image 5330 / 0 / 0 0 / 1763 / 1296
surfaces proved from untracked memos (sampled / storage) 0 / 0 4945 / 2982
[textures] untracked snapshots kept by hint / compared 337 / 0 342 / 0
collect epochs 27946 27854
[guestmem] compare 3.47 s, 35149 MiB 3.50 s, 35048 MiB

The one window of the title intro before the press agrees:

  • off: 89 presents, 1623 µs/draw, 696 µs per revalidation;
  • on: 91 presents, 1541 µs/draw, 680 µs per revalidation, with fast 350 → 2306 and full 2980 → 1112.

So the proof does what the counters promise:

  • collect failures go from 5330 to 0 per 10 s;
  • the fast share goes from 10% to 70%;
  • the hint holds (kept 342, compared 0).

But a revalidation costs the same either way. Solving the two rows, a full walk costs only about 7 µs more than a fast proof here, and the other ~580 µs of each Revalidate is spent outside the image lookups. My guess is the epoch gate's registry scan and flush for imported regions (0 registry scans skipped in every window) or CompletePendingCpuWrites, but I have not split it. Most of the ~5900 revalidations per 10 s come from dispatches, not the ~1530 draws.

On safety, both runs show the same three guest memory changed while GPU results were pending warnings and nothing else. APS5_VERIFY_PROOFS was not set in the title runs. With it, the 565 agc_ ctests pass and 4,000 benchmark proofs were each checked against the full walk without an abort.

I'm not opening this as a competing PR: at this title it doesn't buy anything over #6192, and #6192 has measured gains on Flycast. If you do want the stamp to outlive the epoch for hinted ranges, the test I used (agc_untracked_hint_revalidate) checks a view, a snapshot and a storage image across 20 epochs without stores. It also checks that the proof does not hold after a changed CPU store, a store of the same bytes, a driver store, or over a pending recorded write. I can push it if that's useful.

AI-assisted: yes

This was referenced Oct 15, 2026
TheAggie and others added 4 commits October 15, 2026 10:07
…is forgotten

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…son memo

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y its stamp until the next ordering point

APS5_VERIFY_UNTRACKED_PROOFS=1 runs the full walk beside every such proof and counts disagreements.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n a collect epoch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TheAggie

Copy link
Copy Markdown
Contributor Author

@fdfawef324 thanks for measuring this at Astro Bot's title, and for the detailed write-up.

Your table matches what #6192 claims and no more. It removes the collect fast-fails. On the Flycast demo those fails were where the revalidation time went. At Astro Bot's title the ~580 µs of each Revalidate spent outside the image lookups dominates, so the proof can't show there. I haven't split that part either. If it is the epoch gate's registry scan or CompletePendingCpuWrites, that is a separate change, and a better target for that title than this PR.

On letting the proof outlive the epoch for hinted ranges: I'd keep #6192's stamp with the epoch in it, because of the race you describe (a store between the lookup's read and the capture whose mark another hint walk consumed).

Your agc_untracked_hint_revalidate test would still be useful. Its negative cases (a changed CPU store, a store of the same bytes, a driver store, a pending recorded write) could be run against #6192's rule too. If you push it, as its own PR or as a branch I can pull from, I'll run it against this branch and report back.

I've rebased #6187 and #6192 onto current main locally and will push them after the tests rerun.

AI-assisted (Claude Code)

@ps54all

ps54all commented Oct 17, 2026

Copy link
Copy Markdown

@TheAggie MGS4 (PPSA18194) data on Windows, plus a small extension that may belong in this PR.

What we saw. With #6192 (and #6187) on our test stack, MGS4 gameplay still takes the full walk for about 25.8k of 140k revalidations per 10 s, all collect fast-fails. A diagnostic on that path shows every failure differs from the recorded stamp only in the collect epoch: the surfaces (about 360, 0.2-1.4 MiB each, in the arena at 0x2d...-0x2e...) are Windows host-pointer imports, and the queue worker bumps the epoch about 160 times per 10 s. So each template over them takes one full walk per epoch.

Extension. On Windows, #4878 already keeps write-watch marks of unwatched blocks as a hint, and compareUntracked puts UntrackedHint into its memo key in place of the epoch. The same rule works for the fast proof's stamp:

 std::array<std::uint64_t, 4> untrackedRangeStamp(std::uint64_t address, std::size_t bytes) {
     const auto epoch = GuestMemory::CollectEpoch();
     if (epoch == 0 || bytes == 0 || (GuestMemory::ForgetSerial() & 1u) != 0) return {};
+    const auto hint = GuestMemory::UntrackedHint(address, bytes);
     const auto* recorder = Recorder::Active();
-    return {epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetStamp(address, bytes), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0};
+    return {hint != 0 ? hint | (1ull << 63u) : epoch, GuestMemory::NewestStamp(address, bytes), GuestMemory::ForgetStamp(address, bytes), recorder != nullptr ? recorder->NewestWriteNote(address, bytes) : 0};
 }

A CPU store is still seen at the next ordering point: UntrackedHint walks the range's marks once per collect epoch and thread, so a marked page gives a newer hint and the stamp fails as before. Ranges without a hint (Linux, tracked blocks) keep the epoch. The top bit only keeps a hint from ever equalling an epoch value.

Measured (Windows 11, RTX 3080, NVIDIA 610.74, i9-10850K; our integ stack with #6192/#6187; one build, the change switched off by an env var for the control; 540 s each, the same save, medians of the last 18 windows of 10 s in gameplay):

per 10 s hint stamp epoch stamp (#6192 as is)
full walks 585 25,824
collect fast-fails 463 25,704
revalidate time 732 ms 984 ms
committer lookup phase 1604 ms 1948 ms
committer us/draw 52.0 54.5
FPS (median from t=360 s) 5.70 5.65

Frame rate does not move here: MGS4's draw thread is bound by other phases. APS5_VERIFY_UNTRACKED_PROOFS=1 with the change (420 s run): 2,000,000 fast proofs over untracked memory checked by the full walk, 0 disagreed. HUD and text match the control in screenshots. One pair of runs only; no new test yet (the obvious one: a template over a hinted import stays on the fast proof across epochs without a store and takes the full walk after a CPU store).

Feel free to fold it in if it fits your PR; if you would rather keep #6192 as is, I can open it separately once this one merges.

AI-assisted (Claude Code); the runs and numbers above are ours.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 17, 2026
@CPritch

CPritch commented Oct 18, 2026

Copy link
Copy Markdown

GTA V (PPSA04263, 01.010.002) on Linux, Performance mode, prologue bank, measured on this head (2d03040). #7515 is merged in both builds of each pair for its counters; the desktop pair also has #7646, since main c4c2b3a does not start without it (#7636). Merging onto main hits one hunk in fastRevalidate's storage-image loop: main's depth-surface guard and this PR's stamp check. I kept the guard, followed by this PR's lines.

machine base p50 without / with this PR change time in revalidation per 10 s
i5-12400F, RTX 4060, NVIDIA 580.126.20 c4c2b3a 125.5, 124.7 / 113.3, 113.5 ms -11.7 ms (-9.4 %) ~1.65 s / ~0.88 s
Ryzen AI 9 465, RTX 5060 Laptop, NVIDIA 615.71.09 5057673 101.3, 101.8 / 95.4, 96.8 ms -5.5 ms (-5.4 %) bytes compared 23.3 / 8.8 GiB

Two runs per build, alternating, 120 s windows after the load. GPU time per present is unchanged on both machines, so the gain is on the CPU side. On the desktop the draw committer is the bottleneck, and ShaderResources::Revalidate's memcmp was about 21 % of it before this change.

APS5_VERIFY_UNTRACKED_PROOFS=1 on the laptop: 0 disagreements in 700,000 checks over 3 runs, one of them 900 s long. The verifier prints its totals every 100,000 checks, so the checks after the last print of each run are not counted. On this title most stamped surfaces are textures in partially committed memory: CollectWrites returns 0 because the write watch does not cover the whole range, not because the memory is untracked. The PR's argument is written for untracked memory; the verifier agrees in these cases too, but the counters cannot split textures from storage images, so that part is from reading the code. Rendering looked the same in the frames we compared.

Not tested: Windows, AMD GPUs, Fidelity mode.

AI-assisted: yes

@github-actions github-actions Bot added perf Performance change perf numbers missing perf pull request without a before and after summary line labels Oct 18, 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* build needs rebase Conflicts with main, rebase needed perf numbers missing perf pull request without a before and after summary line perf Performance change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants