Skip to content

fix(agc): build sampled array views from the slices they read - #7258

Merged
adriadam10 merged 4 commits into
boykopovar:mainfrom
Pipodi:fix/sampled-array-viewed-slices
Oct 19, 2026
Merged

adriadam10 merged 4 commits into
boykopovar:mainfrom
Pipodi:fix/sampled-array-viewed-slices

Conversation

@Pipodi

@Pipodi Pipodi commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

What

A sampled array texture was built from slice 0 to the last slice its T# names, whatever slices the view actually reads. Two cases in Ghost of Yōtei (PPSA26344) made that the largest memory cost of its loading phase:

  • 2D reads of a 2D array. A shader that declares a 2D image only ever sees the base slice of a 2D array T# (the first-layer view). One such T# names 1537 slices of a 3328x1872 R_X surface, so every bind built a 12.4 GiB image (13,346,570,240 bytes). On an RTX 4090 that does not fit the device-local budget. It was rebuilt 5-8 times per hour, and each rebuild asked the texture caches to release about 25 GiB, which stalled 11-14 s.
  • Array views past slice 0. A 256x256 cube array view of slices 4299..7232 was built from slice 0, about 3.8 GB instead of 1.5 GB.

How

  • ViewedArrayWindow (TextureTiling.cpp) rebases a sampled 2D array, 1D array or cube view to its viewed slices, at the guest address of its base slice. It applies only when the window is exact: a thin surface with no DCC metadata and, for a 64 KiB XOR swizzle, a base slice that keeps the equation's slice bits at zero. Anything else keeps the whole array.
  • FirstLayerDescriptor (GuestTextureResource.cpp) narrows a 2D array T# to its base slice. The sampled lookups apply it before the decode, cache key and image are made when the shader shape is 2D. That covers stage A records, stage B bindings, revalidation and image tables. A whole-array read of the same T# keeps its own cache entry.
  • ShaderResources::Revalidate narrows the T# words of an image table element with firstLayerWords before decoding, as the other paths do. Without it the whole array was rebuilt and compared against the narrowed texture, so an image table holding a 2D read of a 2D array never revalidated.
  • A sampled or storage view of 1 GiB or more that is still built whole is logged, with the registered guest range holding its base and how many of its slices fit in it.

This is independent of #6772, which serves storage views past maxImageArrayLayers and refuses cubes and slice-dependent XOR swizzles, and of #6481, which faults image table entries the device or guest memory cannot hold. The 1537-slice image is below the 4090's 2048-layer limit and lies in readable memory, so neither one catches it.

Tested

  • agc_driver_graphics:
    • window offsets for R_X at aligned and unaligned base slices, a standard 64 KiB array, a cube array and a volume;
    • no window at base 0 or with DCC;
    • FirstLayerDescriptor narrowing, including a separate cache entry from the whole array.
  • imageTableFirstLayerRevalidateTests (run by agc_draw_descriptors): an image table holding a 2D read of a 2D array must revalidate on the full proof path and keep its texture across a second revalidation. Without the Revalidate fix it fails with AGC graphics: an image table holding a 2D read of a 2D array did not revalidate.
  • ctest on this branch (main ed01d79 + these commits): 1471 of 1471 pass.
  • Ghost of Yōtei, warm 1-hour runs from the same save on an RTX 4090. These ran on our integration branch, which is main plus local fixes needed to boot the game.
    • run201 is the baseline.
    • run203 adds the first commit.
    • run204 adds both commits.
run201 (baseline) run203 (window) run204 (window + 2D reads)
12.4 GiB allocations 5 8 0
texture cache reclaims (~25 GiB asked) 7 36 0
frames presented in 3600 s 963 951 991
resource builds in 3600 s 187k 176k 232k
textures copied from storage images 30.6k 64.7k 31.8k

The loading phase starts around frame 620, so run204 presented 371 frames in it against 343 for run201 (+8%) and built 24% more resource sets.

Log lines from run204
[gpu] 2D read of the 2D array 0x509c710000 (128x128 format 22, slices 0..255) built from slice 0 alone; 512 such reads
[gpu] sampled view 0x50b90a3000 (256x256 format 71, dimension 3, tile 2, slices 0..2933) covers 1467 MiB; 1 such views
[gpu] view 0x50b90a3000 of 1467 MiB lies in the guest allocation 0x5000000000..0x50d3400000 (3380 MiB, range 0x5000000000..0x50d3400000, rwg): 838 of its 2934 slices fit in the range

Technical debt

Added to docs/dev/debt/libSceAgcDriver.md:

  • Sampled array windows: the conditions for a window, and the cost of keeping two images when a whole-array view of the same surface is also used.
  • 2D reads of 2D arrays: only sampled reads are narrowed; storage images read through a 2D shape still build every slice.
  • Sampled and storage views of 1 GiB or more: these are only logged, not capped to their guest range. The Yōtei cube window has only 838 of its 2934 slices inside its range.

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
  • Fixes: none
  • Unblocks: none
  • GPUs tested: NVIDIA GeForce RTX 4090, driver 617.42, Windows 11 Pro 10.0.26200
  • AI-assisted: yes

🤖 Generated with Claude Code

@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 4449 docs/dev/debt/libSceAgcDriver.md L206
PR 6273 docs/dev/debt/libSceAgcDriver.md L206
PR 6481 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp L4889
PR 6772 core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp L77
core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp L3, L467
core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp L390
PR 6959 docs/dev/debt/libSceAgcDriver.md L206
PR 7154 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp L1058-1060
PR 7579 core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp L77
core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp L467
PR 7746 docs/dev/debt/libSceAgcDriver.md L206
PR 7927 docs/dev/debt/libSceAgcDriver.md L206
PR 8014 docs/dev/debt/libSceAgcDriver.md L206

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/tests/Recorder.cpp
PR 1395 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 2434 docs/dev/debt/libSceAgcDriver.md
PR 4448 docs/dev/debt/libSceAgcDriver.md
PR 5021 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 5058 docs/dev/debt/libSceAgcDriver.md
PR 5499 docs/dev/debt/libSceAgcDriver.md
PR 5925 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6146 docs/dev/debt/libSceAgcDriver.md
PR 6177 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6185 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6186 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6187 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6188 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6192 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6194 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6202 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6209 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6223 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6252 docs/dev/debt/libSceAgcDriver.md
PR 6280 core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6292 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6300 docs/dev/debt/libSceAgcDriver.md
PR 6340 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6548 docs/dev/debt/libSceAgcDriver.md
PR 6549 docs/dev/debt/libSceAgcDriver.md
PR 6557 docs/dev/debt/libSceAgcDriver.md
PR 6615 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6636 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6661 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6682 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6683 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6718 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6727 docs/dev/debt/libSceAgcDriver.md
PR 6747 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6808 docs/dev/debt/libSceAgcDriver.md
PR 6820 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6868 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6937 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6946 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6947 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6970 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6971 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 6972 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6977 core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/TextureTiling.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/TextureTiling.cpp
core/libs/prx/libSceAgcDriver/tests/TextureTiling.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6987 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6989 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6995 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7002 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7048 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7256 docs/dev/debt/libSceAgcDriver.md
PR 7332 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7427 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7446 docs/dev/debt/libSceAgcDriver.md
PR 7449 docs/dev/debt/libSceAgcDriver.md
PR 7466 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
PR 7484 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 7505 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7604 core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7642 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/Recorder.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7668 docs/dev/debt/libSceAgcDriver.md
PR 7800 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7978 docs/dev/debt/libSceAgcDriver.md

@github-actions github-actions Bot added docs agc libSceAgc* labels Oct 17, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 17, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Review of 2b2f060. The window math, the XOR slice guard, the texture-relative view ranges and flush coherence all check out. On a local merge with main, agc_driver_graphics passes, and the PR merges cleanly on top of #7189, #7148 and #7089. One blocking issue:

The image-table revalidation path still builds from the un-narrowed words. The image-table slow path in ShaderResources::Revalidate (ShaderResources.cpp:2959-2963 at this head) still calls cachedTexture(context, words, DecodeTextureResource(words), …) with the raw table words, while resolveImageTables (4340-4367) now binds the narrowed texture. For a table element that is a 2D read of a 2D-array T#, every revalidation that misses the reused-table fast path:

  1. builds or looks up the whole-array image again, which is the large allocation this PR is meant to remove, and
  2. gets a different object from the bound one, so Revalidate returns false and the full resolve runs every time.

On main both paths used the raw words and agreed, so the mismatch is new. Only the binding revalidation (around 2914) is narrowed so far.

Suggested fix: apply the same firstLayerWords(words, images->imageShapes.at(element), narrowed, false) before the decode at 2961. images is the table's DescriptorBinding, which has imageShapes, used the same way at 4328 and 4340. A test that revalidates an image table holding a 2D read of a 2D array would cover it; the current unit tests cover the window math and narrowing but don't reach this path.

@Pipodi
Pipodi force-pushed the fix/sampled-array-viewed-slices branch from 2b2f060 to 0b3603b Compare October 18, 2026 08:12
@Pipodi

Pipodi commented Oct 18, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, you were right: Revalidate rebuilt an image table element from the raw T# words, while resolveImageTables binds the first-layer texture for a 2D read of a 2D array. The whole array was compared against the narrowed texture, so the proof always failed.

Fixed in 0b3603b (branch rebased on main ed01d79): Revalidate now narrows the words with firstLayerWords before decoding, like the other paths.

Added imageTableFirstLayerRevalidateTests, run by agc_draw_descriptors. It requires the table to revalidate on the full proof path and to keep its texture on a second pass. Without the fix it fails with "AGC graphics: an image table holding a 2D read of a 2D array did not revalidate". With it, ctest passes 1471 of 1471.

The debt entry for views of 1 GiB or more now also covers storage views, which are logged too.

AI-assisted: yes (Claude Code)

@github-actions github-actions Bot added needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Oct 18, 2026
@adriadam10 adriadam10 self-assigned this Oct 19, 2026
Pipodi and others added 4 commits October 19, 2026 15:20
A sampled 2D array, 1D array or cube view created and snapshotted the whole
array from slice 0 to its last viewed slice. Ghost of Yotei views six faces
at slice 7227 of a 256x256 cube array (3.8 GB built for 3 MB) and one slice
at 1536 of a 3328x1872 R8 array (12.4 GiB built for 8 MiB).

SampledArrayWindow describes the viewed slices as an array of their own at
the first viewed slice when the layout of those slices does not depend on
their index: a thin array without DCC metadata, and for an XOR swizzle a
first slice that keeps the slice bits the equation mixes in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A shader that declares a 2D image reads only the base slice of a 2D array
T# (the first-layer view), but the texture was built from every slice the
T# names. In Ghost of Yotei (PPSA26344) such a read names 1537 slices of a
3328x1872 R_X surface: a 12.4 GiB image that does not fit the 4090's
device-local budget and stalls 11-14 s on each rebuild.

The sampled lookups (stage A records, stage B bindings, revalidation and
image tables) now narrow such a T# to its base slice before the cache key,
the decode and the image are made, so a whole-array read of the same T#
keeps its own entry. Views of 1 GiB or more that stay whole are logged with
the guest allocation they lie in and how many of their slices fit it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Revalidate rebuilt the cached texture of an image table element from the raw
T# words, while resolveImageTables binds the first-layer texture for a 2D
read of a 2D array. The whole array was rebuilt and compared against the
narrowed texture, so revalidation always failed. Narrow the words with
firstLayerWords before decoding, as the other paths do.

Add imageTableFirstLayerRevalidateTests, run by agc_draw_descriptors: it
fails with "an image table holding a 2D read of a 2D array did not
revalidate" without the fix.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@adriadam10
adriadam10 force-pushed the fix/sampled-array-viewed-slices branch from 0b3603b to a71ba9e Compare October 19, 2026 13:52
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 19, 2026
@adriadam10
adriadam10 merged commit d7e62d8 into boykopovar:main Oct 19, 2026
10 checks passed
@adriadam10

Copy link
Copy Markdown
Collaborator

Rebased onto current main (conflicts in ShaderResources.cpp with the depth-surface retire and multisampleViewType changes, and in the debt file; no logic changes on top of yours). Checked the window math and XOR slice guard, the narrowing at every lookup site, and that the image-table revalidation fix from the last round is in with a test that fails without it. ctest passes after the rebase; merged.

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

Labels

agc libSceAgc* docs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants