Skip to content

fix(agc): store a written storage image over a depth plane into the surface - #5113

Merged
adriadam10 merged 2 commits into
boykopovar:mainfrom
XairRrR:fix/agc-depth-store-back-any-storage
Oct 14, 2026
Merged

adriadam10 merged 2 commits into
boykopovar:mainfrom
XairRrR:fix/agc-depth-store-back-any-storage

Conversation

@XairRrR

@XairRrR XairRrR commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What

DepthSurfaceStoreBack only took the storage image a depth surface had handed out for its plane (DepthSurfaceStorage). A storage image over the same plane that the surface did not hand out was written but never reached the depth image, so later depth tests and plane reads used the surface's old contents. For example:

  • an image created and cached before the depth surface existed;
  • an image a dispatch keeps because its resources revalidate from their records.

Changes in Graphics/src/DepthSurface.cpp:

  • A written storage image that exactly covers a depth surface's depth or stencil plane is now copied into that plane when it is stored back, the same way the surface's own view is. "Exactly" means the plane's address and extent, level 0, one 2D layer (or a one-layer 2D array at slice 0), and the plane's 32-bit float, 16-bit unorm or 8-bit uint format.
  • The surface's write serial moves, so plane copies refresh.
  • A view that the surface's own view replaced still throws, as before.
  • copyPlane takes the storage image to copy from or to, instead of always the plane's own one.

Test in tests/execution/DepthStorage.cpp:

  • a storage image created over the depth plane with StorageTexture::Create, filled with 0.125 and stored back, reaches the depth surface (read back through the surface's own storage view);
  • a storage image of another extent over the same memory is not taken.

The first check fails on main.

Uncharted: Legacy of Thieves Collection (PPSA05684) writes a 640x360 downsampled depth with a compute shader, then tests its mist particles against that memory as a depth target. The storage image was created before the depth surface, and the dispatch kept reusing it, so the depth target stayed at its clear value and the mist drew over the foreground cage in the main menu. On that title's branch, an equivalent local change (making such a written storage image the plane's writer, taken at the next bind) puts the mist behind the cage. That branch carries other depth work, so this PR's exact code was verified by the test, not in the title.

Related: #454 (retiring a surface when another writer covers it) and #5003 (2D array storage views of a depth plane) also touch storage access to depth planes. Neither stores an outside view back into the plane.

Tested

  • Arch Linux (kernel 7.2.9), GCC 16.2.1, NVIDIA RTX 4070 Ti SUPER (driver 615.78.08), Release with BUILD_TESTING=ON.
  • Branch built on main ff8fe4d. Full ctest: 1041/1041. One run hit an unrelated failure in guest_kernel_fcntl, which passed when rerun. Conventions check passed.
  • Title: Uncharted: Legacy of Thieves Collection (PPSA05684), main menu, on its own bring-up branch, because plain main does not run it yet.

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 (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
  • AI-assisted: yes

@kr4y4

kr4y4 commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

First Windows and Intel run.

Setup: Windows 11 (26200), WinLibs GCC 15.2.0 r7, Release, CI configure flags, an RTX 4070 Laptop GPU (NVIDIA 595.97) and an Intel Raptor Lake-S iGPU (32.0.101.6790). The head dfc28bd merged into main 0f2683c, which passes 1069 of 1069 on both GPUs.

  • agc_driver_depth_storage, --repeat until-fail:3: passes on both GPUs.
  • Full suite: 1069 of 1069 on both GPUs.
  • With DepthSurface.cpp from main, agc_driver_depth_storage fails on both GPUs with AGC graphics: a storage image over the depth plane that the depth surface did not hand out was not stored back.
  • It merges cleanly with fix(agc): give each slice of a depth array its own HTILE #5111.

Written with AI assistance.

@nikowolf123

Copy link
Copy Markdown
Contributor

DepthSurfaceStoreBack goes through surfaces() oldest first and stops at the first surface whose StoreBack returns true (DepthSurface.cpp L712-L718). The new CoversPlane loop (L204-L211) lets an older surface with the same depth address, extent and format take the copy before the loop reaches the surface that handed the view out.

Such surfaces exist when one depth plane is bound with different stencil planes, because sameSurface includes the stencil address in its key (L460-L462). #4683 reports that PPSA05684 binds one depth plane with three stencil planes. DepthSurfaceTexture (L609), DepthSurfaceAt (L690) and DepthSurfaceStorage (L706) all resolve a plane address to the newest surface (rbegin), so they never see a copy made into an older one.

Both of these work on main and break with this PR:

  1. A surface writes through its own storage view and stores it back. The copy goes into the older pairing, and the pairing that handed the view out keeps its old depth.
  2. Storing back through a replaced view no longer throws when an older pairing exists. The older pairing's CoversPlane accepts the view before the owning surface's replaced check runs. The PR description says this case still throws.

An outside image takes the same loop, so it also lands in the oldest pairing.

Repro: a copy of tests/execution/DepthStorage.cpp with this Run:

const DepthTarget first{address, address + BlockBytes / 2, {Side, Side}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.25f, 7};
const DepthTarget second{address, address + BlockBytes / 2 + 8192, {Side, Side}, VK_FORMAT_D32_SFLOAT_S8_UINT, 0.5f, 3};
DepthSurfaceView(context, first);
DepthSurfaceView(context, second);
const auto depth = DepthSurfaceStorage(context, {}, PlaneResource(address, Format32Float), 0); // second's view, reads 0.5
fill(*depth, 0.75f);
DepthSurfaceStoreBack(context, *depth);
// read back through DepthSurfaceStorage(...) again and count 0.75
auto layered = PlaneResource(address, Format32Float);
layered.dimension = TextureDimension::k2DArray;
DepthSurfaceStorage(context, {}, layered, 0); // replaces `depth` on the second pairing
DepthSurfaceStoreBack(context, *depth);       // should throw

Results (WSL2 Ubuntu, GCC 15.2, CI CMake configuration, Release, lavapipe):

  • main fdefd76: the read back has 4096 texels of 0.75. The second store back throws ... through a view that another view of the plane ... replaced are not implemented.
  • This PR merged onto fdefd76: the read back has 4096 texels of 0.5 (the second pairing's clear value). The second store back returns true and does not throw.
  • The PR's own agc_driver_depth_storage passes on the merge.

A fix I tried locally: first run the identity and replaced checks on every surface, then run the CoversPlane copy newest first, in the same order DepthSurfaceStorage uses:

for (const auto& surface : surfaces())
    if (surface->context.device == context.device && surface->StoreBack(storage)) return true; // own view, or throw for a replaced one
for (auto it = surfaces().rbegin(); it != surfaces().rend(); ++it)
    if ((*it)->context.device == context.device && (*it)->StoreBackCovering(storage)) return true; // the new CoversPlane loop, moved out of StoreBack

With that change (+8 lines), the PR's test and the repro both pass.

AI-assisted: written with Claude Code; I checked the code paths and ran what is described above.

@XairRrR

XairRrR commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

@nikowolf123 thanks, you're right on both points. I pushed 6d1893f with your fix:

  • StoreBack now only takes the surface's own view or throws for a view that another view replaced, and that check runs over every surface first.
  • The covering copy moved to StoreBackCovering, which DepthSurfaceStoreBack calls newest first, in the order DepthSurfaceStorage resolves a plane.

Your repro is now the RunPairings pass of tests/execution/DepthStorage.cpp: one depth plane bound with two stencil planes, a store back through the newest pairing's view, a read back through DepthSurfaceStorage, and a store back through the replaced view that must still throw.

  • With 6d1893f, agc_driver_depth_storage passes.
  • With only DepthSurface.cpp reverted to dfc28bd, it fails with a storage image stored back went into an older pairing instead of the pairing that handed it out.
  • On the branch merged into main 0214069 (Linux, RTX GPU, NVIDIA driver, Release), all 487 agc tests pass (agc_driver_stencil_ref_export is skipped), and check_conventions.py passes.

AI-assisted: yes

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 14, 2026
RINNECODER added a commit to RINNECODER/AnyPS5 that referenced this pull request Oct 14, 2026
#484)

* fix(agc): sample 10_10_10_2 SNorm and SInt textures

Guest texture formats 51 (SNorm) and 55 (SInt) of the 10-bit packed group
were missing from the texture lookup and the descriptor format table, so a
descriptor using them threw 'unsupported guest texture format' while the
UNorm (50) and UInt (54) siblings worked. Map them to
A2B10G10R10_SNORM_PACK32 / A2B10G10R10_SINT_PACK32, classify them for
sampling (float / signed integer), and list SINT among the integer
formats. TextureFormat.cpp and RdnaDescriptorFormat.cpp are shared with
our Metal path.

Metal has no signed RGB10A2 pixel format, so MetalTexture still refuses
these two formats, now by name ("no supported pixel format for guest
format 51") instead of failing the shared lookup. Recorded in
TechnicalDebt.

Test: metal_utility_tests (TextureResource.mm) checks that 51 and 55
resolve to the signed packings, are four bytes and not block compressed,
and that a native texture refuses them by name. The upstream
RunTextureFormatTests cases (tests/TextureFormat.cpp) are carried too;
they build in the Vulkan graphics tests, not on macOS.

Ported from upstream PR boykopovar#5149
(cherry picked from commit c6a4223)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgW4n3qYgi5jAAohEmXRQj

* fix(agc): share a depth plane's contents across its stencil pairings on Metal

A title can bind one depth plane with several stencil planes (or one
stencil plane with several depth planes). MetalDepthSurfaceCache keys a
surface by both plane addresses, so each pairing got its own textures,
cleared on creation: depth written through one pairing never reached the
others, and later draws tested against stale depth.

The cache now records, per depth and per stencil plane address, the
surface that bound it last. Binding a single-sample surface without
DB_DEPTH_VIEW slices takes each of its planes from that surface when both
have the same format and extent: a blit copies the texture layer, or, for
a combined Depth32Float_Stencil8 texture, only that plane's aspect through
a buffer. The blit is committed on the backend's queue, after the passes
that wrote the other surface and before the draw that binds this one.
Multisampled surfaces and depth-array slices keep their own planes, as
before. Clear() forgets the owners.

Upstream does this in the Vulkan DepthSurface.cpp (vkCmdCopyImage, plus a
pending fast clear carried over); our Metal path has no HTILE fast-clear
state, so only the copy applies.

Test: metal_utility_tests (DepthResource.mm, sharedPlanes) binds a depth
plane with a second stencil plane, binds the same pairing again, goes
back to the first pairing, binds the stencil plane with another depth
plane and binds a pairing of another extent, for D16S8 and D32S8. It
fails without this change ("Sampled depth plane returned incorrect GPU
values").

Ported from upstream PR boykopovar#4683

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgW4n3qYgi5jAAohEmXRQj

* fix(agc): retire a Metal depth surface once another writer covers its memory

A depth surface lives only in its Metal textures: nothing writes it back
to guest memory. When a color target or another depth target is later
bound over its memory, the surface still answered every view of that
address, so a texture of another layout there threw ("resident depth
view is incompatible") and a matching one read the dead depth.

The surface is now retired by the last writer:
- binding a color target drops the depth surfaces whose depth or stencil
  plane it overlaps from the draw's resident surfaces (the ones views
  and depth-array gathers search);
- binding a depth target moves its surface last and retires the other
  surfaces its planes overlap: another stencil pairing of its depth
  plane, or a surface of another extent at its address. The lookup then
  finds the surface bound last, not the one bound first.
The cache keeps a retired surface; binding its target again brings it
back with the texels it held (with boykopovar#4683's change, a plane another
pairing wrote since is copied over first).

Not ported: upstream also retires a surface on a written storage image
over its planes and, with write watching, on CPU or driver stores. Our
Metal path has no storage view of a depth plane (a storage image at its
address throws) and no write tracking for this cache, so those cases keep
throwing. Recorded in TechnicalDebt.

Test: metal_utility_tests (DepthResource.mm, retiredDepth): writes next
to the planes retire nothing; the surface being bound is kept; a writer
inside the depth or stencil plane retires it; the cache still returns it;
binding a depth target of another extent at the address retires the
other surface.

Ported from upstream PR #454

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgW4n3qYgi5jAAohEmXRQj

* docs(agc): record the Metal depth-plane views and storage writes not modeled

Upstream boykopovar#4722, boykopovar#4873, boykopovar#5054, boykopovar#5113 and boykopovar#5446 fix the Vulkan depth-plane
copy (padded detiled rows and slices, a cube with a mip chain) and the
storage writes over a depth plane. Our Metal path has neither: a resident
depth surface is served only as a single-level 2D view or a 2D array of
its extent, and a storage image over its plane is bound to guest memory.
Record that in TechnicalDebt instead of porting the fixes.

Refs upstream-pr-batch-1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DgW4n3qYgi5jAAohEmXRQj

---------

Co-authored-by: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
@adriadam10 adriadam10 self-assigned this Oct 14, 2026
…urface

DepthSurfaceStoreBack took only the storage image a depth surface had
handed out for its plane. A storage image over the same plane that the
surface did not hand out (one cached before the surface existed, kept by
a dispatch whose resources revalidate from their records) was written
and never reached the depth image: later depth tests and plane reads
used the surface's old contents.

A written storage image that covers a depth surface's depth or stencil
plane exactly (the plane's address and extent, level 0, one 2D layer,
the plane's 32-bit float, 16-bit unorm or 8-bit uint format) is now
copied into that plane when it is stored back, as the surface's own view
is. A view the surface's own view replaced still throws.

Uncharted: Legacy of Thieves Collection (PPSA05684) writes a 640x360
downsampled depth with a compute shader and then tests its mist
particles against it as a depth target; the depth target stayed at its
clear and the mist drew over foreground geometry.
…ts plane

DepthSurfaceStoreBack walked the surfaces oldest first and let the first
one whose plane the storage image covers take the copy. A depth plane
bound with several stencil planes has one surface per pairing, so the
copy could land in an older pairing while DepthSurfaceStorage,
DepthSurfaceTexture and DepthSurfaceAt read the newest one, and a store
back through a replaced view was accepted by an older pairing instead of
throwing.

The surface's own view and the replaced views are now checked on every
surface first; only then does the newest surface covering the plane take
the copy, the order DepthSurfaceStorage resolves a plane in. Reported
with the repro and the fix by nikowolf123; the repro is the new
RunPairings test.

AI-assisted: yes
@adriadam10

Copy link
Copy Markdown
Collaborator

Rebased onto current main (one conflict in tests/execution/DepthStorage.cpp, next to the new layered-view checks; kept both). Checked: agc_driver_depth_storage* pass, the new check fails with DepthSurface.cpp from main, the pairing order fix from the earlier review is covered by RunPairings, and the rest of the scoped ctest passes except agc_driver_tessellation, which crashes inside RADV's shader compiler and doesn't touch this code. Windows/Intel run by @kr4y4 also green. Merging once the full Build is green.

@adriadam10
adriadam10 force-pushed the fix/agc-depth-store-back-any-storage branch from 6d1893f to b3e5acc Compare October 14, 2026 17:57
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 14, 2026
@adriadam10
adriadam10 merged commit 32e810d into boykopovar:main Oct 14, 2026
8 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.

4 participants