Skip to content

perf(agc): reuse address-based draw builds whose inputs are unchanged - #6636

Closed
EagleBearerYS wants to merge 23 commits into
boykopovar:mainfrom
EagleBearerYS:perf/agc-reuse-address-draw-builds
Closed

EagleBearerYS wants to merge 23 commits into
boykopovar:mainfrom
EagleBearerYS:perf/agc-reuse-address-draw-builds

Conversation

@EagleBearerYS

@EagleBearerYS EagleBearerYS commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

What

A draw whose shaders reach guest memory through the BDA table (an address-based draw) builds its resources again on every draw: the lease of the registered memory, the upload, the page table and its fault buffer, the descriptor set and the image lookups. #2657 left such draws out of the resource cache because their build could not be kept: its completion checked the faults, marked the pages the shader stored to, wrote the written ranges back and released the lease, once per build.

In Resident Evil Village (PPSA01556) at the menu, most of queue 0's draws are address-based builds (APS5_PROFILE_DRAW=1, [draws] lines per 10 s):

  • on main 6ea62f9, with the fixes it needs to reach the menu: 218 of 224 draws, at 29.8 ms each (BDA 29829 (218));
  • on my local tree of main plus open PRs: 867 of 1,392 draws, at 3.8 ms each (BDA 3821 (867)).

The fix. A draw build over the cached address space (#5542) is now kept and reused by a later draw with the same content key, when everything it rests on can be proved again cheaply:

  • the space it was built over is the one a build would take now: the device's, of the current registry generation, with no import retired since;
  • its own regions are imports or read-only image mirrors with unchanged serials, or snapshot copies whose bytes the draw's snapshots repeat in the same order;
  • its images are proved as for any cached build;
  • the read-only buffers and data words the content key leaves out equal the build's. A template hit with other ones rebinds them per draw, which an address-based build can't do.

A reused draw takes the space again and refreshes its mirrors as a build does, and its imports are checked once more after the image proofs. Each draw keeps its own completion: its batch holds the space until then, and the completion checks the faults and marks the written pages and the written ranges served in place. Between draws the build pins no guest allocation, so a guest unmap never waits for the resource cache.

Buffers whose descriptor reaches past their allocation (bae2cd4). The two reuse checks of read-only buffers, MovedReadOnlyBuffers and CheckBufferAliases, took each guest buffer descriptor's raw size. A build binds less: past the device's range limit it cuts the size at the registered end, and it trims a stride overhang past the allocation. So a draw with such a descriptor was never reused, and fixing only the moved-buffer check would have made the reused draw fail the alias check. Both checks now use the size the build binds (ShaderResources::GuestBufferBytes, split out of addGuestBuffer without changing what it binds). #7044 (AlxcBTW, merged) added guestBufferBytes for the same size, for its moved compute dispatches. Since the merge of main 55f82fe that size has one body: main's, in GuestBufferBytes, which guestBufferBytes forwards to, so builds, both reuse checks and #7044's dispatch check size a buffer the same way. Templates of non-address draws use the same checks, so they get the same fix. This was found by an after-the-fact completeness review, not by a title.

Review follow-ups. The same review found three more gaps, each fixed with its test:

  • A rejected image-table element blocks reuse. The reuse proof doesn't prove an element the build rejected (an unmapped surface, a texture lookup that threw, a full table, failed sampler pairing, or a replayed rejection), so a build with one is no longer reusable. Anything that throws inside the reuse proof refuses the reuse, so the draw builds anew. Such a draw already stopped being reused after its first fault; this closes the window before it.
  • A discarded batch releases its build. Since fix(agc): discard recorder batches after submission failure #5837, a batch whose end or submit fails is discarded without running its completions, so the use a draw took on its build was never released and that build was never reused again. BeginUse now returns a token that releases the use when it is destroyed: at the end of the completion as before, or when the batch is discarded.
  • Snapshots in one order. A decode-cache hit listed the draw's snapshots in stage order, a fresh capture by address, so the first reuse after a decode miss failed SameSnapshots and built again. Both paths now sort them by address, then size.

Recorded GPU writes over a snapshot (6e5b5bd). The reuse proof compared a draw's snapshots with memory right after its in-use check. A write recorded earlier in the batch over a snapshot's range (a fill, a copy, a dispatch storing through a descriptor) that hadn't run yet left memory holding the bytes the capture saw, so the proof passed and the reused draw ran after the write on a program specialized on the old bytes. A draw that builds anew syncs through such writes in the decoder's own validation. The proof now first completes the recorded writes over each snapshot's range (CompleteSnapshotWrites), so such a capture is refused and the draw builds again. Stores through the BDA table aren't recorded as pending writes; a later use reads them in place. This was found by a later review, not by a title.

Profile counters. With APS5_PROFILE_DRAW=1, a [rescache] address-based draws: line follows the [rescache] draws: line every 10 s, in windows that had an address-based proof or an in-use refusal. It gives the proofs, the reuses, the refusals by reason (in use, not reusable, snapshots, the space and why, images or buffers, imports, a proof that threw, moved buffers), the time spent in refused proofs, the host import refusals (budget and held import, and how many came while a reused draw held its space), and the pin-contention waits. The counts are relaxed atomics, and without the profile nothing prints. The use token also carries whether its use was a reuse, so a discarded batch releases that count too. From the test:

[rescache] address-based draws: 2401 proofs, 2401 reused; refused: in use 7999, not reusable 0, snapshots 0, space 0 (state 0, replaced 0, device 0, generation 0, epoch 0, mirrors 0), images or buffers 0, imports 0, threw 0, moved buffers 0; 0.0 ms in refused proofs; host import refusals: budget 0 (0 while a reused draw held its space), held import 0 (0); pin-contention waits 0 (0.0 ms)

What it leaves out (also in the debt file):

  • The fault buffer, which holds the fault and written-page records, belongs to the build. So one draw at a time uses a build: a draw that repeats it before the previous draw's completion ran (two in one batch) builds anew and replaces it in the cache.
  • A build whose draw faulted is not reused, nor one with a rejected image-table element.
  • Two stages that read the same or adjacent words of one page give one merged snapshot on a decode miss and two on a hit, so such a draw still misses its first reuse after a decode miss.
  • Dispatches and draw recipes don't reuse address-based builds.
  • APS5_NO_ADDRESS_DRAW_REUSE=1 builds every one as before.

With the draw pipeline (#1202, on by default). A pipelined direct draw is recorded by the committer in commitDraw, and any other draw inline after lockForDraw; both hold the GPU lock through Graphics::Draw, where the cache lookup, the proofs and BeginUse run. Completions run under the hold that reaps or syncs their batch. So the build's use count and its fault verdict change only under that lock, and pipelining opens no reuse race.

The commits:

  1. refactor(agc): split the cached address space checks out of AcquireRegistered (spaceMiss, stampSpaceImports, refreshSparseMirrors, appendWritableMirrors), so a reuse makes the same proof and refresh as a build. No behaviour change.
  2. perf(agc): the reuse, with its tests.
  3. docs(agc): the debt entry.
  4. fix(agc): buffers whose descriptor reaches past their allocation (above).
  5. fix(agc): refuse address-based reuse after rejections or a throw.
  6. A merge of main 5d9dcc4, which brings fix(agc): discard recorder batches after submission failure #5837's discarding of failed batches.
  7. fix(agc): release a draw build's use when its batch is discarded.
  8. fix(agc): order a draw's snapshots the same on decode misses and hits.
  9. perf(agc): count address-based draw reuses and refusals in the draw profile.
  10. fix(agc): complete recorded GPU writes over snapshots before an address reuse proof (above).
  11. A merge of main 55f82fe, which brings perf(agc): serve compute dispatches whose read-only buffers moved from the cached template #7044; its guestBufferBytes and this PR's GuestBufferBytes now share one body (above).
  12. A merge of main 2626e5e, which brings fix(agc): harden host import construction and mutation waits #7339's host import construction guard. WaitForLeasesAndImports takes main's signature, without noexcept, and this PR's liveSnapshotCompared stays beside it.
  13. A merge of main 0d078f7, which brings fix(libSceAgcDriver): normalize reserved bit 30 in guest buffer descriptors #7089's masking of reserved bit 30 in guest buffer descriptors. GuestBufferBytes keeps this PR's signature and takes main's mask.
  14. docs(environment): the APS5_NO_ADDRESS_DRAW_REUSE row in docs/dev/ENVIRONMENT.md, which lists every switch since feat(tools): add the APS5_* and ANYPS5_* environment reference and check it #6078.
  15. A merge of main 6efb726, which brings fix(agc): drop resource-cache entries that hold released sampled textures #6492's debt entry next to this PR's; both are kept.
  16. A merge of main 6696def, which brings fix(agc): enforce MSAA guards for ordinary image cache hits #7650's multisample target guards for cached images and fix(agc): plan a heap again when its mirror's range has left the write watch #6646's mirror check. importAllocation takes main's step-down and pinnedBytes, and counts a refusal only where the import is refused. A cached space is reused only when each of its heap and sparse mirrors is still watched or can't be imported now, both when it is acquired and in ResumeSpace, and its imports are stamped once per space, as main does.
  17. test(agc): reused draw builds over a sampled image whose memory became a multisampled target, with watched imports and without the address table (below).

Cost. A reuse makes the proofs above in place of a build. Synthetic draw on lavapipe (200 draws, median of 3): with each draw waited for, the per-draw time drops 57%, 50% and 48% at 512, 6,600 and 10,800 registered ranges (74.3 to 32.2, 197.2 to 99.2, 283.5 to 148.3 us). Back to back, a draw's build is still in flight when the next draw repeats it, so nothing is reused, and the time is within 3% of main's. Resident Evil Village's pair, with and without APS5_NO_ADDRESS_DRAW_REUSE=1, follows in a comment.

Overlap. Conflicts with open PRs are in shared lines, resolved by keeping both sides:

Tested

  • New agc_driver_address_draw_reuse (tests/execution/AddressDrawReuse.cpp, lavapipe):
    • repeated draws with unchanged inputs build once and read and store the guest value through the BDA table;
    • a guest write into memory the draw reads in place is seen by the next draw without a rebuild;
    • a reused draw that reads an unmapped pointer reports the BDA fault, and a build whose draw faulted doesn't serve the next draw;
    • mapping a range, unmapping a range the draw reads and unmapping another range each force a rebuild.
  • agc_driver_graphics (tests/BdaResources.cpp) additions: SpaceRegions, DirectWrites, SameSnapshots, suspending and resuming the space.
  • agc_driver_address_draw_reuse's OversizedVertexBuffer case (bae2cd4) has a vertex buffer descriptor of 64 GiB over a 64 KiB allocation. The draw is drawn, waited for and drawn again, and both draws must store through the BDA table, with the second reusing the first one's build. On GitHub's runners (run), with ShaderResources.cpp and Draw.cpp reverted to ec17620's, it fails in all 3 runs on Linux ("a draw whose vertex buffer descriptor reaches past its allocation built its resources again"). With them, it and agc_driver_graphics pass 10 runs in a row. On Windows the draw test skips without a Vulkan device and agc_driver_graphics passes.
  • GitHub's runners, on my fork:
    • red on main 44df216 (only the new test applied): agc_driver_address_draw_reuse fails on Linux (run 38795295627); Windows skips it without a Vulkan device.
    • the head, 20 runs each of the new test and of the tests tied to the files it changes (agc_driver_address_draw_reuse, agc_driver_address_space_teardown, agc_sparse_buffer_holes, agc_compute_descriptor_layout_flags, agc_descriptor_pool_reset_failure and the three alpha-to-coverage and centroid state tests): all pass on Linux and Windows (run); Windows skips the lavapipe one. The libc and libkernel suites pass apart from libc's precise_wait on Linux (a timing test; it passed in the red run) and libkernel's guest_kqueue on Windows, which fails on main on that runner too.
    • ^agc sweeps at the head:
    • The branch was on main 44df216; it now merges main 5d9dcc4.
  • The review follow-ups, Linux (lavapipe), CI configuration, each red without its fix:
    • agc_driver_graphics's addressRejectedTableElementTests: an address-based draw whose one image-table element points at reserved pages, which the build rejects as unmapped; the pages are then committed with no registry change. Red: "an address-based draw build whose image table element was rejected as unmapped was reused after the element's surface was committed".
    • agc_driver_address_draw_reuse's ThrowingProof: after the build, the sampled image's memory becomes a 4x multisampled color target, so the reuse proof's texture lookup throws. Red, with rejections blocked but no catch: "the reuse proof of an address-based draw build whose sampled image's memory became a multisampled target threw instead of refusing the reuse".
    • agc_driver_graphics's addressFailedSubmissionTests: a build whose batch fails to submit (the mock returns VK_ERROR_OUT_OF_HOST_MEMORY) must not stay in use. Red: "an address-based draw build stayed in use after the batch that took it failed to submit".
    • agc_driver_address_draw_reuse's DecodeCacheHit: a decode miss, a second miss and a decode-cache hit of the same submitted draw build once. Red: "a submitted draw that hit the decode cache built its resources again after a decode miss of the same draw".
    • With all three: both tests pass 10 runs in a row, ^agc passes 607 of 607 (43 skipped), and each commit builds and passes its own tests.
    • On GitHub's runners (run), the reuse tests and the tests tied to the changed files pass 3 runs in a row on Linux (86 tests). On Windows they pass and the GPU tests skip; the run's one failure is libkernel's guest_kqueue, which fails on main on that runner too.
  • The profile counters (Linux, lavapipe):
    • agc_driver_address_draw_reuse's ReuseCounters: four waited draws count 4 proofs and 4 reuses; two draws back to back count one reuse and one "in use" refusal; a rewrite of the pixel program's bytes counts one "snapshots" refusal. GuestWrite, Fault and ThrowingProof check their counts too, and the reused case of addressFailedSubmissionTests checks that a reused use's hold is released when its batch fails to submit.
    • Red with the counting removed: "4 repeated draws with unchanged inputs counted 0 reuses, 0 proofs and 0 refusals"; with the token not releasing the hold: "a reused address-based draw's hold of its space stayed counted after the batch that took it failed to submit"; with a throw left uncounted: "a reuse proof that threw was not counted as one refusal that threw".
    • Both tests pass 10 runs in a row, and ^agc passes 607 of 607 (43 skipped).
    • Cost: in 12 interleaved rounds, with the profile off, the per-draw time moved by +1.6 µs for a reused draw and +9.1 µs back to back, against about 10 µs between two runs of the base.
    • On GitHub's runners (run), agc_driver_address_draw_reuse and agc_driver_graphics with the tests run alongside pass 10 runs in a row on Linux and Windows (the new tests need the counters' API, so they don't build on the previous head). The suites' only failures were libc's precise_wait on Linux, a timing test, and libkernel's guest_kqueue on Windows, which fails on main there too.
  • The snapshot-write sync (6e5b5bd), Linux (lavapipe), CI configuration: agc_driver_address_draw_reuse's PendingSnapshotWrite records a FillBuffer that rewrites the pointer the pixel shader loads and, in the same GPU-lock scope, offers the same draw with its old capture. It must read the new value, count one more snapshots refusal and no reuse, and build once more. Red without the sync: "a draw whose captured snapshot a pending GPU write changed was not refused for a changed snapshot and rebuilt (1 reuses, 0 builds)". With it the test passes 20 runs in a row, and ^agc passes 607 of 607 (43 skipped; a first sweep's one failure was agc_driver_preparation_scope_settle under the sweep's parallel load, which passes 5 of 5 alone). On GitHub's runners (run) the new case fails 2 of 2 runs on Linux at the previous head, and with the sync it and the tests tied to the file pass 10 runs in a row. On Windows (run, with test(agc): align the indirect arguments block at run time #6452's MinGW fix for an unrelated test applied for this check only), agc_driver_graphics, agc_driver_address_space_teardown and agc_sparse_buffer_holes pass 10 runs and the draw test skips without a Vulkan device. The ^agc sweeps' only failures are agc_driver_preparation_scope_settle on Linux (and, in the first run, agc_frame_dump_io's pipe flake, which main has since fixed in 4b8ccd2) and agc_driver_pm4_replay_tool (0xc0000139) on Windows, as before.
  • Multisampled targets with watched imports (fc9c541), after jotitadev's macOS run (review), where ThrowingProof failed with imports watched. The head that run tested, 28433ba, predates fix(agc): enforce MSAA guards for ordinary image cache hits #7650, which refuses that case in proveBuild (RequireNoMultisampleColorTarget) before either proof runs; the merge of 6696def brings it. Linux (lavapipe), CI's configuration:
    • New agc_driver_address_draw_reuse_watched_imports: the same test with APS5_WRITE_WATCH_IMPORTS=watch, registered on Linux and macOS builds. ThrowingProof also checks a cached build without the address table.
    • Red with fix(agc): enforce MSAA guards for ordinary image cache hits #7650's line removed: the new registration fails at the address-based case ("an address-based draw build whose sampled image's memory became a multisampled target was reused"), and agc_driver_address_draw_reuse at the non-address case ("a draw build without the address table whose sampled image's memory became a multisampled target was reused"). With it, each passes 20 runs in a row.
    • ^agc passes 792 of 793 (57 skipped). The one failure, agc_driver_sgpr_pattern_fill_keys, fails under the sweep's parallel load on main too.
    • On GitHub's runners (run), both registrations and the tests tied to the changed files (30) pass 5 runs in a row on Linux. On Windows the new registration isn't built, the draw test skips without a Vulkan device and the other 27 pass. The libc and libkernel suites pass, apart from libc's precise_wait on Linux, a timing test.
  • Merged with main 6696def (084de35), on GitHub's runners (run): the reuse tests and the tests tied to the merged files pass 5 runs in a row on Linux and Windows (Windows skips the lavapipe ones), and the libc and libkernel suites pass.
  • Merged with main 0d078f7 and 6efb726 (ff5fe23, then the row 4bacab4, then ca05f74, the head), on GitHub's runners: at 4bacab4 the ^agc tests pass 3 runs in a row on Linux (710, 0 failed), and the libc and libkernel suites pass (run); at ca05f74 they pass again on Linux (710, 0 failed), as do the suites (run). On Windows the only failures at both are agc_driver_pm4_replay_tool and agc_shader_replay_tool, as in the runs below, and the suites pass.
  • Merged with main 2626e5e (3db18a2), on GitHub's runners (run): the ^agc tests (705, 0 failed), with the reuse tests and fix(agc): harden host import construction and mutation waits #7339's agc_driver_host_import_lifetime and agc_driver_host_import_mutation_race, pass 3 runs in a row on Linux, and the libc and libkernel suites pass. On Windows the only failures are agc_driver_pm4_replay_tool and agc_shader_replay_tool, as in the run below.
  • Merged with main 55f82fe (b5d0a2a), on GitHub's runners (run): the reuse tests and the 14 tests tied to the changed files pass 3 runs in a row on Linux, as do the ^agc tests (682, 0 failed), and the libc and libkernel suites pass. On Windows the only failures are agc_driver_pm4_replay_tool and agc_shader_replay_tool, which fail the same way on that runner for an unrelated branch (run).
  • Resident Evil Village (PPSA01556), Windows 11, NVIDIA GeForce RTX 3070 Laptop GPU (driver 617.42): the [draws] lines above.
  • Onimusha: Way of the Sword (PPSA27836), run by simonbronk on Windows 11 with an NVIDIA GeForce RTX 5070 (driver 617.42) at 3840x2160 (GPU clocks weren't reported): boot to the language menu with no input, warm shader cache, the trace summary over 30 s from 60 s after the first present (APS5_TRACE_SECONDS=60:30). The base is main 84b0d2f with fix(agc): import native multisample depth whose HTILE is not ITERATE_256 #6557, fix(agc): retire an unwritten native multisample depth surface on guest access #6727 and fix(agc): replace with a stencil operation value the pixel shader exports #6762, 6 runs (3 before and 3 after this PR's); this PR is merged on top of it, 3 runs (comment):
    • Frame p50: 77.9 to 84.5 ms on the base (mean 82.0); 78.7, 85.6 and 87.6 ms with this PR (mean 84.0). Base p90: 84.3 to 93.6 ms; p90 with this PR wasn't given.

    • No gain in this scene. simonbronk reads it as no measurable change and notes that the menu may not reach the path this PR speeds up.

    • simonbronk's summary lines for his first pair, the first base run (base-r1) and the first run with this PR (pr6636-r1), from his comment. The runs and traces are his:

      {"commit":"5c521efea4cb","dirty":false,"env":{"APS5_BINDLESS_MATERIAL_SCAN":"65536","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"60:30"},"gpu":"NVIDIA GeForce RTX 5070","driver":"NVIDIA 617.42","os":"Windows 10.0.26200","resolution":"3840x2160","window":{"spec":"seconds 60:30","first_present":414,"ms":30052.503},"presents":256,"frame_ms":{"p50":84.478,"p90":93.612,"p99":972.971,"max":1952.003},"gpu_busy_ms_per_present":27.587,"worker_busy_pct":{"queue worker 0x38":1.653,"queue worker 0x20":35.070,"queue worker 0x0":91.809,"queue worker 0x28":64.589},"top_phases_ms_per_frame":{"Driver.Packet":224.620,"Driver.Packet.memory_wait":107.008,"Driver.Packet.draw":63.092,"Driver.Draw":63.071,"Driver.Packet.dispatch":52.429,"Driver.Dispatch":50.815,"Driver.Draw.draw_and_resource_release":48.789,"Graphics.Draw":48.759},"trace_dropped":false,"trace":"aps5-trace-5c521efea4cb-1894532.json.gz"}
      {"commit":"9f86b47f65bc","dirty":false,"env":{"APS5_BINDLESS_MATERIAL_SCAN":"65536","APS5_TRACE_DIR":"<path>","APS5_TRACE_SECONDS":"60:30"},"gpu":"NVIDIA GeForce RTX 5070","driver":"NVIDIA 617.42","os":"Windows 10.0.26200","resolution":"3840x2160","window":{"spec":"seconds 60:30","first_present":396,"ms":30238.620},"presents":273,"frame_ms":{"p50":78.703,"p90":87.730,"p99":951.593,"max":1017.455},"gpu_busy_ms_per_present":25.760,"worker_busy_pct":{"queue worker 0x38":1.992,"queue worker 0x20":38.049,"queue worker 0x0":97.744,"queue worker 0x28":72.995},"top_phases_ms_per_frame":{"Driver.Packet":231.188,"Driver.Packet.memory_wait":112.370,"Driver.Packet.draw":60.955,"Driver.Draw":60.933,"Driver.Packet.dispatch":55.705,"Driver.Dispatch":54.322,"Driver.Draw.draw_and_resource_release":46.104,"Graphics.Draw":46.074},"trace_dropped":false,"trace":"aps5-trace-9f86b47f65bc-1902128.json.gz"}
    • That pair's frame p50 is 84.5 ms without this PR and 78.7 ms with it, but as simonbronk says, it isn't representative: his other runs overlap, so the pair doesn't show a reliable gain. Thanks to simonbronk for the runs and the lines.

  • python3 tools/check_conventions.py --base origin/main: passed.

Checklist

…gistered

An address-based build takes the cached address space when it is the
device's, of the registry generation read before any lease, and no import
was retired since; it then stamps the space's imports as used and brings
the space's sparse, writable and heap mirrors up to date. Those steps now
live in helpers of their own (spaceMiss, stampSpaceImports,
refreshSparseMirrors, appendWritableMirrors), so a later use of an
existing build can make the same proof and the same refresh as a build
that takes the space. No behaviour change.
A draw whose stages reach guest memory through the BDA table built its
resources on every draw: the lease of the registered memory, the upload,
the page table and its fault buffer, the descriptor set and the image
lookups. boykopovar#2657 skipped the resource cache for such draws because the
build was never kept: its completion checked the faults, marked the pages
the shader stored to, wrote the written ranges back and released the
lease, once.

A draw build over the cached address space is now kept when everything it
depends on can be proved again cheaply: the space it was built over must
be the one a build would take now (the device's, of the current registry
generation, with no import retired since), its own regions must be imports
or read-only image mirrors with unchanged serials, or snapshot copies whose
bytes the draw's snapshots repeat in the same order; the images are proved
as for any cached build. The read-only buffers and data words the content
key leaves out must equal the build's: a hit with other ones rebinds them
per draw, which an address-based build cannot. A reused draw takes the
space again and refreshes its mirrors as a build does, and the imports are
checked once more after the image proofs. Each draw keeps its own
completion: its batch holds the space until then, and its completion
checks the faults, marks the written pages and the written ranges served
in place. Between draws the build pins no guest allocation, so a guest
unmap never waits for the resource cache.

The fault buffer, which holds the fault and written-page records, belongs
to the build, so one draw at a time uses a build: a draw that repeats it
before the previous draw's completion ran builds anew. A build whose draw
faulted is not reused. Dispatches and draw recipes do not reuse
address-based builds. APS5_NO_ADDRESS_DRAW_REUSE=1 builds every one as
before.
The reuse of address-based draw builds rests on choices that are not
measured on titles: one draw at a time per build (the fault buffer holds
one draw's records), the snapshot list repeated in the same order, and no
reuse for dispatches or draw recipes. Recorded as technical debt so they
can be revisited with title measurements.
@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 6804 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp L488, L625-626
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp L2497-2499
PR 7261 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp L2497, L2502-2503, L2520-2521
PR 7647 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp L493

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 1191 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 1368 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 1395 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 1896 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
PR 2434 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 4040 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 4072 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 4448 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 4449 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 5021 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 5058 docs/dev/debt/libSceAgcDriver.md
PR 5371 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 5372 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 5404 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
PR 5431 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
docs/dev/ENVIRONMENT.md
PR 5441 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 5499 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 5609 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 5925 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6044 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6186 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/ENVIRONMENT.md
PR 6187 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6188 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 6192 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6194 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6197 core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 6202 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6209 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6223 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6235 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 6280 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6300 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6412 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6481 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6501 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6539 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 6548 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6549 docs/dev/debt/libSceAgcDriver.md
PR 6628 core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 6747 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6788 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 6808 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6867 docs/dev/debt/libSceAgcDriver.md
PR 6868 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6879 docs/dev/ENVIRONMENT.md
PR 6887 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6918 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 6924 docs/dev/ENVIRONMENT.md
PR 6946 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/ENVIRONMENT.md
docs/dev/debt/libSceAgcDriver.md
PR 6947 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/ENVIRONMENT.md
docs/dev/debt/libSceAgcDriver.md
PR 6965 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6973 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6977 docs/dev/debt/libSceAgcDriver.md
PR 6987 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6989 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 6995 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 7002 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7048 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 7201 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/tests/BdaResources.cpp
PR 7256 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7275 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 7449 docs/dev/debt/libSceAgcDriver.md
PR 7484 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/ENVIRONMENT.md
PR 7505 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 7514 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/GuestBufferMemory.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 7604 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
docs/dev/debt/libSceAgcDriver.md
PR 7689 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 7800 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7803 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7883 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7893 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7910 docs/dev/debt/libSceAgcDriver.md
PR 8004 docs/dev/ENVIRONMENT.md
PR 8014 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 8077 docs/dev/ENVIRONMENT.md
PR 8105 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 8212 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
PR 8339 core/libs/prx/libSceAgcDriver/Execution/src/Driver/Draw/Draw.cpp
PR 8484 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/ENVIRONMENT.md
PR 8538 core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 8552 core/libs/prx/libSceAgcDriver/CMakeLists.txt
PR 8564 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 8592 core/libs/prx/libSceAgcDriver/CMakeLists.txt
docs/dev/debt/libSceAgcDriver.md
PR 8992 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 9020 core/libs/prx/libSceAgcDriver/CMakeLists.txt
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp
docs/dev/debt/libSceAgcDriver.md
PR 9077 docs/dev/debt/libSceAgcDriver.md

…ation

A guest buffer descriptor can describe more than its build binds. Past
the device's range limit the build cuts the size at the registered end,
or binds an empty buffer when nothing is registered there, and it trims
a stride overhang that reaches past the allocation. A reuse compared each
descriptor's raw size with what the build had bound, so such a buffer
always looked moved: an address-based draw built its resources again on
every draw, and a cached build of another draw rebound a trimmed buffer
at its raw size, past its allocation.

The moved-buffer check and the alias checks a reuse repeats now both
take the size from the build's own computation. The alias checks need
it as well: with only the moved-buffer check changed, the reused draw
failed with "shader buffer aliases the render target", which fails the
driver.

agc_driver_address_draw_reuse draws twice with a vertex buffer
descriptor of 0xffffffff records whose render target lies above its
64 KiB allocation. The second draw must reuse the first one's build;
without the fix it built again.
@johnmaia

Copy link
Copy Markdown
Contributor

In-game run of head ec17620: Epic Mickey: Rebrushed (PPSA15657, UE 4.27), short runs to the first level with APS5_PROFILE_DRAW=1. Linux, RX 7600 (RADV, Mesa 25.2.8). Tree: main 5d9dcc4 + #6236 (to get past PlayGo) + this head.

No new failures. No driver failure, Require or device loss in any of the 14 runs, with or without APS5_NO_ADDRESS_DRAW_REUSE=1.

Speed: no measurable difference in this title. It has about 240 address-based draws per 10 s in the level. Means of the last four [draws] lines of each run that reached the level:

Run Reuse Draws per 10 s BDA us per draw (count) build us per draw (count)
reuse_2 on 27,385 1,091 (239) 309 (1,586)
reuse_3 on 21,245 935 (200) 264 (4,978)
reuse_5 on 20,071 799 (283) 283 (5,018)
reuse_7 on 23,348 959 (250) 276 (5,708)
reuse_1 on 25,212 4,420 (230) 343 (6,382)
reuse_4 on 23,376 10,246 (305) 269 (5,406)
noreuse_6 off 24,175 1,026 (245) 288 (5,914)

Only one run with reuse off reached the level, so this is one comparison point. Most runs with reuse on sit at 0.8 to 1.1 ms per address-based draw, the same as that run; two runs averaged 4.4 and 10.2 ms. The PR doesn't log how many builds it reuses, so I can't tell how often reuse happens here. A [address] count of reuses would make that visible.

Crash rate at level load. This title has an intermittent crash on main while it loads the level (UE "Serial size mismatch": a 64 KiB block of file data the title just read is zeroed about 18 ms later, with no CPU write path of the driver involved; about 2 in 5 runs on main). On this tree:

Setting Crashed before the level
reuse on (default) 1 / 7
APS5_NO_ADDRESS_DRAW_REUSE=1 6 / 7

So reuse doesn't cause it; it makes it rarer. The PR isn't the fix, but this suggests the per-draw build, lease and completion of address-based draws on main are involved. I'm following that up separately.

AI-assisted: yes (Claude Code)

@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

Thanks for the runs and the tables. You're right that nothing counts reuses yet. I'm adding a count of reuses, and of refused reuses by reason, to the draw profile output, which should also show whether the two slow runs (4.4 and 10.2 ms per address-based draw) went into refused proofs. Thanks as well for the crash counts with reuse on and off. I'll post the counts from Resident Evil Village (PPSA01556) here once the commit is in.

@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

On the level-load crash: a 64 KiB block zeroed shortly after the title read it matches a known gap listed in docs/dev/debt/libSceAgcDriver.md. Pending storage-image results are stored when something next reads their memory, not when the GPU work that wrote them completes, so memory a title waits for, frees and reuses can be overwritten by a late store. If a run that crashes still crashes with APS5_STORE_AT_LABEL=1 (or APS5_STORE_AT_FLIP=1), it's something else; if it stops, it points there.

Both walks of the reuse proof skip a rejected image-table element, so
it is never proved again. Two rejections are transient (a surface that
is not mapped, a texture lookup that throws), and pages committed in a
registered reserved range leave the registry generation alone: an
address-based build could be reused with the element still rejected
after its surface became valid. Such a build is now not reusable.

The address-based proof could also throw out of Revalidate and fail
the draw, where a fresh build would reject the element or report the
error itself. Anything the proof throws now refuses the reuse.
A batch whose end or submit fails is discarded without running its
completions. The use count BeginUse took was only dropped by
CompleteUse, so the build stayed in use and was never reused again.

The count now goes with the token BeginUse returns, which the
completion owns: it drops when the completion runs or is discarded.
The mock's queue submit can now fail for the test.
A fresh capture lists a draw's snapshot regions sorted by address
(ShaderMemory::Regions). A decode-cache hit lists the stages' code
and their matched regions in stage order instead. SameSnapshots
compares the two lists entry by entry, so the first hit after a decode
miss failed the address-based reuse and built the draw again whenever
the orders differed, as they do when the pixel shader's code lies
below the vertex shader's.

The draw now sorts its snapshots by address before it hands them on.
EagleBearerYS added a commit to EagleBearerYS/AnyPS5 that referenced this pull request Oct 16, 2026
EagleBearerYS added a commit to EagleBearerYS/AnyPS5 that referenced this pull request Oct 16, 2026
…rofile

A tester of address-based draw reuse could not tell how often a draw
reused its build: the driver counted no reuse, and two runs averaged 4.4
and 10.2 ms per address-based draw where others took about 1 ms. A
refused reuse pays for its proof and then for the build, so the refusals
and their reasons are what tell such runs apart.

The resource cache now counts, for address-based draw builds, the proofs
Revalidate runs, the reuses, and the refusals by reason: in use (the
build's previous draw has not completed; refused before any proof), not
reusable (its draw faulted), snapshots differ, the space did not resume
(by its state, a replaced space, the device, the registry generation, the
import epoch, or a sparse mirror that could not be refreshed), the image
or buffer proof failed, the imports changed, the proof threw, and
read-only buffers that moved after the proof passed. The time of refused
proofs is summed under APS5_PROFILE_DRAW. A reused draw's hold of its
space is counted until its use ends, and host import refusals, over the
budget or of an import that would replace a held one, are counted with
how many came while such a hold was live.

The reused flag travels in the use token BeginUse returns, so the hold
is released with the use count when the token goes, also when a batch
is discarded without running its completions. A proof that throws is
counted on its own, as "threw": the throw can come from resuming the
space, from the proof or from the imports check, and counting it under
any of those would hide an error among ordinary refusals.

APS5_PROFILE_DRAW prints them every 10 s right after the [rescache] draws
line, beside the pin-contention waits of the same window (the
[address-sync] leases line prints those cumulatively on its own timer):
"[rescache] address-based draws: P proofs, R reused; refused: in use N,
..., threw N, moved buffers N; ...; host import refusals: budget B (H
while a reused draw held its space), held import ...; pin-contention
waits W (T ms)".

agc_driver_address_draw_reuse reads the counters: repeated unchanged
draws count as reuses, a draw repeated before the previous one completed
counts one refusal in use, a draw after the guest rewrote its pixel
program counts one refusal for a changed snapshot, a draw after a guest
write into memory it reads in place counts a reuse, the draw after a
fault counts one refusal of a build that is no longer reusable, and a
proof that throws counts one refusal that threw. The BDA resource tests
check that a reused draw's hold is counted while its batch is pending
and released when the batch fails to submit.
@johnmaia

johnmaia commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

@EagleBearerYS thanks, APS5_STORE_AT_FLIP=1 stops it. Epic Mickey: Rebrushed (PPSA15657), short runs to the first level, Linux, RX 7600 (RADV, Mesa 25.2.8). Tree: main 5d9dcc4 + #6236 (e088fd8), no other changes.

Runs Failed before reaching the level
APS5_STORE_AT_FLIP=1 16 (8, then 8 alternated with the runs below) 0
Unset, alternated 8 3

The three failures without it:

  • ObjectSerializationError on SM_MachineSet_01b, the same mesh as before (the "Serial size mismatch" crash).
  • LowLevelFatalError [Line: 1609] right after the title opens /savedata0/ue4savegame.dpx.sav.
  • A SIGSEGV in the title's own code.

Earlier runs of the same tree without the variable failed 2 of 5. With the flag, all 16 runs reached the level and played to the end of the run. APS5_STORE_AT_LABEL=1 couldn't be compared: with it the level load doesn't finish ("GameThread timed out waiting for RenderThread after 120.00 secs", 2 of 2).

So it points at the late store in docs/dev/debt/libSceAgcDriver.md (#1970). One detail that may matter for a fix: in an earlier trace, the crashing pair's APR read landed on a 64 KiB block that had a storage image write-back about 50 ms before. When that read started, its destination was in neither the published pending-write snapshot nor Recorder::PendingWriteOverlaps, so a flush on the read path that relies on them would not catch it.

AI-assisted: yes (Claude Code)

@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@johnmaia thank you for the alternated runs. 0 of 16 with APS5_STORE_AT_FLIP=1 against 3 of 8 without, on a tree without this PR, puts the Epic Mickey: Rebrushed (PPSA15657) level-load crash on main's late storage-image store (the debt entry in docs/dev/debt/libSceAgcDriver.md), not on this PR. Your detail that the APR read's destination was in neither the published pending-write snapshot nor Recorder::PendingWriteOverlaps matters for a fix on the read path, and I've passed it on with your numbers. The reuse counter you asked for is in (the [rescache] address-based draws: line, commit 9); the Resident Evil Village (PPSA01556) counts follow when I have a slot on the PC.

@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@johnmaia Thanks for the A/B. It pinned this down. A storage image's write-back is recorded on the GPU and can land after a later APR read into the same memory, which zeroes the file data. Host writes into guest memory also didn't wait for GPU writes pending there. A fix is on EagleBearerYS:fix/agc-host-write-pending-storage (99af947, on main 66e290f). Could you try it on Epic Mickey: Rebrushed the same way as before: runs without APS5_STORE_AT_FLIP, alternated? If this was their cause, the ObjectSerializationError on SM_MachineSet_01b, the LowLevelFatalError [Line: 1609] after the save opens, and the SIGSEGV should no longer appear. With APS5_TRACE_APR=1 APS5_TRACE_FLUSH=1, an APR read over pending results is now followed at once by a [flush] image … line, before the read's bytes land. If a run still fails, the [apr] read line nearest the failure would help.

@parl0v

parl0v commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

Data point: 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 c842491 vs main + this PR, interleaved with three 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.60 / 6.60 105 / 105 ms 46 / 47 ms
+ this PR (89ab0ea) 6.67 / 6.60 (+0.5 %) 104 / 105 ms 47 / 47 ms

Neutral on this scene (within the row spread); no regression. 0 fatal.

AI-assisted: yes (Claude Code)

…ss reuse proof

The reuse proof of an address-based draw build compares the draw's
snapshots with guest memory for live regions and with the build's
copies for the others. A write that earlier work in the batch recorded
over a snapshot's range (a fill, a copy, a dispatch storing through a
descriptor) may still be pending when the proof runs: memory then still
holds the bytes the capture saw, the proof passes, and the draw runs
after the write on a program the decoder specialized on the old bytes.

Revalidate now syncs the recorder through every snapshot range that a
recorded write overlaps before it compares the snapshots, so the
comparison sees the memory the earlier work leaves, and a capture made
before that write is refused as a changed snapshot. A range with no
pending write costs one overlap test. Stores through the BDA table are
not recorded as pending writes; a later use reads them in place.

agc_driver_address_draw_reuse gains the case: a draw whose snapshots
include the guest pointer its pixel shader reads, reused once, then a
FillBuffer recorded over that pointer and the same draw with the old
capture in the same batch. The draw is refused for its snapshots,
builds again and reads the new pointer; without the sync it was reused
("1 reuses, 0 builds").
@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@adriadam10, thanks for the review. All three points:

Conflicts with main. 084de35 merges main 6696def. importAllocation takes main's step-down and pinnedBytes, so the replaced size counts pinned bytes, and a refusal is counted only where the import is refused, not where it steps down to a smaller size. A cached space is reused only when main's check of its heap and sparse mirrors passes, both when the space is acquired and in ResumeSpace, and its imports are stamped once per space, as main does. On GitHub's runners (run) the reuse tests and the tests tied to the merged files pass 5 runs in a row on Linux and Windows (Windows skips the lavapipe ones).

The macOS failure. Your and @jotitadev's reading is right. At 28433ba, fastRevalidate never looked at the multisample target guards, and MultisampleTargetView writes no guest memory, so with watched imports nothing the fast proof compares had changed. #7650 (merged) now refuses that case in proveBuild, through RequireNoMultisampleColorTarget, before either proof runs, and the merge brings it into this branch. It checks a build's ordinary sampled and storage ranges; image tables stay exempt, as #7650's debt entry says. With that line removed, the case fails on Linux too when imports stay watched (APS5_WRITE_WATCH_IMPORTS=watch).

fc9c541 keeps it covered on CI:

  • agc_driver_address_draw_reuse_watched_imports registers the same test again with APS5_WRITE_WATCH_IMPORTS=watch, on Linux and macOS builds.
  • ThrowingProof also checks a cached draw build without the address table, the non-address case you asked about. It had the same gap before fix(agc): enforce MSAA guards for ordinary image cache hits #7650, in both import modes, and the same guard refuses it now.

With #7650's line removed, the new registration fails at the address-based case ("an address-based draw build whose sampled image's memory became a multisampled target was reused") and the existing one at the non-address case ("a draw build without the address table whose sampled image's memory became a multisampled target was reused"). With it, each passes 20 runs in a row on Linux, and on GitHub's runners (run) both pass 5 runs in a row with the tests tied to the changed files. The ^agc tests pass 792 of 793 (57 skipped). The one failure, agc_driver_sgpr_pattern_fill_keys, fails under the sweep's parallel load on main too.

@jotitadev, could you run agc_driver_address_draw_reuse again on macOS with fc9c541 merged into your tree? With the default (watched imports) it should pass now.

The perf pair. I'll post a Resident Evil Village (PPSA01556) pair from a scene with address-based draws: main against main plus this PR, on the same machine, with both aps5-summary.jsonl lines, the traces and the output of tools/trace_digest.py. It's queued behind getting the title to boot on current main, and the post will name anything either tree needs beyond main and this PR.

main brings boykopovar#8224, which cuts a read-only strided buffer that runs less
than one stride into the render target at the target, in addGuestBuffer
and in CheckBufferAliases. CheckBufferAliases keeps this branch's
GuestBufferBytes size and applies main's cut to it, as addGuestBuffer
does after guestBufferBytes.
…uild does

Since boykopovar#8224, a read-only strided buffer that runs less than one stride
into the render target is bound only up to the target, and the cached
build keeps that size. MovedReadOnlyBuffers compared it with the size
without that cut, so a hit with the same words took the buffer as moved
and rebound all of it, over the render target, and an address-based
build holding such a buffer was never reused.

GuestBufferBytes takes the target and applies the cut. The build,
CheckBufferAliases and MovedReadOnlyBuffers pass the draw's target for
read-only buffers, so all three size a buffer the same way.

agc_driver_graphics checks a cached build whose read-only stride-60,
4-record buffer runs 8 bytes into the target: the same words list
nothing moved, a buffer moved to run 4 bytes in is listed at 236 bytes,
and one moved to run a whole stride in keeps its 240.
The entry described each proof the reuse makes. It now names where they
are (ShaderResources::Revalidate) and keeps what stays open: one draw at
a time per build, no reuse for dispatches and draw recipes, and no
measurement in a title. It goes from 1,537 to 871 characters.
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Oct 21, 2026
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 21, 2026
@EagleBearerYS

Copy link
Copy Markdown
Contributor Author

@adriadam10, the Resident Evil Village (PPSA01556) runs show this PR doesn't pay off there, so I'm closing it rather than make you wait for the pair I promised.

What the runs showed. Both ran at the title's menu, with this PR at fc9c541. They aren't on main plus this PR only, as you asked: main 1ff5c8a aborts in Resident Evil Village's intro without #8926's fix. So both trees are main 1ff5c8a + #8926's fix (63d5414) + this PR. Both used APS5_LAZY_SHADER_PREPARE=1, APS5_NO_GPL=1 and APS5_HOST_IMPORT_MIB=6400 on a warm cache.

  • With the reuse counters, 6 minutes: 17,613 address-based proofs, and none reused. Each was refused because a snapshot had changed.
  • With one more commit on top that names each refusal's first failing check: of 2,295 refused proofs, 2,185 failed on a read's address and 108 on a count. None failed on size, bytes or a lookup. Its logging slowed the game to a crawl, so this run covers fewer frames of the menu.

Why. The proof compares the addresses a build read through its address space. Most of those reads are per-draw constants whose address changes from one draw to the next: mesh-stage constants of 0x50 and 0x70 bytes, and 4- and 8-byte pixel-stage reads. Ten draw keys read 9 or more distinct addresses each, and they account for 55% of the address reads the second run logged. Keeping a few builds per key would match about a quarter of those reads at best.

A gain here would need a proof over what the build derives from those reads, not over their addresses. That's a bigger change than this PR.

Thanks to you for the review, to @jotitadev for the macOS run, to @MarcZX10 for the review comments, to @simonbronk for the Onimusha: Way of the Sword (PPSA27836) runs, and to @johnmaia for the Epic Mickey: Rebrushed (PPSA15657) runs.

teepean added a commit to teepean/AnyPS5 that referenced this pull request Oct 21, 2026
… (local test)

Address-based builds take boykopovar#6636's Revalidate path with the full proof;
the proof memo stays for the other builds (memoRevalidate). The repeated
template of the previous draw goes through the same in-use check and
snapshot proof as a cache hit, and the [address] line keeps its keeps and
stamps laps around boykopovar#6636's stampSpaceImports.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
teepean added a commit to teepean/AnyPS5 that referenced this pull request Oct 21, 2026
teepean added a commit to teepean/AnyPS5 that referenced this pull request Oct 21, 2026
…the port (local test)"

This reverts commit 58ace42, reversing
changes made to 289f030.
Celegans12 added a commit to Celegans12/AnyPS5-Ghost-of-Yotei that referenced this pull request Oct 23, 2026
… (APS5_NO_BDA_TEMPLATES)

Stage 1 of research/structural/committer-key.md. An address-table draw build
whose stages never store through BDA (SPIR-V check), with no written buffer,
written image or GPU copy, is cached as a template:
- later draws use it while earlier uses are still in flight (it is never
  written after its build: each use takes its own descriptor set for changed
  data words or moved read-only buffers, as non-address templates do);
- a hit proves the address space (resume), the textures and imports as
  boykopovar#6636's reuse does, and its captures with SnapshotsServed: every capture
  lies in live registered memory and equals it, or equals one of the build's
  own snapshot copies, and every snapshot copy is captured again. Live memory
  is read in place through the page table, so it may change between uses.
Builds that store through BDA are cached only as before (boykopovar#6636, off in the
README env). APS5_NO_BDA_TEMPLATES=1 (a RuntimeSwitch, APS5_AB-flippable)
restores today's behaviour. [bda-template] counts hits while a use is in
flight and per-use sets. No SourceVersion change.

Tests (agc_driver_graphics, BDA resource tests): two uses in flight; a store
to live memory between uses is taken, a stale capture, a changed snapshot copy,
a missing or outside capture refuse; other data words get their own set and
never reach the template; a storing build keeps one use at a time; a rejected
image-table element refuses a read-only template too; the switch restores
today's refusals.

AI-assisted: yes
alex73630 pushed a commit to alex73630/AnyPS5 that referenced this pull request Oct 24, 2026
Integrate PR boykopovar#6636 at ca05f74.
Revalidate snapshots, address spaces, imports and image resources before
reuse, and retain each draw's resources through its completion.

Keep the existing DeviceReads output overload and exclude packed blend
targets from reuse. Apply guest allocation bounds to alias checks for
both address-based and ordinary template hits.

Refs boykopovar#6636
alex73630 pushed a commit to alex73630/AnyPS5 that referenced this pull request Oct 24, 2026
Integrate PR boykopovar#6636 at ca05f74.
Revalidate snapshots, address spaces, imports and image resources before
reuse, and retain each draw's resources through its completion.

Keep the existing DeviceReads output overload and exclude packed blend
targets from reuse. Apply guest allocation bounds to alias checks for
both address-based and ordinary template hits.

Refs boykopovar#6636
alex73630 pushed a commit to alex73630/AnyPS5 that referenced this pull request Oct 24, 2026
Integrate PR boykopovar#6636 at ca05f74.
Revalidate snapshots, address spaces, imports and image resources before
reuse, and retain each draw's resources through its completion.

Keep the existing DeviceReads output overload and exclude packed blend
targets from reuse. Apply guest allocation bounds to alias checks for
both address-based and ordinary template hits.

Refs boykopovar#6636
EagleBearerYS added a commit to EagleBearerYS/AnyPS5 that referenced this pull request Oct 31, 2026
A tester of address-based draw reuse cannot tell how often a draw was
served by its cached build, or why it was not: a refused reuse pays for
its proof and then for the build.

Every address-based draw that could take part in the resource cache
(recordable, its stages compiled, the reuse on) now counts exactly one
outcome:
- reused: a hit served by the cached build;
- built: a new build that the cache keeps for later draws;
- not read-only: a stage stores through addresses (NeverReusable);
- not space-only: the build cannot be kept, as some of its bindings lie
  outside the cached address space (or noteReusable keeps it out for
  another reason: copied writes, multisample sources, the loop guard);
- snapshot outside the space (copied): a snapshot of the draw lies
  outside the space, or in a region that is not read live;
- partial overlap: a snapshot straddles the end of a region;
- snapshot changed: a snapshot no longer equals guest memory, or lies
  under a unit shadow;
- moved GuestBuffers element and patched data word: the moved-buffer
  proof refused the hit, for a data word under a misalignment patch or
  for anything else;
- space or epoch change: the space could not be retaken (replaced, or
  its device, registry generation or import epoch moved);
- in use: the cached build still holds its space for another draw, which
  the hit no longer shares (nor removes from the cache);
- revalidation: Revalidate refused the hit.
SnapshotsServedInPlace returns which of its checks refused, and
MovedReadOnlyBuffers says when a patched data word did.

APS5_PROFILE_DRAW prints them every 10 s after the [rescache] draws line,
as boykopovar#6636's 89ab0ea did for its reuse: "[rescache] address-based draws
(10 s): reused N, built N; refused: not read-only N, ...". The counters
are cumulative (AddressDrawReuseCounters) and the line shows a window.

agc_driver_address_draw_space_reuse's counters case draws through each
refusal it can reach and requires each to count under its own reason
(two reuses, a space change after an allocation is mapped, a snapshot
outside the space and the uncached builds after it, a snapshot across
the end of an allocation, a snapshot that differs from memory, a storing
pixel shader), and the outcomes of all its draws to add up to the number
of draws.
EagleBearerYS added a commit to EagleBearerYS/AnyPS5 that referenced this pull request Oct 31, 2026
…reuse proof

The reuse proof of a space-reuse draw build compares the draw's
snapshots with guest memory inside the cached space. A write that
earlier work in the batch recorded over a snapshot's range (a fill, a
copy, a dispatch storing through a descriptor) may still be pending when
the proof runs: memory then still holds the bytes the capture saw, the
proof passes, and the draw runs after the write on a build proven
against the old bytes.

As boykopovar#6636's 6e5b5bd did for its proof, the hit now syncs the recorder
through every snapshot range a recorded write overlaps before
SnapshotsServedInPlace compares them, so the comparison sees the memory
the earlier work leaves and a capture made before the write is refused
as a changed snapshot. A range with no pending write costs one overlap
test.

agc_driver_address_draw_space_reuse gains the case and the moved-ring
cases:
- pending-snapshot-write: a draw whose snapshots include the pointer
  its pixel shader loads, reused once, then a FillBuffer recorded over
  that pointer and the same draw with the old capture in the batch. It is
  refused for a changed snapshot and builds again; on the previous commit
  it was reused ("reused 1 and 0 builds").
- ring-same-constants: each draw writes the same constants into the next
  slot of an eight-slot ring in an imported allocation, passes the slot's
  address in user data and its bytes as a snapshot; all fifteen draws
  after the first are reused.
- ring-own-constants: as above with other constants per draw; each draw
  renders its own and all are reused.
- ring-old-slot-write: a CPU write to the previous slot after each draw
  is recorded leaves the reused draw's result alone.
- ring-refusals: a moved snapshot outside the space is refused under its
  reason; a moved read-only vertex buffer inside the space is bound anew
  and reused (boykopovar#9508's moved-buffer path), and one that a pending fill
  overlaps is refused as a moved GuestBuffers element.
The ring cases pass from the default-path commit on (with
APS5_NO_ADDRESS_DRAW_REUSE=1 they fail, 15 builds for 15 draws). No case
reaches the patched-data-word refusal: the stage this test builds has no
data word under a misalignment patch. docs/dev/debt/libSceAgcDriver.md
records the reuse and what is not measured.
oneandonlydean pushed a commit that referenced this pull request Nov 4, 2026
A tester of address-based draw reuse cannot tell how often a draw was
served by its cached build, or why it was not: a refused reuse pays for
its proof and then for the build.

Every address-based draw that could take part in the resource cache
(recordable, its stages compiled, the reuse on) now counts exactly one
outcome:
- reused: a hit served by the cached build;
- built: a new build that the cache keeps for later draws;
- not read-only: a stage stores through addresses (NeverReusable);
- not space-only: the build cannot be kept, as some of its bindings lie
  outside the cached address space (or noteReusable keeps it out for
  another reason: copied writes, multisample sources, the loop guard);
- snapshot outside the space (copied): a snapshot of the draw lies
  outside the space, or in a region that is not read live;
- partial overlap: a snapshot straddles the end of a region;
- snapshot changed: a snapshot no longer equals guest memory, or lies
  under a unit shadow;
- moved GuestBuffers element and patched data word: the moved-buffer
  proof refused the hit, for a data word under a misalignment patch or
  for anything else;
- space or epoch change: the space could not be retaken (replaced, or
  its device, registry generation or import epoch moved);
- in use: the cached build still holds its space for another draw, which
  the hit no longer shares (nor removes from the cache);
- revalidation: Revalidate refused the hit.
SnapshotsServedInPlace returns which of its checks refused, and
MovedReadOnlyBuffers says when a patched data word did.

APS5_PROFILE_DRAW prints them every 10 s after the [rescache] draws line,
as #6636's 89ab0ea did for its reuse: "[rescache] address-based draws
(10 s): reused N, built N; refused: not read-only N, ...". The counters
are cumulative (AddressDrawReuseCounters) and the line shows a window.

agc_driver_address_draw_space_reuse's counters case draws through each
refusal it can reach and requires each to count under its own reason
(two reuses, a space change after an allocation is mapped, a snapshot
outside the space and the uncached builds after it, a snapshot across
the end of an allocation, a snapshot that differs from memory, a storing
pixel shader), and the outcomes of all its draws to add up to the number
of draws.
oneandonlydean pushed a commit that referenced this pull request Nov 4, 2026
…reuse proof

The reuse proof of a space-reuse draw build compares the draw's
snapshots with guest memory inside the cached space. A write that
earlier work in the batch recorded over a snapshot's range (a fill, a
copy, a dispatch storing through a descriptor) may still be pending when
the proof runs: memory then still holds the bytes the capture saw, the
proof passes, and the draw runs after the write on a build proven
against the old bytes.

As #6636's 6e5b5bd did for its proof, the hit now syncs the recorder
through every snapshot range a recorded write overlaps before
SnapshotsServedInPlace compares them, so the comparison sees the memory
the earlier work leaves and a capture made before the write is refused
as a changed snapshot. A range with no pending write costs one overlap
test.

agc_driver_address_draw_space_reuse gains the case and the moved-ring
cases:
- pending-snapshot-write: a draw whose snapshots include the pointer
  its pixel shader loads, reused once, then a FillBuffer recorded over
  that pointer and the same draw with the old capture in the batch. It is
  refused for a changed snapshot and builds again; on the previous commit
  it was reused ("reused 1 and 0 builds").
- ring-same-constants: each draw writes the same constants into the next
  slot of an eight-slot ring in an imported allocation, passes the slot's
  address in user data and its bytes as a snapshot; all fifteen draws
  after the first are reused.
- ring-own-constants: as above with other constants per draw; each draw
  renders its own and all are reused.
- ring-old-slot-write: a CPU write to the previous slot after each draw
  is recorded leaves the reused draw's result alone.
- ring-refusals: a moved snapshot outside the space is refused under its
  reason; a moved read-only vertex buffer inside the space is bound anew
  and reused (#9508's moved-buffer path), and one that a pending fill
  overlaps is refused as a moved GuestBuffers element.
The ring cases pass from the default-path commit on (with
APS5_NO_ADDRESS_DRAW_REUSE=1 they fail, 15 builds for 15 draws). No case
reaches the patched-data-word refusal: the stage this test builds has no
data word under a misalignment patch. docs/dev/debt/libSceAgcDriver.md
records the reuse and what is not measured.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* build docs perf Performance change unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants