Skip to content

perf(libc): keep the guest allocation lease snapshot until the registry changes - #6917

Merged
oneandonlydean merged 2 commits into
boykopovar:mainfrom
milescoviello:perf/guest-lease-snapshot
Oct 19, 2026
Merged

oneandonlydean merged 2 commits into
boykopovar:mainfrom
milescoviello:perf/guest-lease-snapshot

Conversation

@milescoviello

@milescoviello milescoviello commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

GuestAllocationsAcquire (and GuestAllocationsAcquireAll) shares one snapshot of the registry between the leases taken while it is unchanged. The registry kept that snapshot only through a weak_ptr, though, so it lived only as long as some lease held it. Most leases are short: the driver takes one per address-based build, import refresh and range check. So the first lease after the previous one ended rebuilt the snapshot from scratch, walking the whole registry map and taking a reference to every readable range.

In MXGP 26 (PPSA30839) on Linux that rebuild is a large part of the queue workers' time. The run uses the PRs the title needs to get this far (#6814, #6094, #6160) plus #6308, #6184 and #6455, and both workers are at about 99% busy. perf record on the process puts std::_Rb_tree_increment at 9.9% of all samples, GuestAllocationsAcquire_nid_postfix at 9.7% and the snapshot vector's shared_ptr copies at 3.1%.

The registry now keeps the snapshot (shared_ptr) until the next registry change, where it is dropped exactly as before:

  • GuestAllocationsBegin, GuestAllocationsEnd and the pin wait reset it, under the registry mutex, as they already reset the weak pointer.
  • The pin wait resets it at the top of each round, before it counts the leases that hold a range (use_count() != 1), so a kept snapshot never counts as a lease.
  • A removed range is dropped from the snapshot by the change that removes it, so the kept snapshot holds no range longer than a lease taken at that moment would.

The test that required "a snapshot outlived its leases" to be false now states the new contract instead. After the leases end, the snapshot is still the registry's, and the next lease reuses it without a rebuild. Any registry change, even an empty mutation, drops it.

Tested

Ubuntu 24.04, GCC 13.3, Release, RTX 4070 Ti (NVIDIA 595.99.02).

  • agc_driver_graphics (leaseSnapshotTests and RunGuestLeaseWaitTests): with main's GuestAllocations.cpp it fails (a lease taken after the others ended, without a registry change, rebuilt the snapshot), and with the change it passes 3 of 3. Since 602b464, leaseSnapshotTests also makes a range unreadable through Mutation::Protect while both snapshots are kept: the call must not wait on them, the replaced range must expire, and the next readable lease must leave the range out while the next lease of all ranges shows it unreadable. Passes 3 of 3.
  • BdaResources' failed-import check now expects the kept snapshot's reference along with the registry's (use_count() == 2 instead of 1). Its other checks are unchanged, including that a removed owner expires. All 391 guest_* and libc tests pass, and the 501 tests matching ^agc_driver_|^agc_vertex_barrier|recompiler|shader_cache|request_serializer|spirv|^agc_shader|bda|journal pass except agc_driver_store_over_untracked, which fails on main too.
  • 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. The commits are local test merges: 7b386bafd265 is the stack (with perf(agc): time compute pipeline creation #6896's one-line compute pipeline trace timer), and 5f469824dc57 adds this PR's GuestAllocations.cpp change (the same library diff as this branch).
Build commit presents frame p50 / p90 / p99 (ms)
base, run 1 7b386bafd265 29 338.4 / 415.5 / 459.8
this PR, run 1 5f469824dc57 44 230.5 / 239.5 / 247.3
base, run 2 7b386bafd265 30 338.1 / 343.1 / 346.3
this PR, run 2 5f469824dc57 43 231.9 / 242.0 / 244.2

tools/trace_digest.py:

base 1: **seconds 100:10** from present 369: 29 presents, frame p50 338.4 / p90 415.5 / p99 459.8 / max 459.8 ms, GPU busy 118.9 ms per present
PR 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
base 2: **seconds 100:10** from present 368: 30 presents, frame p50 338.1 / p90 343.1 / p99 346.3 / max 346.3 ms, GPU busy 118.6 ms per present
PR 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

Summary lines (aps5-summary.jsonl, base 1, PR 1, base 2, PR 2):

{"commit":"7b386bafd265","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":369,"ms":10200.783},"presents":29,"frame_ms":{"p50":338.431,"p90":415.518,"p99":459.807,"max":459.807},"gpu_busy_ms_per_present":118.884,"worker_busy_pct":{"queue worker 0x0":99.611,"queue worker 0x20":99.536},"top_phases_ms_per_frame":{"Driver.Packet":700.083,"Driver.Packet.dispatch":298.609,"Driver.Dispatch":297.638,"Driver.Packet.memory_wait":284.109,"Vulkan.Dispatch":149.898,"Resources.Complete":144.679,"Vulkan.Dispatch.shader_resources":141.290,"Driver.Dispatch.capture_resources":60.594},"trace_dropped":false,"trace":"aps5-trace-7b386bafd265-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":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":"7b386bafd265","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":368,"ms":10140.142},"presents":30,"frame_ms":{"p50":338.075,"p90":343.082,"p99":346.269,"max":346.269},"gpu_busy_ms_per_present":118.599,"worker_busy_pct":{"queue worker 0x0":99.617,"queue worker 0x20":99.539},"top_phases_ms_per_frame":{"Driver.Packet":672.674,"Driver.Packet.dispatch":286.752,"Driver.Dispatch":285.828,"Driver.Packet.memory_wait":271.222,"Vulkan.Dispatch":141.910,"Resources.Complete":137.169,"Vulkan.Dispatch.shader_resources":133.907,"Driver.Dispatch.capture_resources":61.058},"trace_dropped":false,"trace":"aps5-trace-7b386bafd265-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"}

Traces: https://gist.github.com/milescoviello/336bdc50227a65c6ea50f86e84ffbe1d, files stack-7b386bafd265-run{1,2}.json.gz (base) and lease-5f469824dc57-run{1,2}.json.gz (this PR). They open in ui.perfetto.dev.

  • Earlier runs with APS5_PROFILE_DRAW=1 (which costs time, so they are not the speed claim): 90 s, interleaved, four runs each. Presents per 10 s:
Build presents per 10 s (the first window is the intro)
without last window 21, 22, 28, 28 (runs: 193 9 11 22 10 21 / 210 8 15 20 10 22 / 226 8 13 24 28 28 / 218 8 15 20 22 27 28)
with last window 40, 45, 42, 41 (runs: 242 9 14 24 16 40 / 247 8 15 27 33 45 45 / 239 8 16 25 19 42 / 215 6 12 26 41 41)

In separate 130 s runs with APS5_TRACE_SECONDS=100:10 (also with APS5_PROFILE_DRAW=1), the frame p50 is 342 and 345 ms without the change (2 runs) and 240 ms with it (1 run; the other is the crash below). A perf profile of the same phase with the change no longer has _Rb_tree_increment, GuestAllocationsAcquire_nid_postfix or the vector copies among its top entries; the largest is now PageSpan::runEnd (13.8%).

  • One of 12 runs with this change (a 130 s run) segfaulted 24 s in, and its core was not kept; the next 10 runs with it, and all runs without it, reached their limits. So did the 18 runs with it since then (10 of them also with perf(agc): cross uniform runs of cached guest pages 64 pages per load #6921). I found no path by which the kept snapshot could cause it (every reset happens under the registry mutex, and the pin wait resets before counting), but I can't rule it out.

@simonbronk ran it on Windows with Onimusha: Way of the Sword (PPSA27836) at its language menu (RTX 5070, 617.42, 3840x2160, 3 runs against 6 base runs). Frame p50 stays within the base's run-to-run range (84.4, 82.5 and 84.4 ms against 77.9–84.5 ms), so that scene shows no measurable change.

Not checked: macOS, AMD/Intel.

#6565 changes the same two lines in GuestAllocationsAcquire/GuestAllocationsAcquireAll, to leave retiring ranges out of new leases and skip storing the snapshot while a range retires. #6499 adds a Registry field right after the two snapshot members that this PR changes, so tools/pr_overlap.py reports an adjacent-line conflict there. #6951 changes the cache-hit and the final return lines of GuestAllocationsAcquire to pass the generation into the lease. The changes compose; whichever of them merges later needs a small rebase.

Checklist

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

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.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 6499 core/libs/prx/libc/src/GuestAllocations.cpp L39-40
PR 6565 core/libs/prx/libc/src/GuestAllocations.cpp L39-40

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
core/libs/prx/libc/src/GuestAllocations.cpp
PR 1835 core/libs/prx/libc/src/GuestAllocations.cpp
PR 4448 core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 5021 core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
core/libs/prx/libc/src/GuestAllocations.cpp
PR 5372 core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 6177 core/libs/prx/libSceAgcDriver/tests/GuestAllocations.cpp
PR 6197 core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 6412 core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 6628 core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 6879 core/libs/prx/libc/src/GuestAllocations.cpp
PR 7201 core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
core/libs/prx/libc/src/GuestAllocations.cpp
PR 7292 core/libs/prx/libSceAgcDriver/tests/BdaResources.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 cause is clear (the snapshot lived only as long as one lease, so the next lease rebuilt it by walking the whole registry; _Rb_tree_increment 9.9% and GuestAllocationsAcquire 9.7% of samples in MXGP 26) and the change is small. Questions:

  • The registry now holds a shared_ptr to the snapshot, so every Range in it stays alive until the next registry change, including ranges whose owner memory the title has already released through a path that doesn't mutate the registry. What destroys a Range's backing when its last lease ends? The test change use_count() == 2 ("a failed import pins its owner beyond the registry and its kept lease snapshot") shows that the kept snapshot now holds owners that nothing held before. Please state which owners can be pinned this way and for how long, and that none of them are waited on by the pin wait (use_count() != 1 is the check).
  • Anything that changes readable for a range without Begin/End has to reset the snapshot, or a lease gets the old readability. Mutation::Protect is used in the #6921 test; add a case here where a range turns read-only and the next lease must not return the old snapshot.
  • No before/after. Please post the summary lines of main and this PR from the same scene in PPSA30839 (no APS5_PROFILE_*) and the perf shares after the change, instead of the shares before it.

@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 (e3590fa) 6.65 / 6.60 (-0.4 %) 104 / 104 ms 46 / 46 ms

Within the run-to-run spread (about ±1.5 % here), no regression, 0 fatal.

AI-assisted: yes (Claude Code)

@simonbbby

Copy link
Copy Markdown
Contributor

Independent CPU-side checks for the first two review questions, on exact head e3590fa2ceb62a81a4f5cc57dfec22b5842eb610 (Ubuntu 24.04 x86-64, GCC 13.3). I compiled the complete, unchanged production GuestAllocations.cpp with a small standalone probe, real anonymous mmap allocations and real mprotect callbacks. No GPU/runtime stand-in was involved.

Ownership detail: Range contains addresses, sizes and flags, with no backing-storage owner or destructor. Dropping its last shared_ptr destroys that metadata; it does not itself free or unmap the address. The readable cache retains the nonempty readable ranges; the all-ranges cache retains every nonempty range. Both are cleared by Begin, End, and every RequireUnpinned scan. An independently held lease can retain the old metadata longer. Retaining a snapshot therefore does not make a free path that bypasses registry mutation safe; this test does not audit all such paths.

Observed in five fresh-process runs:

  • After acquiring readable and all-ranges snapshots and dropping both external leases, the range has three references (registry plus the two cached vectors), versus one on the parent. An empty mutation returns this to one; removal expires a weak reference to the metadata.
  • Protecting the middle page of a three-page RW allocation to RO expires the original metadata. The next snapshots contain three replacement ranges with writable neighbours and a readable, non-writable middle page. Protecting that page again to NONE expires the RO metadata: the readable snapshot contains only the two neighbours, while the all-ranges snapshot contains all three with the middle flags cleared. This distinction matters because RO is still readable.
  • Cached snapshots alone never invoke the pin waiter. With a real external lease, removal invokes the waiter once. The callback re-enters both acquire functions while the registry lock is released, recreates both caches, then drops the external lease. The next scan clears those caches and removal completes, expiring the weak reference.

Negative controls discriminate the reset paths: deleting only the two resets inside the pin-wait loop makes the reentrant callback case fail as still pinned; deleting only the Begin/End resets makes the empty-mutation check fail with retained cached references. The parent passes the protection and pin-wait compatibility cases, and fails the intended new snapshot-retention check.

This supplies registry-level protection/pinning evidence only. It does not establish concurrent GPU lifetime safety, resolve the previously reported unretained-mode title crash, or supply the requested unprofiled PPSA30839 FPS/after-change perf comparison.

@milescoviello

Copy link
Copy Markdown
Contributor Author

Thanks for the review, and thanks @simonbbby for the standalone probe. It answers the first two points independently and includes the negative controls.

Which owners the kept snapshots hold, and for how long. A Range is the registry's record of one piece of a guest allocation: address, size, access and allocation. It owns no memory, so dropping its last reference frees only the record. The guest pages are unmapped by the apply callback of Unmap/Remove, inside a Mutation, after GuestAllocationsRequireUnpinned.

Every registry writer is a Mutation member (Add, Remove, Protect, Unmap, RegisterMainImage, RegisterImage, UnregisterImage), and nothing calls them outside one. GuestAllocationsBegin resets both kept snapshots under the registry lock, and GuestAllocationsEnd resets them again before it releases the lock. So between two mutations:

  • the readable snapshot holds each nonempty readable Range once;
  • the snapshot of all ranges holds each nonempty Range once;
  • both let go at the next mutation of any range.

They never hold a record the registry has already dropped. Memory the title releases without a registry change keeps its record in the registry anyway, so the kept snapshot adds no lifetime there. A lease someone still holds keeps its snapshot alive, as it did before. In failedImportRemapTests, use_count() == 2 counts the registry's own record plus the kept readable snapshot.

The pin wait. GuestAllocationsRequireUnpinned resets both snapshots at the start of every round, before it counts use_count() != 1. That includes the rounds after it takes the lock back following a wait, when a driver worker may have rebuilt a snapshot in the meantime. Only leases that are still held count as pins.

Protect. 602b464 adds the case to leaseSnapshotTests:

  1. Take and drop a readable lease and a lease of all ranges, so both snapshots are kept, and require use_count() == 3.
  2. Make the range unreadable through Mutation::Protect. The call has to complete without the kept snapshots counting as a pin; otherwise it would wait APS5_PIN_WAIT_MS and throw.
  3. Require the old record to be expired. The next readable lease must not contain the range, and the next lease of all ranges must have it with readable and writable cleared.
  4. Make the range readable again, and require it to be back in the next readable lease.

agc_driver_graphics passes 3/3 on the branch.

Before/after. main does not reach this scene. main at 1b1f8483c (today, 94 commits before the current head) exits at 103 s, 3 s into the window, so it writes no summary. It hits deferred write-back failed: BDA access failed: address=0x1227d10251 ... bytes=4 (the unaligned 32-bit atomic that #6094 handles), then computed/data-dependent s_swappc_b64 call target ... is not statically resolvable (#6160). So the base stays the stack: main plus #6814, #6094, #6160, #6308, #6184 and #6455. A second session today, same scene, interleaved, no APS5_PROFILE_* switch, perf record -F 499 over 100–112 s of each run:

Build commit presents frame p50 / p90 (ms) _Rb_tree_increment GuestAllocationsAcquire
stack, run 1 7b386bafd265 29 348.4 / 357.7 11.7% 10.5%
this PR, run 1 5f469824dc57 41 244.1 / 252.7 0.07% 0.04%
stack, run 2 7b386bafd265 28 346.6 / 469.3 12.0% 10.1%
this PR, run 2 5f469824dc57 42 241.5 / 250.5 0.01% below 0.01%

tools/trace_digest.py:

stack 1: **seconds 100:10** from present 375: 29 presents, frame p50 348.4 / p90 357.7 / p99 363.6 / max 363.6 ms, GPU busy 120.0 ms per present
PR 1:    **seconds 100:10** from present 424: 41 presents, frame p50 244.1 / p90 252.7 / p99 257.6 / max 257.6 ms, GPU busy 124.6 ms per present
stack 2: **seconds 100:10** from present 354: 28 presents, frame p50 346.6 / p90 469.3 / p99 505.8 / max 505.8 ms, GPU busy 115.7 ms per present
PR 2:    **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

After the change, the largest share is PageSpan::runEnd, at 14.2% and 14.5% (#6921). The summary lines (session2-summary.jsonl) and traces are in the same gist as before, under session2-*: https://gist.github.com/milescoviello/336bdc50227a65c6ea50f86e84ffbe1d. The first session's numbers in the description (29/30 vs 44/43 presents) agree with these.

AI-assisted: yes (Claude Code)

@milescoviello

Copy link
Copy Markdown
Contributor Author

A second title, for scale. Metal Gear Solid 4 (PPSA18194), Linux 6.8, RTX 4070 Ti (595.99.02), Release, pad input. Four interleaved rounds of 300 s, no APS5_PROFILE_*. Base is main 0dc1562 plus what MGS4 needs here (#6814, #6842, #6237, local merges); the other build is the same plus this PR. The numbers are presents per 10 s interval, aligned on the dip between the launcher and the game.

Build 8–22 intervals after the dip, per run Mean ± sd 18–24 after, mean ± sd
base 468, 444, 420, 457 447 ± 18 426 ± 33
+ this PR 493, 507, 526, 409 484 ± 45 463 ± 32

That's 8% higher on average, but at four runs it's within the run-to-run spread (Welch t ≈ 1.5). So I'd read it as no regression on a second title, not as a measured gain.

The costs this PR targets dropped in every run (share of samples in a 12 s perf record at 225 s):

  • GuestAllocationsAcquire_nid_postfix: 0.80, 0.70, 0.87, 0.89% → 0.24, 0.43, 0.33, 0.42%
  • _Rb_tree_increment: 1.69, 1.93, 2.12, 1.82% → 1.10, 1.46, 1.68, 1.28%

They make up only 1–3% of MGS4's samples to begin with, against about 22% in MXGP, which fits the small difference in presents.

AI-assisted: yes (Claude Code)

milescoviello and others added 2 commits October 17, 2026 18:48
…ry changes

The registry cached its lease snapshot only while a lease held it, so the
first lease taken after the others ended rebuilt the snapshot from the whole
registry: every address-based build and import refresh paid a walk of every
range and a reference to each. The registry now keeps the snapshot until the
next registry change, which drops it as before; the pin check of a mutation
drops it before counting the leases that hold a range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A range turned unreadable must leave the next readable lease and show as
unreadable in the next lease of all ranges, the kept snapshots must not
count as a pin when the protection change waits for leases, and the
replaced range must not outlive the change.

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

Copy link
Copy Markdown
Contributor Author

A third title: Forspoken (PPSA03984). Linux 6.8, RTX 4070 Ti (595.99.02), Release, warm shader and pipeline caches, scripted pad input. main is 922dc89; the other build is main plus this PR's head 8f0bd51 (merge 0b6640f568eb).

Where the time goes. An 8 s perf record 50 s in, one run per build:

Symbol main this PR
GuestAllocationsAcquire_nid_postfix, self 17.4% 1.8%
std::_Rb_tree_increment, self 11.2% 2.1%

With the PR, the largest new share in that window is shader registration (PublishRegisteredShader at 15% inclusive). I'm opening a separate PR for it.

Summary lines, no APS5_PROFILE_* switch, APS5_TRACE_SECONDS=0:60:

main: commit 922dc8999ea0, seconds 0:60: 143 presents, frame p50 296.7 / p90 585.2 / p99 6437.0 ms, GPU busy 61.5 ms per present
PR:   commit 0b6640f568eb, seconds 0:60: 197 presents, frame p50 126.8 / p90 420.5 / p99 6281.1 ms, GPU busy 37.5 ms per present

One window went the other way. With APS5_TRACE_SECONDS=40:30, main had 56 presents (p50 398.1, p90 988.3 ms) and this PR 24 (p50 689.2, p90 6295.5 ms). In that run the PR's queue workers were busy only 15 to 30% of the window, against 97 to 99% on main. Its trace has 7,422 Shader.PrepareArtifact scopes and 80 s of Driver.WorkerWait, so the workers spent that window waiting on shader preparation. I read it as the two runs being at different points of the title (the caches were warmed by main runs), not as a slowdown, but it is a single run.

Whole runs, 120 s with APS5_PROFILE_DRAW=1, two interleaved rounds:

  • main: 202 and 203 presents, 29,500 and 31,300 compute dispatches, both to the limit.
  • This PR: 214 presents in 85 s and 332 in 110 s, 48,700 dispatches in the second run. Both stopped on vkAllocateMemory storage texture: Vulkan result -2, a device memory exhaustion that main doesn't get far enough to hit within 120 s. With fix(agc): release the texture caches when a device allocation runs out of memory #6174 added, two 150 s runs reached the limit with 480 and 488 presents (details). I don't think this PR causes the exhaustion, but I haven't proved that.

AI-assisted: yes (Claude Code)

@simonbronk

Copy link
Copy Markdown
Contributor

Tested on Onimusha's menu: no measurable change in this scene.

Setup: Onimusha: Way of the Sword (PPSA27836), boot to the language menu with no input, APS5_TRACE_SECONDS=60:30, warm cache. Windows 11 26200, RTX 5070 (617.42), 3840x2160. Base main 84b0d2f + #6557 #6727 #6762 (5c521efea), 6 runs (3 before and 3 after the PR runs). The PR is merged on top of it (clean merge), 3 runs.

  • Base frame p50 (ms): 84.5, 82.6, 81.8, 81.5, 77.9, 83.5 (mean 82.0); p90 84.3-93.6.
  • With this PR, p50: 84.4, 82.5, 84.4. Within the base's run-to-run range.

This scene may simply not exercise the path this PR speeds up. Summary lines and traces available on request. AI-assisted: yes.

@milescoviello

Copy link
Copy Markdown
Contributor Author

Thanks @simonbronk, and sorry for the slow reply. A clear "no measurable change" in a menu scene is useful, and it's in the description now. This PR saves the work of rebuilding the lease snapshot when the allocation registry hasn't changed, so it shows mainly in scenes with many guest-memory checks per frame. A menu may not do many.

AI-assisted: yes (Claude Code)

@github-actions github-actions Bot added the perf Performance change label Oct 18, 2026
@oneandonlydean oneandonlydean self-assigned this Oct 19, 2026
@oneandonlydean
oneandonlydean merged commit bc9b1e6 into boykopovar:main Oct 19, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* libc perf Performance change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants