Skip to content

fix(agc): drop image stores to a view that starts past the surface's last mip level - #483

Closed
ThisIsAkill wants to merge 1 commit into
boykopovar:mainfrom
ThisIsAkill:fix/agc-empty-mip-view
Closed

ThisIsAkill wants to merge 1 commit into
boykopovar:mainfrom
ThisIsAkill:fix/agc-empty-mip-view

Conversation

@ThisIsAkill

@ThisIsAkill ThisIsAkill commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

What

A T# whose BASE_LEVEL is past its own MAX_MIP names a mip level the resource does not have. DecodeTextureResource threw guest texture descriptor starts past the surface's last mip level.

PPSA21567 (Astro Bot) hits this in a per-level compute chain. For a 1920x1080 surface with 6 levels it creates views of levels 0..6, and every one of them carries MAX_MIP = 5. It then runs one pass of the chain (same shader, same 60x34 grid as the valid levels) that stores into the level-6 view.

There are two ways to handle it; maintainers may prefer the other one:

  • Grow the image (KytyPS5): Kyty allocates max(MAX_MIP + 1, BASE_LEVEL + view levels) host levels, so the level-6 view gets a seventh, host-only level. The title runs there.
  • Drop the stores (this PR): treat the view as covering no level. Stores to it are discarded, and reads still fail.

I chose to drop because growing invents a level that the title's own descriptors say does not exist. Writing that level back could address memory the title never allocated for this surface, and sizing a cached surface from a view rather than from MAX_MIP would change it for every other view of that surface. This also extends a precedent already in DecodeTextureResource: a LAST_LEVEL past MAX_MIP is clamped, because "the hardware never addresses them".

Changes:

  • Recompiler: the image specialization gets an empty flag (non-null T#, not dynamic-mip, BASE_LEVEL > MAX_MIP), part of the variant and disk-cache keys (static_assert sizes updated). The backend compiles image_store to an empty image away. Loads, samples, size queries and atomics on it fail compilation. The T# level fields are read through small named helpers, also used by the two existing sites in ResourceMaterializer.cpp.
  • Driver: DecodeTextureResource returns such a view marked empty and clamped to MAX_MIP. The storage binding uses that existing level as a placeholder that is never written back. Sampled bindings of an empty view still throw (RequireLevelsPresent). Dynamic-mip storage writes to an empty view throw.
  • The discard is logged once per surface and level: [gpu] storage image view of surface 0x... starts at mip 6, past its last mip 5: the shader's stores to it are dropped.
  • TechnicalDebt entry.

Hardware behaviour: I found no AMD documentation for stores through a view whose BASE_LEVEL is past MAX_MIP (dropped or clamped). The RDNA2 ISA reference was not retrievable (its published URLs now redirect to the docs.amd.com portal), and searching turned up only emulator code. So this is a conservative choice, not a documented one. For comparison, stores past a view's extent are already left to the GPU here (image stores carry no bounds check and no robustness feature is enabled), and the title's valid passes at levels 3..5 rely on that.

Tested

  • Linux (CachyOS, GCC 16.2, RTX 3070): ctest 151/151 on current main + this branch.
  • New execution test agc_driver_empty_mip_view (2-level 8x8 R32_UINT surface): a store through the last real level reaches memory (positive control), a store through a level-2 view leaves memory untouched and logs once, and a load through the level-2 view fails to compile. New decode cases in GuestTextureResource.
  • PPSA21567 on Linux, on main with other open PRs and local changes (not clean main): the level-6 views of two surfaces log their dropped stores once each, and the title runs on (about 4 more minutes of rendering) to an unrelated texture error.

Overlap with #425 / #426: this PR is independent of both. The pr-overlap check flags ShaderResources.cpp because the edits touch neighbouring lines (textual overlap only, no shared behaviour). I'll rebase after either one merges.

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 (the dropped store is logged, not silent)
  • No notes, investigation .md files or images added to the repository
  • New third-party code is a submodule built from source (none added)
  • Generic behaviour, not specific to one title
  • Depends on: none
  • AI-assisted: yes (Claude). I reviewed it and take responsibility for every line.

🤖 Generated with Claude Code

…last mip level

A T# whose BASE_LEVEL is past its own MAX_MIP names a level the resource
does not have. PPSA21567 builds per-level views 0..6 of a 6-level
surface (every view carries MAX_MIP = 5) and runs one pass of its
per-level compute chain into the level-6 view; decoding it threw.

Such a view now decodes as empty, clamped to the surface's last level.
The recompiler specializes the image as empty: image stores to it are
compiled out, and loads, samples and atomics fail compilation. The driver
binds the clamped level as a placeholder that is never written back, and
logs once per surface and level that the stores are dropped. Sampled
bindings of an empty view still throw.

The hardware behaviour is not confirmed by AMD documentation; KytyPS5
instead adds the missing level to its host image. Recorded in the
technical debt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 3, 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
#293 core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp L68
#426 core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp L68
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp L513-514, L1641-1642, L2464-2465
#490 core/libs/prx/libSceAgcDriver/Graphics/include/GuestTextureResource.hpp L63
core/libs/prx/libSceAgcDriver/Graphics/src/GuestTextureResource.cpp L132, L166-167
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp L513-516
core/libs/prx/libSceAgcDriver/tests/GuestTextureResource.cpp L271

@github-actions github-actions Bot added shader core/shader docs agc libSceAgc* labels Oct 4, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for writing up both options so clearly. I hit the same chain in PPSA21564 (two 1920x1080 RGBA16F bloom chains, MAX_MIP 5, level 6 stored every frame) and went the other way on my integration branch, because I think the hardware does write that level. On GFX10, MAX_MIP only decides where the levels are placed: a level's address comes from the levels below it, and a level in the mip tail is a fixed slot of the tail block. For this surface addrlib gives the same layout for 6 and 7 levels, with level 6 in tail slot 1, so the store lands inside the surface's own allocation and levels 0 to 5 do not move. Dropping the stores then loses texels the console writes. My version addresses such a view only when extending the chain to the view's last level keeps the allocated levels and the surface size unchanged, keeps one storage image for the surface, and still throws otherwise (linear chains, chains without a tail). It is waiting on #394 because it touches the same tiling files. I can open it so the maintainer can compare the two; what do you think?

@ThisIsAkill

Copy link
Copy Markdown
Contributor Author

You're right, and thanks for explaining it. I checked with Mesa's addrlib (GFX10 path, Addr2ComputeSurfaceInfo; 1920x1080, 64 bpp, SW_64KB_R_X, 2D) for 6 and for 7 levels, under five GB_ADDR_CONFIG settings (Navi10 with 4, 8 and 16 pipes, Navi21, Navi22). They all agree:

  • Levels 0 to 5 have the same offsets either way, and the total size is the same (0x1640000).
  • The mip tail starts at level 5 (64x64) in both layouts.
  • Level 6 (32x32) is a slot in that same tail block (tail offset 0x4000, against 0x8000 for level 5), inside the allocation.

So the title's level-6 stores land in memory the surface already owns, and dropping them loses texels the console writes. Please open your version. I'll close #483 in favour of it once it's up.

@ThisIsAkill

Copy link
Copy Markdown
Contributor Author

Closing in favour of #490, which writes the level-6 view into the mip tail the way the hardware does (verified with addrlib above). Thanks @oneandonlydean for the explanation and for opening it.

@ThisIsAkill ThisIsAkill closed this Oct 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* docs shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants