Skip to content

fix(agc): count copied guest buffers and draw snapshots in the batch byte budget - #2283

Merged
adriadam10 merged 4 commits into
boykopovar:mainfrom
MathiasDevelopes:fix/agc-count-copied-buffers-in-batch-budget
Oct 10, 2026
Merged

adriadam10 merged 4 commits into
boykopovar:mainfrom
MathiasDevelopes:fix/agc-count-copied-buffers-in-batch-budget

Conversation

@MathiasDevelopes

Copy link
Copy Markdown
Contributor

What

The [recorder](https://github.com/boykopovar/AnyPS5/blob/main/core/libs/prx/libSceAgcDriver/Graphics/src/Recorder.cpp) bounds the bytes a batch keeps alive: BoundKeptBytes submits an open batch at KeptBytesBudget (512 MiB) and waits for the oldest in-flight batches past twice that. Only bytes passed to Recorder::Keep(object, bytes) count, and a recorded dispatch or draw kept its ShaderResources (and a draw its DrawBindings snapshots) with a size of 0. The host-visible copies of guest buffers that these objects own were never counted.

Planet Coaster (PPSA01736) binds a 124 MiB writable guest buffer in nearly every draw after its intro. The process went from 8.7 GB to 20.7 GB private memory within three seconds. 98 buffers of 124 MiB were created and 12 released, until:

[videoout] presentation failed: AGC graphics: vkAllocateMemory buffer: Vulkan result -2

GuestBufferMemory::CopiedBytes() now reports the bytes of its regions that are served by a copy, ShaderResources::CopiedBytes() forwards it, and the dispatch keep, the draw keep and the draw snapshots pass their bytes to Recorder::Keep. The existing bound then applies to them; nothing else about the bound changes.

Related open PRs: #1364 (and #1366, which contains it) counts retained draw upload pages in the same budget; it does not touch these keeps. #1997 passes sizes for its own image merge buffers. The call sites above are untouched by both.

This is a bound, not a fix of the copies themselves: the title still re-copies that buffer for each draw (see the related region fix in a separate PR).

Tested

Windows 11, MinGW-w64 15.2.0, RTX 3060.

  • agc_driver_bda_resources: CopiedBytes() is 0 before an upload and, after it, either 0 (served by an import) or the one copied region's size.
  • Full ctest suite: 507/507 passed. tools/check_conventions.py --base origin/main passes.
  • PPSA01736: with the change, the title runs the 200 s capture without the allocation failure; before, it aborted about 60 s in. It needs other fixes to reach that point on main.

Checklist

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

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Copilot AI balanced review requested due to automatic review settings October 9, 2026 14:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added the agc libSceAgc* label Oct 9, 2026
@MarcZX10

MarcZX10 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

I reviewed the complete six-file diff. The byte accounting follows the retained GuestBufferMemory regions and draw snapshots, and the new test checks the no-copy/import path as well as a copied upload. The Windows full-suite report and Linux/Windows/macOS relinker jobs are green; the upstream full Linux and Windows Build jobs were skipped. Could an authorized collaborator run the required full Build workflow on this PR head before merge?

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the bound makes sense. Build and the libSceAgcDriver ctest scope are green here (the new assertions fail to compile without the change). Points to address:

  • tests/BdaResources.cpp:431: CopiedBytes() == 0 || CopiedBytes() == sizeof(guest) accepts both outcomes. Pin the expected value for the case the test sets up (copy vs. import), or build one case for each.
  • The test only covers the accessor. Reverting any of the three Keep calls (VulkanDevice.cpp:3318, Draw.cpp:1176, ShaderResources.cpp:3159) leaves it green. Please add a test showing that the kept bytes reach the recorder's budget (see the existing agc_recorder_kept_bytes_tests fixture).
  • Please confirm a ShaderResources retained by several draws/dispatches is not counted once per keep, otherwise the batch is submitted earlier than the budget says.

@MathiasDevelopes

Copy link
Copy Markdown
Contributor Author

@adriadam10

Thanks for the review. Pushed 26d5926 (and 55b09c0, which drops comments the conventions check rejects):

BdaResources: the check now pins CopiedBytes() == sizeof(guest). guest is stack memory no allocation registers, so no host import serves it and the two views merge into one copied region (the WriteBack publishing the aliased write below already depends on that copy).

New device test agc_driver_recorded_kept_bytes (tests/execution/RecordedKeptBytes.cpp). It reads the open batch's kept bytes (new Recorder::OpenKeptBytes) after a recorded dispatch writing a copied 64 KiB buffer, after a recorded draw reading a copied 64 KiB buffer (into a resident target, so the draw is recorded rather than synchronous), and after two draws reading a registered, write-watched 64 KiB buffer in place, which take a draw snapshot.

I checked it against each revert separately: removing the dispatch keep, the draw keep or the snapshot keep each fails it, and so does counting a reused snapshot twice. It takes about 0.2 s on lavapipe.

The mock fixture gets SharedOwnerCountsOncePerBatch for the counting rule itself.
It was counted once per keep, so you were right. A cached ShaderResources (the dispatch resource cache, the draw SharedResourceCache, recipe templates) is one object shared by every draw or dispatch that hits it. Reusable() accepts device-local staged regions, which have a buffer, so CopiedBytes() can be nonzero for a shared object. Draw snapshots were affected too: ReusableDrawSnapshot hands the next draw the same buffer. Recorder::KeepBytes(owner, bytes) now counts an owner once per batch through a per-batch set, and all three sites use it. An object used by two batches that are both in flight still counts once in each. That errs toward waiting early, never late.

Tested on Linux with lavapipe, with the PR merged into current main: the libSceAgcDriver ctest scope gives 320/320 passing (27 skipped because llvmpipe has subgroup size 8).

@Mouhand-Kaddo

Copy link
Copy Markdown
Contributor

Additional platform validation on standalone PR head 6e3e5e48edc11c15f72b57b266876f5832382f75:

Linux: passed — Arch Linux in WSL2 (kernel 6.6.87.2), GCC 16.2.1 20260810; CMake 4.3.1 / Ninja.

Release / BUILD_TESTING=ON: built the selected test targets; 1 CTest cases, 0 failures, 0 CTest skips.

Tests: agc_driver_graphics.

Reproduce after building the test targets: ctest --test-dir <build> --verbose --no-tests=error --timeout 30 -R "^agc_driver_(bda_resources|graphics)$".

Vulkan: Mesa lavapipe 26.2.4 software Vulkan; no native Linux GPU/retail-title validation.

Internal limitation: write-watched guest-heap mirror subcases returned early because WSL userfaultfd async write protection is unavailable. The other graphics assertions ran; zero CTest skips does not include this internal early return.

@github-actions

github-actions Bot commented Oct 10, 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
#2864 core/libs/prx/libSceAgcDriver/Graphics/include/Recorder.hpp L87

@orishamir

Copy link
Copy Markdown
Contributor

55b09c0f merges cleanly onto current main (4a747c48) but the new test does not build there: tests/execution/RecordedKeptBytes.cpp:97 calls GuestAllocations::Mutation().Add(block, bytes, true, true), and since c11a9cb5 ("address-based builds map only memory mapped for the GPU") Add takes a fifth gpu argument, so agc_driver_recorded_kept_bytes_tests fails to compile (no matching function for call to ... Add(uint8_t*&, std::size_t&, bool, bool)). #374 and #1619 made the same one-argument adaptation.

With that call changed to Add(block, bytes, true, true, true) locally, the three review points check out on llvmpipe: agc_driver_recorded_kept_bytes, agc_recorder_kept_bytes, agc_driver_bda_device and agc_driver_bda_span_reads pass, and the new test fails under each keep-site revert to main separately: VulkanDevice.cpp ("a dispatch writing a copied 65536 bytes buffer left 0 bytes in its batch's kept bytes"), Draw.cpp ("a draw reading a copied 65536 bytes buffer left 0 bytes ..."), ShaderResources.cpp ("a draw snapshotting a 65536 bytes buffer left 0 bytes ..."). KeepBytes counts an owner once per batch through keptOwners.

AI-assisted: yes (Claude Code Fable 5.1 xhigh)

@Scramblrrrr

Copy link
Copy Markdown
Contributor

Windows cross-test of #2283 head 55b09c0, applied to the 007 integration stack at 79dd488f. I included the fifth gpu=true argument in GuestAllocations::Mutation().Add, as identified by orishamir. Public integration head f4d4d04: https://github.com/Scramblrrrr/AnyPS5/tree/codex/first-light-2283-cross-test . Original authorship preserved.

Release MinGW GCC 15.2, Windows 11, RTX 5070 Ti, SPIRV-Tools OFF, Vulkan required: 4/4 selected CTest checks pass (agc_driver_recorded_kept_bytes, agc_recorder_kept_bytes, agc_driver_graphics, agc_host_import_unmap). The execution regression exercises copied dispatch and draw keeps. Its reusable draw-snapshot subcase explicitly returns early here because Windows direct imports use comparisons; this is not a claim that all internal snapshot subcases ran. The owner-deduplication mock regression does run.

This is an integration-stack result, not a standalone build of #2283 on main. A fresh 007 run is next; no game-result claim for this head yet. My standalone prototype 01754bf0 on main 262c0316 also built and passed its 3 selected tests, but I am keeping it as a diagnostic comparison, not submitting a competing PR.

AI-assisted (Codex).

@SleglJan

Copy link
Copy Markdown
Contributor

Linux cross-test of 55b09c0. Setup: CachyOS (kernel 7.2.9, GCC 16.2.1), RTX 3080 (NVIDIA 615.71.09), Release.

Merged onto main 262c031, SPIRV-Tools on
It merges cleanly. I applied orishamir's adaptation of tests/execution/RecordedKeptBytes.cpp:97 (Add(block, bytes, true, true, true)). As orishamir found, the test doesn't compile on main without it. Full suite: of 712 tests, 707 pass, 4 are skipped and 1 fails.

007 First Light (PPSA11386)
On main, 007 stops earlier (#2523 row 4), so this was tested on the 007 stack from #2523: Scramblrrrr's 79dd488 with local commits, SPIRV-Tools off, default asynchronous draws, one 240 s run each.

An earlier trace on the 75e319f stack from #2523, which has the same GuestBufferMemory code and the same 0-byte draw keep, showed the Planet Coaster pattern at a larger size. Draw builds copied one or both of two sparse 512 MiB regions into new host-visible buffers, about 1 GiB per build, and the draws kept those copies with 0 bytes. On 79dd488 without this PR, available memory falls from 33.8 to 6.1 GiB within 7 s, and my watchdog stops the title.

With the three commits cherry-picked:

Combined with #2992 there's no jump either: the lowest available memory is 27.2 GiB. One thing to know about that combination: CopiedBytes() counts region.end - region.begin, but #2992 cuts a sparse region's copy at bufferEnd. With both applied, a cut region is counted at its full size, so the batch is submitted earlier than its real kept bytes require. The full table is in #2523.

AI-assisted: yes (Claude Code)

@Scramblrrrr

Copy link
Copy Markdown
Contributor

Follow-up to #2283: I reproduced a buffer-only enforcement gap and pushed 0280ac3 on codex/recorder-buffer-only-budget-followup (parent integration f4d4d041): Scramblrrrr@0280ac32 . This is a 24-line follow-up, not a competing copied-byte implementation.

The existing bound checks are in sampled/storage texture lookups. A dispatch with no images can keep adding counted buffers without reaching either check. The new Vulkan regression records one buffer-only dispatch, seeds that open batch at the existing byte budget using a kept test holder, then dispatches to a distinct buffer. It requires that the preceding batch has submitted. The helper holds GuestMemory::GpuMutex, matching the driver's calling convention; no giant allocation or title data is needed.

Negative control: with #2283's byte accounting but no entry checks, the regression fails: a buffer-only dispatch did not submit the preceding batch at its kept-byte budget. Positive: budget checks at ordinary/recipe dispatch and draw entry points, before recording new work, make it pass. The four selected Windows tests pass again with Vulkan required. Snapshot subcase remains explicitly untested here as reported above. Conventions and diff checks pass. These are integration-stack checks, not a standalone whole-suite result.

@MathiasDevelopes, happy for you to fold the follow-up into #2283 with authorship preserved if useful. It depends on #2283's recorded-kept-bytes fixture; the integration also has the already reported fifth gpu=true registration argument.

The currently running 007 process still has f4d4d04, not this follow-up, so I am not attributing its behavior to these entry checks. At 369 flips it remains black, paused/resumed under the memory guard during the same large NVIDIA pipeline compilation. No playability claim.

AI-assisted (Codex).

@SleglJan

Copy link
Copy Markdown
Contributor

Linux check of @Scramblrrrr's follow-up 0280ac3. Setup: CachyOS (kernel 7.2.9, GCC 16.2.1), RTX 3080 (NVIDIA 615.71.09), Release, SPIRV-Tools on. Tested on main a3c43ad with #2283 (55b09c0) merged, then f4d4d04 (the fifth gpu argument) and 0280ac3 cherry-picked.

  • Full suite. Of 722 tests, 717 pass and 4 are skipped: video_out_flip_{lifetime,reopen,controls,overlay}, under the SDL_VIDEODRIVER=offscreen their CMake registration sets. One fails: agc_driver_dcc_keys_first_write, a known failure on this machine (agc_driver_dcc_keys_first_write fails where host imports stay write-watched (NVIDIA, Linux): cause and patch #2767).
  • The four selected tests pass: agc_driver_recorded_kept_bytes, agc_recorder_kept_bytes, agc_driver_graphics and agc_host_import_unmap.
  • The draw-snapshot subcase runs here. agc_driver_recorded_kept_bytes prints recorded kept bytes tests passed without "(draw snapshots not tested)", so the reusable-snapshot case that returned early on your Windows run passes on this setup.
  • Negative control. With 0280ac3's four BoundKeptBytes() entry checks reverted (both source files set back to its parent) and its test kept, agc_driver_recorded_kept_bytes fails with a buffer-only dispatch did not submit the preceding batch at its kept-byte budget.

AI-assisted: yes (Claude Code)

MathiasDevelopes and others added 4 commits October 11, 2026 00:33
…byte budget

The recorder bounds the bytes a batch keeps alive (KeptBytesBudget) and
waits for older batches past twice that. A recorded dispatch or draw kept
its ShaderResources, and a draw its snapshot buffers, with a size of 0, so
the host-visible copies of guest buffers they own never counted. A title
that binds a 124 MiB writable guest buffer in every draw then held about
ninety copies, 11 GB, until the GPU caught up, and aborted with
VK_ERROR_OUT_OF_HOST_MEMORY from vkAllocateMemory.

GuestBufferMemory reports the bytes of its copied regions, and the
dispatch and draw keeps and the draw snapshots pass their bytes to
Recorder::Keep, so the existing bound applies to them.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ree keep sites

A ShaderResources from the resource caches is the same object for every
draw or dispatch that uses it, and Recorder::ReusableDrawSnapshot hands
the next draw the same snapshot buffer, so passing their bytes with each
Keep counted one allocation once per use and submitted the batch before
it held the budget. Recorder::KeepBytes counts an owner's bytes once per
batch, and the dispatch, draw and snapshot keeps use it.

The recorded kept bytes test runs a dispatch and a draw that copy a
guest buffer and two draws that snapshot one on the device, and fails
when any of the three sites stops counting or a reused snapshot counts
twice. The BdaResources check pins the copied region it sets up.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZHkb8kUwnHAuBBbHywvJf
Comments are reserved for technical debt (docs/dev/TechnicalDebt.md), so
the ones the previous commit added to the recorder, the snapshot keep and
the tests go.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZHkb8kUwnHAuBBbHywvJf
Co-authored-by: orishamir <orishamir@users.noreply.github.com>
Co-Authored-By: Claude <noreply@anthropic.com>
@adriadam10
adriadam10 force-pushed the fix/agc-count-copied-buffers-in-batch-budget branch from 55b09c0 to d731f50 Compare October 10, 2026 22:37

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, the new tests address the earlier review. Checked on current main:

  • The new execution test did not build against main: GuestAllocations::Mutation().Add now takes a fifth gpu argument (tests/execution/RecordedKeptBytes.cpp:97, found by @orishamir). Added as our own commit Add(block, bytes, true, true, true).
  • Build and ctest of bda_resources, recorder_kept_bytes and recorded_kept_bytes pass. Without the change the tests don't compile; removing the KeepBytes call at VulkanDevice.cpp:3489 or Draw.cpp:1436 makes agc_driver_recorded_kept_bytes fail.
  • Cross-tests by @Scramblrrrr (Windows) and @SleglJan (Linux, RTX 3080) agree.

Merging once the full Build workflow is green. The buffer-only follow-up (0280ac3) can go in its own PR.

@adriadam10
adriadam10 merged commit 617256e into boykopovar:main Oct 10, 2026
14 checks passed
@MathiasDevelopes
MathiasDevelopes deleted the fix/agc-count-copied-buffers-in-batch-budget branch October 10, 2026 23:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc*

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants