Skip to content

fix(agc): free retired depth surfaces nobody samples - #6465

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
milescoviello:fix/free-retired-depth-surfaces
Oct 19, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
milescoviello:fix/free-retired-depth-surfaces

Conversation

@milescoviello

@milescoviello milescoviello commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What

A depth surface stays in DepthSurface.cpp's list until its device goes. retire() (another writer covered its memory, #454) only sets a flag, so a retired surface keeps its image and memory. DepthSurfaceView matches a target by address, stencil address, extent, format and sample count. A title whose depth targets move through a transient heap, or change extent under dynamic resolution, therefore adds a surface for every new target and never frees the old ones.

Forspoken (PPSA03984) does this. With #5937, #5975 and #5982 (needed to get it in game), a throwaway print over a 300 s run showed 134 surfaces created: 67 at 1920x1080 and 37 at 1984x1116 (D32_SFLOAT_S8_UINT), plus 2048², 1024² and 3072² D32_SFLOAT shadow maps. 117 of them were retired by the end, still holding their memory. In another 300 s run, a throwaway vkAllocateMemory tally by call site put the DepthSurface constructor's live allocations at 629 MiB at 70 s, 1,687 MiB at 153 s and 2,665 MiB at 293 s. Between 153 s and 293 s, every other site stayed within 30 MiB. They count against no cache budget, so the sampled and storage caches shrink to make room until an allocation fails:

terminate called after throwing an instance of 'std::runtime_error'
  what():  compute shader 0x900b3a5800: AGC graphics: vkAllocateMemory storage texture: Vulkan result -2
  • Creating a surface first frees the retired ones on the same device. Only single-sample surfaces are freed, and only when no sampled view (textures) is held outside the surface. The surface goes to Recorder::Keep, so it is destroyed only after the recorded work that may still use its image completes. With no recorder there is no recorded work, so it is destroyed right away.
  • Freeing one bumps a counter (DepthSurfacesReleased). Pipeline::AcquireFramebuffer checks only the color attachments' owners, so a cached framebuffer with a depth attachment could match a recycled view handle. Such entries are now dropped once a release happens after they were made. This only matters on the render-pass path (devices without graphics pipeline library, or APS5_NO_GPL=1).
  • Multisampled surfaces keep their existing ownership (owned, the depth guards) and are not freed here.

DepthSurfaceReuse.cpp gains TestRetiredSurfaceRelease. Four retired surfaces at moving addresses must free their images when the next surface is created. A retired surface whose sampled view is held must keep its image until the view goes. A surface that was not retired must keep its image. On main's DepthSurface.cpp (with a stub accessor) the test stops at "a retired depth surface must free its image once a new surface is created".

Tested

Linux (Ubuntu 24.04, GCC 13.3, Release), RTX 4070 Ti, NVIDIA 595.99:

Build Result GPU memory in use
without this PR stops at 128 s with the error above climbs to 11.8 GB of 12 GB
with this PR reaches the 600 s limit about 10.0 GB from 90 s to the end, peak 10.15 GB
with this PR and APS5_NO_GPL=1 (render passes, so the framebuffer cache is used) reaches the 600 s limit peak 10.7 GB

GPU memory is nvidia-smi's total, including about 2.1 GB the desktop used before the title started. An earlier run on main 8856f18 with the same three PRs stopped at 180 s on vkAllocateMemory render target: Vulkan result -2.

On the Intel VM (Arrow Lake iGPU, Mesa 26.1.6 ANV, Debian 13), the agc_driver_graphics binary from 0538d6a, which holds the new DepthSurfaceReuse cases, passes 3 of 3.

@SleglJan ran a second title, 007 First Light (PPSA11386), on an RTX 3080 (615.78.08, Linux), with this head on their bring-up stack. It keeps creating depth surfaces in gameplay, about 13 per present (2,199 by present 1350). With the PR, 10 to 23 are alive at a time. Without it, the depth-surface list keeps growing. Each sampled-texture lookup scans that list in DepthSurfaceTexture, which was 17.8% of the title's samples in a perf profile. Time per present grew from 8.3 s to 28.6 s, and the driver ran out of device-local memory 15 presents into gameplay, placing about 19 GB of storage textures in host memory over the run. With the PR, time per present stays at 5.5 to 6.3 s for the whole run, nothing is placed in host memory, and device-local use is 6.8 to 7.2 GB against 7.4 to 8.2 GB.

Not checked on Windows, on AMD, or on a console.

Checklist

#6215 changes the same part of AcquireFramebuffer (for multisampled color targets). Whichever lands second needs a small rebase.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 15, 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 6727 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp L477-478

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 1368 core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/tests/DepthSurfaceReuse.cpp
PR 1395 core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
PR 4072 core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp
PR 5499 core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
PR 5609 core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp
PR 5925 core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp
PR 6300 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
PR 6451 core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp
PR 6505 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
PR 6539 core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp
PR 6615 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp
PR 6683 core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp
PR 6801 core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp
PR 6808 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
PR 6947 core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp
PR 6959 core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp
PR 6970 core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp
PR 6977 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
PR 7154 core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
PR 7201 core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp
PR 7233 core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
PR 7256 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
PR 7332 core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp
PR 7371 core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
PR 7642 core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/Pipeline.cpp

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
@milescoviello
milescoviello force-pushed the fix/free-retired-depth-surfaces branch from 81dddc8 to 0538d6a Compare October 15, 2026 16:54
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 15, 2026
@github-actions github-actions Bot added the agc libSceAgc* label Oct 16, 2026
@SleglJan

Copy link
Copy Markdown
Contributor

Data from a second title, 007 First Light (PPSA11386 v01.018.000), on Linux 7.2.10, NVIDIA GeForce RTX 3080 10 GB, driver 615.78.08, GCC 16.2.1, Release with BUILD_TESTING=ON, in a bubblewrap sandbox. Head 0538d6a6a cherry-picked onto a bring-up stack: main 4186f06 plus open PRs (#5987 and #6553 among them), unpublished local fixes the title needs to get this far, local logging and scripted input, and the title's two feedback-wait limits widened in its executable as described in #6172 (local only). Non-default upstream switches: APS5_RESOURCE_CACHE_ENTRIES=64, APS5_NO_DRAW_SRT_ENTRIES=1, APS5_HOST_IMPORT_MIB=12288. Only tests/DepthSurfaceReuse.cpp needed a conflict resolved on that older base. Two runs on the same stack, with and without this PR (the run with it also carries the local counter below), same switches, save and scripted input, both stopped at present 1350. They reached gameplay at different presents (1209 without the PR, 1265 with it), so the figures below are aligned by presents into gameplay.

This title keeps creating depth surfaces in gameplay. A local counter in the run with this PR:

  • 1,046 depth surfaces were created before gameplay began and 2,199 by present 1350, about 13 per present in gameplay.
  • 10 to 23 surfaces were alive at any present in gameplay (149-172 MiB), 1 to 18 of them retired and not yet freed.
  • The run without this PR had no counter. The counter does not tell a new target from one created again after this PR freed it.

The list's length also costs time. Each sampled-texture lookup calls DepthSurfaceTexture, which scans the whole list (std::none_of(surfaces()...) under surfacesMutex) unless a held surface matches. In this title Revalidate's full walks cover about 1.9 million image-table elements per present, and each element they compare is such a lookup. In a user-space perf profile of a third run without this PR (same build and switches), 41-44 presents into gameplay, DepthSurfaceTexture was the costliest driver function: 17.8% of the title process's samples, most of them in that predicate.

Per present, averaged over the same presents into gameplay:

presents into gameplay wall time without / with Revalidate time without / with per image-table element without / with
10-19 8.3 s / 5.9 s 3.94 s / 2.45 s 2.06 µs / 1.28 µs
40-49 10.0 s / 5.9 s 5.29 s / 2.48 s 2.76 µs / 1.27 µs
76-85 16.8 s / 5.9 s 9.54 s / 2.38 s 5.03 µs / 1.23 µs
  • Without this PR, Revalidate's work stayed the same (about 500 walks and 1.9 million elements per present) while its time per element grew. Over the run's last nine presents (133-141 into gameplay) it took 18.2 s per present and 9.7 µs per element, at 28.6 s wall time per present.
  • With this PR, wall time stayed at 5.5-6.3 s per present from the fourth present of gameplay to the end of the run (85 presents into gameplay).
  • Memory: without this PR, from 15 presents into gameplay on, the driver refused device-local allocations and the stack placed new textures and device buffers in host memory (the fallback from fix(agc): place device memory in host memory when the driver refuses a device-local allocation #5987, which fix(agc): place device memory in a host heap only when that heap has room #6553 includes). By present 1350 the run had created 2,504 storage textures in host memory (about 19,100 MiB, cumulative) and its device-buffer placement counter had passed 8,192; 1,129 MiB of live storage textures were in host memory at present 1350. With this PR the run made no host placement, and the device-local heap's reported usage was 6,842-7,225 MiB per present in gameplay, against 7,402-8,245 MiB without it.
  • Title runs only; no CTest run of this head on this machine.

AI-assisted: yes (Claude Code)

@milescoviello

Copy link
Copy Markdown
Contributor Author

Thanks @SleglJan, a second title, and one where the list cost shows up in frame time as well as memory, is very helpful. I added your results to the description.

AI-assisted: yes

@milescoviello
milescoviello force-pushed the fix/free-retired-depth-surfaces branch from 0538d6a to ab68352 Compare October 17, 2026 18:37
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
@adriadam10 adriadam10 self-assigned this Oct 19, 2026
A depth surface stayed in the list until its device went, even after another
writer retired it. A title whose depth targets move or change extent (dynamic
resolution, a transient heap) adds one surface per new target and never frees
the old ones. Creating a surface now frees the retired single-sample ones whose
sampled views no one else holds, through the recorder so recorded work that
still uses them completes first. Framebuffers cached with a depth attachment are
dropped after a release, since a freed view's handle can be reused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@adriadam10
adriadam10 force-pushed the fix/free-retired-depth-surfaces branch from ab68352 to 98600b4 Compare October 19, 2026 01:12
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 19, 2026
@milescoviello

milescoviello commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

@adriadam10 thanks for rebasing this onto main 0c25997. Your head 98600b4 has the same tree as the resolution I'd made locally (adbeaa8d3b), so I didn't push. On that tree, agc_driver_graphics (with the DepthSurfaceReuse cases) passes 3 of 3, and the 24 tests matching ^agc_driver_graphics$|depth pass. That was Linux, RTX 4070 Ti.

The resolution: main's multisample retirement change (785f8f7) reworked Pipeline::AcquireFramebuffer. Its color owners are shared_ptr<void> now, and the persistent parameter is gone. main's owner loop stays, with this PR's depthReleases read next to it.

AI-assisted: yes (Claude Code)

@adriadam10
adriadam10 merged commit fd2ef7e into boykopovar:main Oct 19, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc*

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants