Skip to content

fix(agc): keep depth planes coherent with buffer reads and writes of their memory - #7154

Open
XairRrR wants to merge 3 commits into
boykopovar:mainfrom
XairRrR:fix/agc-depth-plane-buffer-coherence
Open

XairRrR wants to merge 3 commits into
boykopovar:mainfrom
XairRrR:fix/agc-depth-plane-buffer-coherence

Conversation

@XairRrR

@XairRrR XairRrR commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

What

On main a depth surface lives only in its Vulkan image. Two buffer accesses to its depth plane go wrong:

  • Buffer read. A shader or vertex fetch that reads the plane through a buffer descriptor reads guest memory, which no draw has ever written. It gets zeros or old bytes, not the depth that was drawn.
  • Buffer write. If a buffer write (a compute store or a CPU write) changes the plane, the surface never sees it. Binding the target again keeps the old depth. fix(libSceAgcDriver): retire a depth surface once another writer covers its memory #454 retires the surface when it is sampled through a view it cannot serve, but a bind as a depth target still keeps the image ("Bound again, it keeps its own depth instead of the bytes written over it", debt entry).

Uncharted: Legacy of Thieves Collection (PPSA05684, #4001) hits both in its main menu. A buffer compute (61440x1x1, buffer(ro) -> buffer(rw), 0xf00000 bytes) copies the main depth plane as raw memory into two ping-pong planes. The title then draws the cage's depth-only meshes into those planes, downsamples them to linear depth and fogs the scene from the result. On main the copy carries nothing (89 % of the half-res depth reads 1.0), so the fog covers the cliff and the whole skeleton cage.

Changes

  • A single-sample D32_SFLOAT/D32_SFLOAT_S8_UINT surface with the 64 KiB Z swizzle (DB_Z_INFO.SW_MODE 24, SW_64KB_Z_X) keeps a storage image in that swizzle over its depth plane (mirrorPlane, guest format 32_FLOAT).

  • DepthSurfaceView now takes depthWritten, and both bind sites in Draw.cpp pass state.depthWrite. A bind that writes depth, or applies a recognized HTILE fill clear, marks the plane stale.

  • New function FlushDepthPlanes(address, bytes). It copies each stale plane that overlaps the range into its storage image, then lets StorageTexture::FlushPending store the image into guest memory. It is called from:

    • StorageTexture::FlushPending
    • GuestBufferMemory::UploadFinish, over the regions the build binds through its descriptors (not over the whole heaps an address-based shader maps, as for pending storage images)
    • VulkanDevice::dispatch, for each V# of its GuestBuffers bindings. This is needed because a cached dispatch reads its buffers without an upload.
    • every image lookup (sampled and storage) over the range it reads, and the write-stamp checks that let a cached image or a reused build skip that lookup (fastTexture, fastRevalidate, fastRevalidateEach): a stale plane's memory has not been written yet, so it looks unchanged to them
    • a reused build, over its in-place buffer regions

    Stale planes are kept as a list of ranges under their own lock, with an atomic hull. A call whose range overlaps no stale plane returns without taking GpuMutex or the surfaces lock.

  • Every bind of such a surface, and every flush of its stale plane, collects the plane's writes (GuestMemory::CollectWrites). If someone else wrote the plane since the surface's previous bind, store or reload (WrittenSince against the generation taken then), the surface reloads its depth from the storage image instead of storing. So a guest write made after a depth draw is not overwritten by that draw's late store, also before the first store. A guest write is ordered by the bind or flush that collects it: it replaces the depth drawn before it, and depth drawn from a later bind is stored over it.

  • A pending HTILE fast clear wins over the reload: a tile HTILE marks cleared reads the clear value whatever the plane's bytes hold.

  • A color target flushes the stale planes under it before it retires their surfaces (Draw.cpp, as the storage-image path already did), so a target that loads or blends reads the stored depth. retire() clears the surface's memoryStale and drops its range from the stale list, so a later flush never stores a retired plane's old depth over what the new owner of the memory wrote.

  • A bind of a mirrorable target drops the plane images, and the pending store, of older surfaces whose planes overlap its plane over another range (another extent or base, as when a title changes its output resolution over the same memory). Those images stayed live and lent their old results over the new plane, also to buffer copies served as image copies. A same-range surface (another stencil or HTILE address) keeps its image. The scan runs only when a surface or plane image was created since the surface's last scan.

  • The surfaces mutex is now a std::recursive_mutex, because a store reached from a flush under DepthSurfaceTexture re-enters it.

  • copyPlane can run in its own CommandBatch when no recorder is active.

  • Debt: a new entry, "Depth planes in guest memory", lists what stays incoherent: D16 planes, other swizzles, stencil, MSAA, CPU reads, reads through the heaps an address-based shader maps. It also states the ordering above and its granularity: the tracker stamps 64 KiB blocks and a store or reload covers the whole plane, so a partial guest write over unstored depth also drops that depth outside the written bytes. It also notes that console memory under a compressed HTILE holds compressed data until a DB decompress (HTILE definitions in Mesa src/amd/common/ac_descriptors.h). The fix(libSceAgcDriver): retire a depth surface once another writer covers its memory #454 entry points to the new entry.

No new GPU semantics are introduced. The bytes in guest memory come from the existing kZ64KBX detiler, the same equation a sampled view of the plane already uses.

Tested

Arch Linux, kernel 7.2.9-zen, NVIDIA RTX 4070 Ti SUPER (driver 615.78.08), GCC 16.2.1, Release, BUILD_TESTING=ON, on main 40315fc.

  • agc_driver_depth_plane_buffer_memory (new, tests/execution/DepthPlaneBufferMemory.cpp, on the DepthFastClear harness, which gains a swizzle field and a vertex-buffer argument):

    1. Draws depth 0.5 into a 64x32 D32 Z_X plane, then fetches vertices from the plane. All 2048 texels must read 0.5.

    2. Writes 0.75 into the plane's memory, then draws at 0.5 under LESS. The draw must pass.

    3. Draws at 0.5 with depth write, then fetches again. Memory must hold that 0.5.

    4. On a new plane: draws 0.5, writes 0.75 into memory before any store, then fetches the plane with a draw on another target. Memory must still hold 0.75, and a draw at 0.5 under LESS must pass. Then it writes 0.25 and draws 0.5 with depth write: the fetch must see 0.5.

    5. On a plane with HTILE: stores 0.5, writes 0.25, then a whole-HTILE ZMask 0 fill. A draw at 0.5 under LESS must pass (the clear wins).

    6. A linear R32F texture at an offset into a plane must read what memory holds after a flush, also when it is viewed through its cached storage image after another depth draw.

    7. On a plane filled with 0.25: draws depth 0.5, then a color target at the same address that blends ZERO/ONE (keeps what it loaded), then fetches the plane. It must hold 0.5. Red without the color-target flush, with or without the retire() change.

    Step 4 is red on the previous revision (064a08c): "the depth 0.5 a draw wrote before a guest write into the plane was stored over that write". Step 5 is red when the reload drops the pending clear, as 064a08c did. Step 6 is red on 53b20c4 at the cached storage image read.

    Red on main at step 1 ("a vertex fetch from the depth plane did not see the depth 0.5 a draw wrote into it"). With step 1's check removed, it is red at step 2 ("a draw at depth 0.5 failed LESS against the 0.75 written into the depth plane's memory"). Green with the change. Steps 2 and 3 run only where guest memory is write-watched; the test prints when it is not.

  • All 701 agc_ tests pass (ctest -R ^agc_ -j8, on c8a8ff1); agc_driver_stencil_ref_export skips on this device as on main.

  • Uncharted: Legacy of Thieves Collection (PPSA05684), on the local integration branch (main + open PRs): the main menu shows the cliff and the whole cage under the fog in all 15 menu frame dumps. Frame times against the earlier version are in the comments. The title does not run on plain main yet.

Overlap with open PRs

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 under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Unblocks: PPSA05684 Uncharted: Legacy of Thieves Collection (PPSA05684) on Linux / NVIDIA #4001
  • GPUs tested: NVIDIA RTX 4070 Ti SUPER, driver 615.78.08, Linux
  • AI-assisted: yes

@MarcZX10

Copy link
Copy Markdown
Contributor

I traced the documented unstored-depth limitation through the new buffer-write path. The current regression calls the buffer reader before writing 0.75, so that first access has already run FlushDepthPlanes and set storedGeneration. If a CPU/buffer write happens after the depth-writing bind but before the first store, UploadFinish calls FlushDepthPlanes while memoryStale is true; that path unconditionally StoreDepthPlanes the old image, and SyncFromMemory is not reached before the guest write can be overwritten. With storedGeneration == 0, there is no prior store generation to compare either. Could you add a regression for this ordering and decide whether the store should detect/reload the newer guest write, or make the supported buffer-write claim explicitly require a prior depth-plane flush? The current limitation is documented, but this ordering is close to the advertised buffer-write case.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for this. Review at 064a08c: changes requested before it can go further.

Confirmed from the code:

  • An HTILE fast clear gets dropped when SyncFromMemory reloads the plane in the same bind (DepthSurface.cpp, the reload path). The clear should win over the reload.
  • VulkanDevice::dispatch decodes every StorageBuffer/UniformBuffer guestDescriptor as a V#, including bindings that aren't buffer descriptors (ShaderData, FlattenedSrt, ImageTableMap, and so on). The scan should be limited to GuestBuffers.

Plausible:

  • The late write-back can overwrite guest data written after the depth draw, since nothing checks WrittenSince before the store.

Performance:

  • anyMemoryStale looks effectively always set, so every FlushPending, dispatch binding and UploadFinish region takes GpuMutex plus surfacesMutex, and stores can SubmitAndWait synchronously.

Asks:

  1. Let the HTILE clear win over a reload.
  2. Before storing, check WrittenSince on the plane since its depth write.
  3. Restrict the dispatch scan to GuestBuffers.
  4. Post frame-time numbers on a depth-heavy title, and say what keeps anyMemoryStale from staying permanently set.
  5. Add tests for (1) and (2).

Ordering note: this merges cleanly with #7131, #7116 and #7085, but #7131 handles part of the same case (a fill writing depth memory), so the order between them will need settling.

@github-actions github-actions Bot added docs agc libSceAgc* labels Oct 17, 2026
@github-actions

github-actions Bot commented Oct 17, 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 6682 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp L3641
PR 7371 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp L1210-1217
PR 7650 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp L3132-3133, L4384-4385

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 1324 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 1368 core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 1395 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 4040 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 4072 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 4448 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 4449 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 4472 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 5021 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 5058 docs/dev/debt/libSceAgcDriver.md
PR 5441 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 5499 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
docs/dev/debt/libSceAgcDriver.md
PR 5609 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 5925 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6146 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6177 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6185 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 6186 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6192 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6194 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6197 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6202 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6218 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6223 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6249 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6290 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6292 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6300 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6340 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6539 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6548 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6549 docs/dev/debt/libSceAgcDriver.md
PR 6612 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6615 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6636 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/debt/libSceAgcDriver.md
PR 6661 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6683 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6727 core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6741 docs/dev/debt/libSceAgcDriver.md
PR 6762 docs/dev/debt/libSceAgcDriver.md
PR 6788 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 6808 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6867 docs/dev/debt/libSceAgcDriver.md
PR 6868 docs/dev/debt/libSceAgcDriver.md
PR 6937 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6941 docs/dev/debt/libSceAgcDriver.md
PR 6946 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6947 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6955 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 6958 docs/dev/debt/libSceAgcDriver.md
PR 6960 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6966 docs/dev/debt/libSceAgcDriver.md
PR 6970 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6971 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 6972 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6977 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7008 docs/dev/debt/libSceAgcDriver.md
PR 7048 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7201 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 7233 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 7254 core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7256 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7258 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7292 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 7332 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7340 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 7369 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
PR 7390 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7432 docs/dev/debt/libSceAgcDriver.md
PR 7449 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7473 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 7484 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 7505 docs/dev/debt/libSceAgcDriver.md
PR 7514 core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
PR 7604 core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7622 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
PR 7642 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7647 core/libs/prx/libSceAgcDriver/Execution/src/VulkanDevice.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp
PR 7659 docs/dev/debt/libSceAgcDriver.md
PR 7689 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 7729 docs/dev/debt/libSceAgcDriver.md
PR 7800 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/Texture.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7803 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md

@XairRrR
XairRrR force-pushed the fix/agc-depth-plane-buffer-coherence branch from 064a08c to 53b20c4 Compare October 17, 2026 15:33
@XairRrR

XairRrR commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @MarcZX100 and @oneandonlydean. Pushed 53b20c4 (amended), PR text updated.

Write after the depth draw, before the first store (MarcZX100, ask 2): every bind of a mirrored surface now collects the plane's writes and takes the tracker generation, created surfaces included, so there is always a baseline. FlushDepthPlanes checks WrittenSince against it before storing; if the plane was written since the last bind, store or reload, it reloads the depth from memory instead of storing. The rule: a guest write is ordered by the bind or flush that collects it. It replaces the depth drawn before it, unstored depth included, and depth drawn from a later bind is stored over it. On the console both are memory writes that land per byte in submission order. Here the tracker stamps 64 KiB blocks and a store or reload covers the whole plane, so a partial guest write over unstored depth also drops that depth outside the written bytes. The debt entry says this.

HTILE clear vs reload (ask 1): a pending depth fast clear now skips the reload, and the reload no longer clears pendingClear. A tile HTILE marks cleared reads the clear value whatever the plane bytes hold, so the clear wins.

Dispatch scan (ask 3): only DescriptorRole::GuestBuffers bindings are decoded as V#s.

Tests (ask 5), in agc_driver_depth_plane_buffer_memory:

  • New plane: draw 0.5, CPU write 0.75 before any store, then a buffer fetch through a draw on another target. Memory must keep 0.75 and the next LESS draw must pass. Then a write of 0.25 followed by a depth draw: the draw's 0.5 must reach memory.
  • Plane with HTILE: store 0.5, write 0.25, then a whole-HTILE ZMask 0 fill. The LESS draw at 0.5 must pass.

The first check is red on 064a08c. The HTILE one is red when the reload drops the pending clear, as 064a08c did. All 646 agc_ tests pass and so does the conventions check.

Performance (ask 4): you're right that the flag stayed set. A depth target drawn every frame and never read through a buffer stays stale until a flush over its range, so one global flag is set almost all the time. I replaced it with the list of stale plane ranges, kept under its own small lock, plus an atomic hull. FlushPending, dispatch bindings and UploadFinish regions that miss every stale plane return after the hull check, or one short lock inside the hull, without GpuMutex or the surfaces lock. A flush that covers the range rebuilds the list from the surfaces that are still stale. What's left: a range that does overlap a stale plane still stores it, and outside an active recorder (a CPU-side flush) that store submits and waits. I don't have frame times yet. I'm measuring this revision against the previous one in PPSA05684 (menu and a depth-heavy gameplay stretch) and will post the numbers here.

#7131: with both PRs, a recognized whole-plane fill becomes a pending clear with the fill value, so the reload here is skipped and the fill wins exactly. Any other write to a D32 Z_X plane reloads. That holds in either merge order. The second one needs a small rebase in DepthSurfaceView.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for 53b20c4. Asks 1 (HTILE clear wins over the reload), 2 (WrittenSince before the store), 3 (GuestBuffers only) and 5 (tests) are closed in the code. The new checks fail with the amend reverted, so they test the fix.

Still owed: the before/after numbers for ask 4 on a heavy title (frame time and [lock] lines). Please make sure the A/B also covers the new per-bind cost: SyncFromMemory, and with it GuestMemory::CollectWrites over the whole plane, now runs on every bind of a mirrorable depth target, created surfaces included.

Heads-up on order: #7131 is planned to merge first. After it, this PR will conflict on one line in DepthSurfaceView (surface->NoteHtileClear(htileCleared); vs surface->pendingClear |= htileCleared; plus the new SyncFromMemory line). The resolution keeps NoteHtileClear(htileCleared) followed by the SyncFromMemory line. No need to rebase until #7131 lands.

@XairRrR
XairRrR force-pushed the fix/agc-depth-plane-buffer-coherence branch from 53b20c4 to f5f7fcb Compare October 17, 2026 20:10
@XairRrR

XairRrR commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Pushed f5f7fcb, with a correction to my previous reply.

Correction. Restricting the dispatch scan to GuestBuffers (ask 3) exposed a real missing flush. Before, the old scan decoded float constants in FlattenedSrt bindings as V#s spanning hundreds of GB, which stored every stale plane by accident. Without that, cached images over a plane read stale memory. The write-stamp checks that let a cached texture, a cached storage image or a reused build skip their lookup see a stale plane's memory as unchanged, because it has not been written yet. In PPSA05684 this left the main menu under a blue fog wall in 15 of 17 frames. Now every image lookup and each of those checks (fastTexture, fastRevalidate, fastRevalidateEach) stores an overlapping stale plane first, and a reused build flushes its in-place buffer regions. The dispatch scan stays restricted to GuestBuffers. New test step: a linear texture at an offset into a plane, read through its cached storage image after another depth draw. It is red on 53b20c4 and green now. All 646 agc_ tests pass.

Frame times (ask 4). Arch Linux (kernel 7.2.9-zen), i9-14900KF, RTX 4070 Ti SUPER, NVIDIA 615.78.08. Local integration branch (main 864729d + open PRs #6412 #6182 #6771 #6772 #6280 + local patches), with this PR's earlier version 63f1e9078 (A) or f5f7fcb (B). Uncharted: Legacy of Thieves Collection, warm caches. Two 10-minute runs per side into a depth-heavy scene (APS5_PROFILE_DRAW=1), plus 60-75 s main-menu runs.

A run 1 A run 2 B run 1 B run 2
main menu: fps, mean / p95 / p99 ms 9.23, 108 / 124 / 133 8.78, 114 / 127 / 140 9.07, 110 / 129 / 137 9.19, 109 / 131 / 142
scene, first 2 min: fps, mean / p95 / p99 ms 4.99, 201 / 266 / 434 4.84, 206 / 271 / 303 4.98, 201 / 266 / 326 4.99, 200 / 275 / 442
scene, heavy part (last 120 s): fps, mean / p95 / p99 ms 1.77, 564 / 652 / 687 1.80, 554 / 633 / 673 1.96, 511 / 600 / 638 1.87, 535 / 603 / 644
depth plane stores per 10 s (heavy), count / MiB 1132 / 16002 1084 / 15261 121 / 1168 136 / 1222
graphics queue device lock, ms per frame (heavy) 106.7 104.7 79.3 80.8
GPU-mutex waits, all queues, ms per 10 s (heavy) 2176 2374 2160 2021

Perf (DWARF stacks, three busiest driver threads, heavy part): FlushDepthPlanes 0.30 / 2.77 / 0.04 % on A, 0.11 / 0.39 / 0.05 % on B. StoreDepthPlane is about 0 on both. FlushPending and UploadFinish are 0.1-0.6 % on both.

What keeps the stale state from staying set: a plane leaves the list when it is stored or reloaded, and a flush only touches planes in its range. That is why B stores about 9 times fewer planes than A, whose global flag made every flush store every stale plane. A depth target drawn every frame is still stale between its draw and the first read over its memory; a read that overlaps it stores it, and outside an active recorder that store submits and waits.

No new depth errors, throws, crashes or device losses. All 15 menu frame dumps with B show the cliff and the whole cage.

AI-assisted: yes

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 17, 2026
@XairRrR
XairRrR force-pushed the fix/agc-depth-plane-buffer-coherence branch from f5f7fcb to b596505 Compare October 17, 2026 21:41
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 17, 2026
@oneandonlydean oneandonlydean self-assigned this Oct 17, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 17, 2026
@blawesom

Copy link
Copy Markdown
Contributor

Rebase handoff — rebased onto current main (authorship unchanged). Two conflicts in ShaderResources.cpp: main's DepthSurfaceOverlaps fast-fail guard kept alongside your FlushDepthPlanes coherence check, both applied in order.

Verified on Linux (CachyOS, kernel 7.2.9, GCC 16.2.1, Ninja, Release, BUILD_TESTING=ON, AMD RX 9070 XT / RADV): builds; conventions pass; full suite green apart from the known main regression (agc_driver_tessellation segfault — Mesa, #5444), not this PR — your depth-plane tests pass.

To pick it up:

git fetch https://github.com/blawesom/AnyPS5 rebase/7154
git checkout <your branch>
git merge --ff-only FETCH_HEAD

Head is 07a967b67.

@XairRrR
XairRrR force-pushed the fix/agc-depth-plane-buffer-coherence branch from b596505 to 19fc797 Compare October 18, 2026 10:55
@XairRrR

XairRrR commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @blawesom. I rebased it myself onto main 9caecfd as 19fc797, with one difference from your resolution: in fastRevalidate and fastRevalidateEach I kept main's DepthSurfaceOverlaps guard and dropped this PR's FlushDepthPlanes line. The guard already sends any image over a depth surface to the full walk, which flushes stale planes, so with both lines the flush could never find anything. All 701 agc_ tests pass (Linux, RTX 4070 Ti SUPER), conventions OK.

AI-assisted: yes

@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for the rebase and the f5f7fcb fixes; the flushes before each write-stamp shortcut look right.

One gap, which was already in 53b20c4: DepthSurfaceOverlaps only counts active, non-retired surfaces, but FlushDepthPlanes stores every memoryStale surface, and retire() leaves memoryStale set. So a plane that is drawn and then retired by a color target at the same memory (Draw.cpp:2042, no flush before it) is skipped by the storage-image fast path, and a later flush over that range stores the old depth over the color target's write-back.

Could you:

  1. have retire() clear memoryStale and drop the surface's range from the stale list, so a retired plane is never stored later;
  2. flush before retiring in the color-target path (FlushDepthPlanes(color.address, ...) before Draw.cpp:2042), as ShaderResources.cpp:984 already does for storage images;
  3. add a test step: depth draw into a plane, then a color target at the same address that loads or blends, then a buffer read?

With that, dropping the flush in the storage-image loops is fully justified. On performance, our main-vs-PR gate runs will cover the comparison against plain main, so no extra A/B is needed from you.

@XairRrR

XairRrR commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

@oneandonlydean thanks, pushed 154c405 with all three:

  1. retire() clears memoryStale and drops the plane's range from the stale list (unless another stale surface still covers the same range), so a retired plane is never stored later.
  2. The color-target path calls FlushDepthPlanes(color.address, ...) before RetireDepthSurfaces, as the storage-image path does.
  3. agc_driver_depth_plane_buffer_memory has a new step: fill a plane with 0.25, draw depth 0.5 into it, then draw a color target at the same address that blends ZERO/ONE (keeps what it loaded), then fetch vertices from the plane. The plane must hold the 0.5.

Negative controls on this test: without (2), with or without (1), the new step fails (a color target blending over a depth plane did not load the depth drawn into the plane); with (2) alone it passes.

I also tried a step for the late store itself (depth draw, plain color overwrite, buffer read). It passed even without both changes. In the test, the color write-back is a CPU write the tracker sees, so the flush's write-generation check reloads the plane instead of storing it. So (1) only matters for a write-back the tracker does not see, and the test cannot reach that case. I left that step out because it passes either way.

All 701 agc_ tests pass (Linux, RTX 4070 Ti SUPER), conventions OK.

AI-assisted: yes

@XairRrR
XairRrR force-pushed the fix/agc-depth-plane-buffer-coherence branch from 19fc797 to 154c405 Compare October 18, 2026 11:44
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks. On 154c405 the functional results are clean, but the gate measured a consistent cost against main on the same base: the level-phase median frame time is 7-9% higher (reproduced in 2 of 2 runs, both above all 3 controls), and menu FPS is 2-4% lower. Before merging we'd like to cut that cost first. From reading the head, it seems to come from three places:

  1. The flush in GuestBufferMemory::UploadFinish. It calls FlushDepthPlanes over every upload region and over the whole cached address space (space->base). Every depth-writing draw on a mirrorable target sets memoryStale again in DepthSurfaceView, so the next build's UploadFinish most likely overlaps the plane and stores it, even if that draw never reads the depth memory. If so, a depth pass stores the full plane once per draw rather than once per reader.
  2. The cost of each store. StoreDepthPlane → copyPlane copies the full plane image → staging buffer → storage image behind three ALL_COMMANDS barriers, or with SubmitAndWait when no recorder is active. Then FlushPending writes it back to guest memory.
  3. The global lock. FlushDepthPlanes holds GuestMemory::GpuMutex() together with surfacesMutex() while it runs SyncFromMemory and records the copies for every overlapping stale surface. That serialises other threads' guest-memory work for the duration.

Suggestions (your call on the details):

  • Flush where memory is actually read, not at upload: drop the UploadFinish flushes, especially the space->base loop, and rely on the consumer sites that already flush (cachedTexture, cachedStorageTexture, FlushPending, the GuestBuffers scan in VulkanDevice::dispatch, Revalidate), narrowed to the ranges they bind.
  • Make sure a plane is stored at most once between depth writes and its next reader. Mark it stale once per depth pass, or skip the store when nothing has read the memory since the last store.
  • Take GpuMutex only for the part that needs it, and keep the copy recorded into the active recorder (no SubmitAndWait) on the draw path.

A per-frame count of StoreDepthPlane calls (you had a store counter in the f5f7fcb comparison) before and after, on the same scene, would show whether point 1 is the main cost. Frame time against plain main from the same machine would let us re-gate quickly.

Separately, could you name the case this PR fixes, as a test or a short scenario without a game title? For example: what reads the depth memory as a buffer or texture, what it sees on main and what it sees with the PR. DepthPlaneBufferMemory tests the mechanism, but the PR doesn't yet say which behaviour on main is wrong. That helps us weigh the remaining cost against the fix.

@github-actions github-actions Bot added the unblocks title Names a title ID on the Unblocks line label Oct 18, 2026
@XairRrR
XairRrR force-pushed the fix/agc-depth-plane-buffer-coherence branch from 154c405 to 9626d8e Compare October 18, 2026 23:44
@XairRrR

XairRrR commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

@oneandonlydean thanks for the gate numbers. Pushed 9626d8e (amended).

What changed. The space->base loop in UploadFinish is gone. space->base holds every registered heap an address-based shader maps, so it overlapped every stale plane. Each build with such a shader therefore stored every plane drawn since the last store, once per draw instead of once per reader, which is your point 1. main already exempts those heaps from pending storage-image flushes ("such shaders do not read texture memory through pointers"), and depth planes now follow the same rule. The debt entry says a read through those heaps is not caught. The loop over regions stays: those are the ranges the build binds through its descriptors, and it covers the CPU-copied regions that no FlushPending reaches.

A plane is now stored only when a reader's range overlaps it, and the store clears memoryStale, so depth draws between two readers cost one store. On the locks: draws and dispatches already hold GpuMutex while they record, so on those paths the flush's lock is a recursive re-entry. copyPlane records into the active recorder whenever one exists, and it submits and waits only for a flush from a CPU-side access.

Numbers. Arch Linux, RTX 4070 Ti SUPER, NVIDIA 615.78.08, 2560x1440. PPSA05684 on my integration build (the title does not run on plain main yet), same session, APS5_PROFILE_DRAW=1 in every run:

154c405 9626d8e mirroring off
main menu: depth plane stores per 10 s ~680 (5.5 GB) ~176 (2.65 GB) 0
main menu: fps, last 60 s 8.43 8.70
level, steady part: fps, 120-190 s after the first frame 5.17-5.25 5.13-5.20 5.27-5.28
level, heavy part: depth plane stores per 10 s ~140-170 ~64-75 0
level, heavy part: fps, first 75 s 2.29 2.27 (run ended before)

"Mirroring off" is 9626d8e with mirrorable() returning false: nothing is marked stale, synced or stored. It is the closest I can get to main here. The remaining stores all come from real readers. The dispatches that copy the depth plane as a buffer (about two per frame) take about 100 stores per 10 s. Image-table entries whose texture ranges lie over a stale plane take about 130.

This title runs at 2-5 fps here, so per-draw costs hide in its frame time. Your gate title runs at 20-27 fps in 4K, where a plane is about 33 MB and a store costs a larger share of a frame. I could not reproduce the 7-9% here and cannot run that title, so a re-gate would tell whether the store fix closes it. If a cost remains, the likely source is the per-bind SyncFromMemory (a CollectWrites over the whole plane on every bind of a mirrorable target). I can make that skip binds whose plane cannot have been written since the last check.

The case it fixes, without a title. A draw writes depth into a single-sample D32 plane with the 64 KiB Z swizzle. A compute dispatch then reads the plane through a buffer V# and copies it as raw dwords into another buffer, which is later sampled as depth. On main, the dispatch reads whatever guest memory held, because no draw writes depth into guest memory: zeros on first use, then stale bytes. With the PR, it reads the drawn depth. Step 1 of agc_driver_depth_plane_buffer_memory is this case with a vertex fetch as the reader, and it fails on main ("a vertex fetch from the depth plane did not see the depth 0.5 a draw wrote into it"). The reverse is step 2: a buffer write into the plane, then a depth-tested draw. On main, the draw tests against the old image.

All 701 agc_ tests pass, and so does the conventions check.

AI-assisted: yes

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

XairRrR commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Pushed c8a8ff1 on top: a fix for a bug in this PR that a title hits when it changes its output resolution.

Case. The title switches its render mode at runtime (2560x1440 → 1920x1080 → 3840x2160 → back). Each mode binds new D32 Z_X depth targets over memory where the previous mode's planes were: the 3840x2160 plane covers both the 2560x1440 and the 1920x1080 planes, from another base. After the first switch, buffer reads of the new planes kept seeing old depth, and that stayed true after switching back to the first mode.

Cause. Each surface stores through a storage image of its own plane (mirrorPlane), kept for the surface's life. The older surfaces' images stayed live over the new planes' memory. Storage images over the same memory lend each other pending results, and a buffer copy whose source range is exactly a live image becomes an image copy (AliasCopy). So the new plane's stores stopped reaching the readers.

Counted per 10 s on the main plane (a store lands in guest memory, and the next bind sees it as written and syncs):

first mode after a switch, before the fix after a switch, with the fix (same rule, counted before the scan limit was added)
stores ~100 ~97-119 ~60-125
syncs at bind after the store ~100 0 ~60-125

Fix. A bind of a mirrorable target drops the images, and the pending store, of older surfaces whose planes overlap its plane over another range (another extent or base). A same-range surface (another stencil or HTILE address over the same plane) keeps its image. The scan runs only when a surface or a plane image was created since the surface's last scan, so steady frames do not scan. The debt entry describes the rule.

Tested. All 701 agc_ tests pass (Linux, RTX 4070 Ti SUPER), and so does the conventions check. In the title, every mode and the switch back render as at launch. Before the fix, the first switch broke them until restart.

No regression test yet. I tried three shapes on the harness, and all pass without the fix:

  • two depth targets of different extents at the same base;
  • a plane inside a wider plane's memory, read by a vertex fetch;
  • the same, read through VulkanDevice::CopyBuffer with the CPU path refused (alias path).

The title also has several surfaces per plane (one per stencil address), HTILE clears on every frame, and the stream-copy kernel going through the driver's packet path. I am working on a test that reproduces it and will add it here.

AI-assisted: yes

…their memory

A depth surface lives in its Vulkan image, so a buffer read of its depth
plane saw guest memory the surface never wrote, and a buffer write into the
plane never reached the surface: binding the target again kept the old
depth.

- A single-sample 32-bit depth surface with the 64 KiB Z swizzle
  (DB_Z_INFO SW_MODE 24) keeps a storage image in that swizzle over its
  plane. A bind that writes or fast clears depth marks the plane stale;
  FlushDepthPlanes copies the depth into the storage image and stores it
  into guest memory. It runs from StorageTexture::FlushPending, when a
  buffer upload finishes (over its regions and address space), and before
  a dispatch for each GuestBuffers V# it binds (a cached dispatch reads
  them without an upload). A reused build flushes its in-place buffer
  regions, and an image lookup flushes the range it reads, as do the
  write-stamp proofs that let a cached image or a reused build skip that
  lookup: a stale plane's memory looks unchanged to them.
- Stale planes are kept as ranges under their own lock with an atomic
  hull; a flush that overlaps none returns without the GPU and surface
  locks.
- Every bind of such a surface, and every flush of its stale plane,
  collects the plane's writes. Written by someone else since the surface's
  previous bind, store or reload, the plane is reloaded from memory
  instead of stored, so a guest write after a depth draw is not
  overwritten by that draw's late store. A pending HTILE fast clear wins
  over the reload.
- A color target flushes the stale planes under it before it retires their
  surfaces, so a target that loads or blends reads the stored depth. A
  retired surface's plane is no longer stale: a later flush never stores
  its old depth over what the new owner of the memory wrote.
- The depth surfaces lock is recursive: a store reached from a flush under
  DepthSurfaceTexture re-enters it.
- Draws pass whether they write depth, so read-only binds do not mark the
  plane stale.

PPSA05684 copies its main depth plane into two ping-pong planes with a
buffer compute, draws the cage's depth into them and fogs its menu from the
result; without this the copy carried nothing and the fog covered the
cliff and the cage.

agc_driver_depth_plane_buffer_memory draws depth 0.5 into such a plane,
fetches vertices from it (the plane must hold 0.5), writes 0.75 into the
memory and draws at 0.5 under LESS (must pass), then checks the depth
written after the reload reaches memory again. On a new plane it writes
memory after a depth draw and before any store: a buffer read must keep
the guest bytes and the next bind must reload them; a guest write before a
depth draw must then lose to that draw. A linear texture at an offset into
a plane must read what memory holds after a flush, also when it is viewed
through its cached storage image after another depth draw. On a plane with
HTILE, an HTILE fast clear after a guest write must win. A color target
that blends ZERO/ONE over a plane just drawn at 0.5 must write back the 0.5
it loaded.

AI-assisted: yes
…verlaps

Each depth surface stores its plane through a storage image of its own,
kept for the surface's life. A title that switches output resolution
binds new depth targets over memory where the previous targets' planes
were, and the old images stayed live there. Storage images over the same
memory lend each other pending results, so a later store of the new
plane did not reach guest memory and buffer reads kept seeing the old
depth. A bind now drops the images and the pending store of older
surfaces whose planes overlap its plane over another range. A
same-range surface (another stencil or HTILE address) keeps its image.
The scan runs only when a surface or plane image was created since the
surface's last scan.

AI-assisted: yes
@XairRrR
XairRrR force-pushed the fix/agc-depth-plane-buffer-coherence branch from c8a8ff1 to 132e69d Compare October 19, 2026 01:58
@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Oct 19, 2026
# Conflicts:
#	core/libs/prx/libSceAgcDriver/Graphics/include/DepthSurface.hpp
#	core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 19, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* docs unblocks title Names a title ID on the Unblocks line

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants