Skip to content

feat(agc): store to images selected through image tables - #6402

Merged
oneandonlydean merged 4 commits into
boykopovar:mainfrom
Pipodi:feat/image-table-stores
Oct 17, 2026
Merged

oneandonlydean merged 4 commits into
boykopovar:mainfrom
Pipodi:feat/image-table-stores

Conversation

@Pipodi

@Pipodi Pipodi commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What

An IMAGE_STORE whose T# comes from a runtime-indexed image table used to mark the whole table as an unsupported operation. The dispatch's deferred write-back then failed. This follows #757/#758's table model:

  • AssignTables now accepts written, non-atomic table images and gives them storage modes. Atomic, packed, BY2/BY4 and dynamic-mip table stores still fault, as before.
  • A new STORAGE_IMAGE table binding (260). The sampled image table moves to 261 and keeps the variable descriptor count. The runtime ABI goes to v14, so existing shader caches are invalidated.
  • The driver binds storage views of the tracked storage textures at the same element indices. It marks every bound entry as written, so each dispatch writes back all of the table's storage images. Tables with written entries are never reused.
  • The descriptor pool of a set that isn't split counts the storage table's descriptors, as the split path does.
  • A table storage element is checked like a direct one: an element whose minLod clamps above the level it addresses is rejected (it faults when a lane selects it), and the element is looked up as written, so a depth surface under it is retired.
  • RunStoreTests is now a positive test: a lane stores to the key-1 image, while a null-key store and an EXEC-cleared store write nothing. verifyImageTableRegistration expects the bound written elements, a store-only table binds no sampled elements, and image_store_mip, image_store_pck and image_atomic_add through a table still poison the table with an unsupported operation and bind no storage elements.

Native pipeline replay with storage tables, reuse of table layouts across size classes (which also lets storage-table sets back into chain pools), and rejecting the second writer of a depth plane are follow-ups. @Celegans12 will add them on top (#675).

Technical debt

New entry "Image storage tables" in docs/dev/debt/libSceAgcDriver.md:

  • every bound storage element is stored back, so two elements over the same bytes race on write-back order (PPSA26344's 0x80001e5f00 binds the stencil plane 0x505f350000 as R8_UINT and R8_UNORM);
  • the storage table is declared at full capacity, so its sets get dedicated descriptor pools;
  • the capacity is a heuristic (the smallest of the sampled table capacity, 4096 and a quarter of maxPerStageDescriptorStorageImages), and the sampled capacity isn't reduced by it;
  • storage elements at or past that absolute index are rejected;
  • atomic, packed, BY2/BY4 and dynamic-mip table stores still fault;
  • PresyncSurfaces walks only the sampled table;
  • untested cases: mixed sampled and storage tables, a T# both read and written, non-uniform storage indexing, non-2D storage modes.

Tested

Windows 11, NVIDIA GeForce RTX 4090, driver 617.42.

Run This PR Cache "unsupported operation" faults Result
run150 no warm yes: image table: shader 0x80001e5f00 stage 4 pc 0x783c ... unsupported operation all workers stop at frame ~828 (~333 s); private memory 32.6 -> 51.8 GB in a minute
run152 yes cold, 1500 s 0 0x80001e5f00 compiles (1,457,658 words, 63 s) and dispatches; workers keep running until ~1240 s
run156 yes warm, 1800 s 0 0x80001e5f00 v2 dispatched with 150-169 storage images bound (resources 108-343 ms); loading continues to the first in-game scene (~25-30 min)

The game runs predate the review changes, which affect descriptor pool sizing, minLod rejection and depth-surface retirement for table storage elements.

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: PPSA26344 Ghost of Yōtei (PPSA26344) #675
  • GPUs tested: NVIDIA GeForce RTX 4090, driver 617.42, Windows 11
  • AI-assisted: yes

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 6340 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp L4236-4253
PR 6454 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp L4228-4232, L4236
PR 7223 core/libs/prx/libSceAgcDriver/tests/execution/ImageTable.cpp L371

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 907 core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 1341 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 2434 docs/dev/debt/libSceAgcDriver.md
PR 4072 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp
PR 4339 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp
PR 4448 docs/dev/debt/libSceAgcDriver.md
PR 4472 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
PR 4650 core/shader/recompiler/Recompiler.hpp
core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp
docs/dev/debt/libSceAgcDriver.md
PR 5058 docs/dev/debt/libSceAgcDriver.md
PR 5371 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
core/shader/recompiler/RuntimeAbi.hpp
PR 5372 docs/dev/debt/libSceAgcDriver.md
PR 5499 docs/dev/debt/libSceAgcDriver.md
PR 5500 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.hpp
PR 5609 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Recompiler.hpp
PR 5925 core/shader/recompiler/Recompiler.hpp
docs/dev/debt/libSceAgcDriver.md
PR 5958 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
core/shader/recompiler/Optimization/src/ResourceTracker.cpp
core/shader/recompiler/Recompiler.hpp
PR 6146 docs/dev/debt/libSceAgcDriver.md
PR 6177 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.hpp
PR 6188 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/shader/recompiler/Recompiler.hpp
PR 6194 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6222 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.hpp
docs/dev/debt/libSceAgcDriver.md
PR 6234 docs/dev/debt/libSceAgcDriver.md
PR 6235 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Recompiler.hpp
core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp
PR 6273 docs/dev/debt/libSceAgcDriver.md
PR 6300 docs/dev/debt/libSceAgcDriver.md
PR 6373 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6412 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
core/shader/recompiler/Recompiler.hpp
docs/dev/debt/libSceAgcDriver.md
PR 6414 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6445 docs/dev/debt/libSceAgcDriver.md
PR 6539 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Recompiler.hpp
PR 6548 docs/dev/debt/libSceAgcDriver.md
PR 6553 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
PR 6563 docs/dev/debt/libSceAgcDriver.md
PR 6601 docs/dev/debt/libSceAgcDriver.md
PR 6636 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6645 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6647 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6718 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
PR 6727 docs/dev/debt/libSceAgcDriver.md
PR 6808 docs/dev/debt/libSceAgcDriver.md
PR 6887 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
docs/dev/debt/libSceAgcDriver.md
PR 6946 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
core/shader/recompiler/Recompiler.hpp
docs/dev/debt/libSceAgcDriver.md
PR 6947 core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/BindingAllocator.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
core/shader/recompiler/Recompiler.hpp
core/shader/recompiler/RuntimeAbi.hpp
core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvDescriptors.cpp
core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp
docs/dev/debt/libSceAgcDriver.md
PR 6959 core/shader/recompiler/Recompiler.hpp
docs/dev/debt/libSceAgcDriver.md
PR 6965 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
docs/dev/debt/libSceAgcDriver.md
PR 6973 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Recompiler.hpp
docs/dev/debt/libSceAgcDriver.md
PR 6987 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Recompiler.hpp
docs/dev/debt/libSceAgcDriver.md
PR 6989 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Recompiler.hpp
docs/dev/debt/libSceAgcDriver.md
PR 6995 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Recompiler.hpp
docs/dev/debt/libSceAgcDriver.md
PR 7002 core/libs/prx/libSceAgcDriver/Graphics/include/Context.hpp
core/libs/prx/libSceAgcDriver/Graphics/include/ShaderResources.hpp
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp
core/shader/recompiler/Recompiler.hpp
docs/dev/debt/libSceAgcDriver.md
PR 7154 core/libs/prx/libSceAgcDriver/Graphics/src/ShaderResources.cpp
docs/dev/debt/libSceAgcDriver.md
PR 7178 docs/dev/debt/libSceAgcDriver.md
PR 7201 core/shader/recompiler/Recompiler.hpp
core/shader/recompiler/SpirvBackend/src/SpirvMemory/SpirvEmitterHelpers.cpp
PR 7256 docs/dev/debt/libSceAgcDriver.md

@EagleBearerYS

EagleBearerYS commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Heads-up on an interaction with #6243 (selecting the record of a T# whose offset the CPU evaluates), which is still open. The two merge without conflicts, but on a merge of both, agc_shader_bindless fails: #6243's selected record direct operations case expects an image_store through a T# keyed per lane to stay an unsupported table operation, and this PR makes that store supported.

With that one expectation changed to TableOperation::Supported for image_store (atomics, PCK and BY2/BY4 keyed per lane still fault, as both PRs expect), the merge passes agc_shader_bindless, agc_driver_image_table and the other shader-memory tests 5 runs in a row on GitHub's Linux and Windows runners. ctest -R '^agc' on the merge fails nothing that main doesn't. A store whose record the CPU evaluates stays a direct binding under #6243; this PR's storage table takes the stores keyed per lane.

Whichever of the two merges second needs that one-line test change. If this one lands first, I'll make it on #6243.

@Celegans12

Copy link
Copy Markdown
Contributor

@Pipodi I went through this against our local store work, and the model fits #757/#758 nicely. Keeping one element domain with the storage element in 260 and the sampled one in 261, faulting only on use for poisoned or rejected elements, and never reusing tables with written entries are all what I'd have done. A few things I think need fixing before it goes in, plus some I'd be fine leaving to a follow-up.

The two I'd treat as blocking:

  1. The descriptor pool has no room for the storage table on the non-split path (compute, so 0x80001e5f00 included). Binding 260 is declared with StorageTableCapacity descriptors (up to 4096), but the pool sizes at ShaderResources.cpp:1802 still only count plannedStorageImages. The split path (:1741) and draw snapshots count the table, and this one doesn't. NVIDIA doesn't enforce per-type pool counts, so our runs won't show it, but a driver that does will fail the allocation. Once the count is right, a 4096-entry set no longer fits a 1024-image chain pool either, so every build would get its own pool. That's why ours sized 260 from the element count in size classes.
  2. No test still checks that the unsupported table stores fault. The old negative store test became the positive one, so atomic, packed, BY2/BY4 and dynamic-mip table stores aren't covered any more.

Smaller ones:

  1. The table path doesn't do the direct path's minLod check (:4072), so a T# whose minLod clamps above the addressed level gets written anyway. A per-element rejectTableElement would match the direct path.
  2. The element is looked up with cachedStorageTexture(..., guestBytes) (so written is false and RetireDepthSurfaces never runs) but pushed with storageWritten = true (:4289 vs :4297). The direct path keeps the two in step. Either pass true, or reject elements over a depth surface explicitly.
  3. Depth/stencil planes through a table. This one we hit in Yōtei. 0x80001e5f00's storage table holds the stencil plane 0x505f350000 twice: as R8_UINT in its own tile mode, and as R8_UNORM in another. With every element marked written, that's two writers of the same bytes, and write-back order decides the result. On our branch we reject the second writer per element, so the dispatch only faults if a lane actually selects it. That's fine as a follow-up from us if you'd rather keep this PR focused, but a TechnicalDebt line here would help.

Happy to send the depth-plane part and our two extras (pipeline replay with storage tables, and 260 sized in classes of the element count so 233-element sets stay in chain pools) on top once this settles. I'll run it on Yōtei as part of our next game build.

Details and notes
  • Performance, as a design note rather than a regression, since ours does the same: marking every bound element written means each 0x80001e5f00 dispatch dirties all 150-233 storage images of the table, though only the three stores at 0x783c/0x785c/0x7868 write. Any remembered sampled table overlapping one of them then fails TableCurrent and can't be reused, which matters with Yōtei's 33k-entry sampled tables. A profile split of your 108-343 ms resource time (lookups vs write-back) would show whether it bites. Per-element written bits from the GPU would be the exact fix later.
  • PresyncSurfaces (:4441-4449) walks ImageTable only and applies the sampled rule to every element, so a store-only table isn't considered.
  • StorageTableCapacity is a heuristic (maxPerStageDescriptorStorageImages / 4, max 4096), and ImageTableCapacity doesn't subtract it, so ValidateDescriptorLayout could throw on devices with tight limits.
  • Storage elements at absolute index ≥ 4096 are rejected even when only a few are storage (:4281-4283). That's fine for 0x80001e5f00 (about 233 elements), but mixed tables with many sampled elements first would hit it.
  • Untested: mixed sampled+storage tables, read and write of the same T#, non-uniform storage indexing, non-2D storage modes (newly allowed), and that the null-key run doesn't fault.

@Pipodi

Pipodi commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

@Celegans12 thanks for the careful pass. Pushed 085fa565b on top of a rebase onto current main:

  1. Pool sizes on the non-split path: the set's pool now counts the storage table binding's descriptors, as the split path does. As you said, a set declared with the full StorageTableCapacity no longer fits a 1024-image chain pool, so those sets get a dedicated pool for now. I noted that in the debt entry. Your size classes of the element count are the real fix, and I'd be glad to take them as your follow-up together with the native replay.
  2. Negative coverage: verifyImageTableRegistration again checks that image_store_mip (dynamic mip), image_store_pck and image_atomic_add through a table stay TableOperation::Unsupported, poison the table with UnsupportedOperation so it faults on use, and bind no storage elements. I didn't add BY2/BY4: I don't have a hand-encoded instruction for them that I trust, so if your branch has one, I'm happy to add it.
  3. minLod: a storage table element whose minLod clamps above the level it addresses is now rejected per element with rejectTableElement, the same rule the direct path enforces with Require.
  4. written vs storageWritten: the element is now looked up with written = true, so RetireDepthSurfaces runs as it does on the direct path.
  5. Stencil plane through two elements: left for your follow-up as you offered. There's a TechnicalDebt entry under libSceAgcDriver that names 0x80001e5f00 and 0x505f350000 and the write-back order problem.

The limits in your details block are in the same debt entry now too, along with the ones I should have listed in the first place: the storage capacity heuristic and that ImageTableCapacity doesn't subtract it, the absolute-index rejection, PresyncSurfaces walking only the sampled table, the dedicated pools, and the untested cases (mixed tables, read+write of one T#, non-uniform storage indexing, non-2D modes). I'd keep the code changes for those out of this PR unless you think one of them blocks.

Tested on 9161151 (rebased onto main 92e5041): Windows 11, MinGW-w64 GCC 15.2.0, RTX 4090. Full BUILD_TESTING=ON build, then ctest: 1294/1294 pass.

One thing the new check caught: image_store_mip through a table is still planned as Supported with ImageMipMode::DynamicStorage, and only ApplyStaticInterface refuses it. So the test accepts either the plan's Unsupported or a dynamic mip, and then requires the poisoned map and no storage elements in the compiled result.

@Dextroid17

Copy link
Copy Markdown
Contributor

Tested on Linux with lavapipe, the CI device: llvmpipe (Mesa 26.2.4, LLVM 23.1.1), CachyOS, GCC 16.2.1, CMake 4.4.3, default build type, full build with BUILD_TESTING=ON. Each run used a clean environment (env -i with HOME, PATH, VK_ICD_FILENAMES and GALLIUM_OVERRIDE_CPU_CAPS=avx).

This PR (9161151ef merged into main 44df21632, no conflicts):

  • agc_driver_image_table and agc_shader_bindless: pass, 3 runs out of 3.
  • I could not run a check with the fix reverted: ImageTable.cpp uses DescriptorRole::ImageStorageTable, which this PR adds, so the test does not build against main's sources.

Merged together with #6416 (on main 44df21632, no conflicts): full ctest: 2 of 1269 fail, 57 skipped (the same skips as main). The two failures are not from either PR:

  • agc_driver_mesh_index_cache segfaults on main too (and 5 runs out of 5 on the merge).
  • agc_frame_dump_io (from fix(agc): disable optional BMP dumps after host I/O failures #5800) also fails on plain main, in 13 of 200 runs on an idle machine: closed pipe pixel write was accepted. Reading the test, its 128×128×4 = 65,536 pixel bytes are exactly Linux's default pipe buffer, so if the reader drains the 54-byte header before the pixel write starts, the empty pipe takes the whole write and it succeeds before the reader closes. Not measured beyond that.

Apart from those, the failed and skipped lists match main's full run.

Not tested: NVIDIA, AMD, Intel, PPSA26344.

AI-assisted: yes

@SleglJan

Copy link
Copy Markdown
Contributor

Data from 007 First Light (PPSA11386 v01.018.000), a title that stores to images selected through image tables in compute.

Setup: Linux 7.2.10, RTX 3080 10 GB, NVIDIA 615.78.08, GCC 16.2.1, Release, bubblewrap sandbox. Stack: main 4186f06 plus open PRs (#5987, #6553, #6465, #6868 and the image-table PRs #6440 and #6567 among them), unpublished local fixes the title needs, local logging and scripted input, and the title's two feedback-wait limits widened in its executable as described in #6172 (local only). This head's three commits (9161151) cherry-picked cleanly onto it. Compared with a run of the same stack without this PR, same save and switches (APS5_HOST_IMPORT_MIB=12288, APS5_RESOURCE_CACHE_ENTRIES=64, APS5_NO_DRAW_SRT_ENTRIES=1, a guest fault dump).

Without this PR, the title's compute dispatches fail their deferred write-back at five program sites with deferred write-back failed: image table: … unsupported operation. The failed fault check also skips that dispatch's CPU write-back of copied buffer results; buffers the GPU writes in place are not affected. That path is the same on main and this PR doesn't change it. Counts per site, in the same order in every cell; this stack prints the line once per failure:

boot, menus and loading to present 1430 end of run
without this PR 877 / 2 / 2 / 4 / 2 1349 / 190 / 190 / 116 / 11 1369 / 978 / 978 / 121 / 11 (present 2218)
with this PR 0 / 0 / 0 / 0 / 2 0 / 0 / 0 / 0 / 11 0 / 0 / 0 / 0 / 11 (present 1586)
  • Present numbers differ between the runs from the loading screen on. The frame rate drops at the end of loading at present 1243 without this PR and at 1219 with it. The first column ends at that drop in each run, and it is the like-for-like one.
  • Four of the five sites no longer fail.
  • The fifth fails 11 times in both runs, at the same point: twice just before that drop, then once per present for the next nine presents.
    • With this PR its message is rejected by the driver: the image storage table exceeds its capacity of 4096 images.
    • That message is printed both when the set has no storage-table binding and when the element's index is at or above the capacity. I haven't split the two cases yet.
  • No stop and no guest fault in either run. The run without this PR used its full 7,200 s budget; I ended the run with this PR at present 1586.
  • Not comparable between the runs after loading: the player's movement.
    • The run with this PR used a newer input script whose stick input never reached the title, so the player stood still. Without this PR the player walked until he got stuck at about present 1430.
    • So the later columns and the window-title FPS over presents 1244–1586 (mean 0.15 without this PR, 0.18 with it) cover different scenes.
  • The run with this PR started from a cold shader cache, because this PR changes recompiler sources and with them the cache's source version.
  • Not run: ctest on this machine; Dextroid17's comment above has lavapipe.

AI-assisted: yes (Claude Code)

@Pipodi

Pipodi commented Oct 16, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, that's the first run on a title that stores through image tables: four of the five sites going from hundreds of failures to zero is what this PR is for.

You're right about the fifth site's message. It comes from one check in ShaderResources.cpp that covers two different failures:

  1. The set has no storage-table binding, although the shader writes through that table element. The storage-table binding is added only for a set where some table is marked as a store table at recompile time. So this case would mean the recompiler's record and the element's written flag disagree.
  2. The element's index is at or above the storage capacity, min(image table capacity, 4096, maxPerStageDescriptorStorageImages / 4), so 4096 on your 3080. The index is the element's position in the whole set: every table element before it in the same set counts, sampled ones included. So a small storage table can still land past 4096 if a large sampled table comes first in the set.

The two need different fixes. Case 2 means giving storage tables their own index space; case 1 is a recompiler bug. 0229b5f (pushed here) gives each case its own message:

  • set N has no image storage table, but the shader stores to its table element I
  • the stored table element I of set N is past the image storage table's capacity of C images

It changes only those messages and corrects the debt entry, which said the index counts within the table rather than the set. It cherry-picks on its own, so a run with it would show which case your eleven failures are.

AI-assisted: yes (Claude Code)

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Reviewed at 0229b5f. Looks good. Two small things for your next rebase:

  1. main is already at RuntimeAbi v13 (f7b2ebd), so the 12 -> 13 bump here merges silently and the binding move (storage table 260, sampled table 261) would ship under the same v13. Could you bump it to v14 when you rebase?
  2. perf(shader): select the record of a T# whose offset the CPU evaluates #6243 also touches image-table stores. Whichever of feat(agc): store to images selected through image tables #6402 and perf(shader): select the record of a T# whose offset the CPU evaluates #6243 lands second needs to flip one agc_shader_bindless expectation to Supported.

Full CI is running on this head. It'll also go through our game gates before merge.

@oneandonlydean oneandonlydean self-assigned this Oct 17, 2026
Pipodi and others added 4 commits October 17, 2026 08:45
An IMAGE_STORE whose T# comes from a runtime-indexed image table used to
poison the whole table as an unsupported operation. Written, non-atomic
table images now get storage modes, a STORAGE_IMAGE table binding (260,
the sampled image table moves to 261 and keeps the variable count), and
storage views of tracked storage textures at the same element indices.
Every bound entry is marked written, so each dispatch writes back all of
the table's storage images. Atomic, packed, BY2/BY4 and dynamic-mip table
stores still fault. Tables with written entries are not reused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… elements

The shader memory registration test pinned the old refusal. It now checks
that the store's table is supported, does not fault, and binds every
element as written on the storage table binding, and that a store-only
table binds no sampled elements. The driver no longer requires a written
flag per table element, so bindings that carry none read as unwritten.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lements like direct ones

The non-split descriptor set's pool now counts the storage table binding's
descriptors, as the split path already did. A table storage element whose
minimum LOD clamps above the level it addresses is rejected per element, as
the direct path refuses it, and the element is looked up as written so a depth
surface under it is retired. The registration test checks again that
image_store_mip, image_store_pck and image_atomic_add through a table stay
unsupported operations that fault on use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…st its capacity

Both cases printed that the image storage table exceeds its capacity. They need different fixes, so each now has its own message with the set, the element's index in the set and the capacity. The debt entry now says that the index counts every table element before it in the set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Pipodi
Pipodi force-pushed the feat/image-table-stores branch from 0229b5f to b7e21ca Compare October 17, 2026 07:01
@Pipodi

Pipodi commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

@oneandonlydean thanks. Rebased onto main 6e7ba92 and bumped the runtime ABI to v14 in the commit that moves the bindings, so no commit ships the 260/261 layout under v13. The description says v14 now.

On #6243: agreed. EagleBearerYS offered to make the change on #6243 if this one lands first. If #6243 lands first, I'll flip the image_store expectation in agc_shader_bindless to Supported here.

Tested on b7e21ca: full BUILD_TESTING=ON build, then ctest: 1364/1366. agc_driver_pixel_lds also fails on plain main 6e7ba92 on this machine (a fence timeout in the wave64 unbounded case). select_tests failed only in the parallel run and passes alone.

AI-assisted: yes (Claude Code)

@oneandonlydean
oneandonlydean merged commit 8f0e276 into boykopovar:main Oct 17, 2026
8 of 9 checks passed
oneandonlydean pushed a commit that referenced this pull request Oct 18, 2026
Main's #6402 runs image stores through an image table, so a T# keyed
per lane keeps its table with a supported image_store and an
unsupported mark for the other operations.
adriadam10 pushed a commit that referenced this pull request Oct 19, 2026
Main's #6402 stores to image tables: a table with a written entry takes
the fresh path, as it does on main, and so does one without a sampled
table binding, whose entries the fresh path rejects. The shared place
step uses the binding as a pointer. Main's #6614 frees descriptor sets
from shared pools one by one, while this branch keeps only dedicated
pools, so both stay. #7101's bulk key read counts its unmapped-page
skips as unmapped, and its key words sit next to this branch's write
stamp and compared runs.
adriadam10 pushed a commit that referenced this pull request Oct 19, 2026
#6243 now expects a per-lane table image store to be supported, since
main's #6402 runs image stores through image tables. This branch
carries #6243's test, so it takes the same expectation.
TheBlenDed pushed a commit to TheBlenDed/AnyPS5 that referenced this pull request Oct 25, 2026
Brings combo19 (async lane, gpuind, chain-A, 1a f32 mix, boykopovar#7489, S1,
bindless images, staging diff, HTILE work, epoch 5) up to upstream main
of 2026-10-19. 118 conflicted files, resolved by area; notes in
work/research/structural/merge19/notes-*.md.

- Versions: FormatVersion 48, RuntimeAbi 18, request serialization 37
  (upstream's 31..36 as upstream writes them, our single-workgroup flag
  is 37, gpuind's trailing block last; includes b75c4e6's fix).
- Image tables: upstream's boykopovar#6454 reuse model, scratch indexes, selected
  records and pointer chains, with our bindless columns and resolve memo
  in front.
- Pairs kept with ours as the default: boykopovar#6210 presenter epoch only with
  APS5_PRESENT_EPOCH=1; boykopovar#6402 table stores (our path); pending HTILE
  fills only with APS5_PENDING_HTILE_FILLS=1. boykopovar#6208 takes upstream's
  final code with DMA pipelined by default (APS5_NO_PIPELINE_DMA=1 off).

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

6 participants