Skip to content

feat(agc): implement image and sampler tables indexed at runtime offsets - #757

Merged
adriadam10 merged 1 commit into
boykopovar:mainfrom
Celegans12:feat/agc-image-tables
Oct 14, 2026
Merged

adriadam10 merged 1 commit into
boykopovar:mainfrom
Celegans12:feat/agc-image-tables

Conversation

@Celegans12

@Celegans12 Celegans12 commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

What

Image and sampler descriptors that s_buffer_load reads at runtime key * stride + addend offsets fail planning (GetImageResource dword 0 is not a valid runtime value). This snapshots such tables per invocation and selects the entry in the shader; table contents stay out of shader, pipeline and disk-cache keys.

  • An entry that can't be bound exactly reads zeros and faults only when an active lane selects it: unsupported formats, MSAA, border/cube/ANISO_OVERRIDE pairings, and a T# whose surface has no mapped byte.
  • Reads of one heap with one record shape share a column, narrowed to the records its keys select when every key is a V#-relative read or an SRT-walker value (@SleglJan's changes); loop-carried, lane-reduced and (key << n) & mask keys (@tibzejoker's fix) keep the whole table. Records are read in runs (@SleglJan's ShaderMemory::readWords).
  • Fragment tables use the fragment set when pipeline libraries split it; empty tables reserve one descriptor.
  • The resource cache keeps at most 65,536 table elements and the dispatch cache 256 MiB of captured words, evicting least recently used (@tibzejoker's memory report).
  • Documented in docs/dev/ARCHITECTURE.md; runtime ABI 12, disk format 32 (stored plans carry table columns); limits in docs/dev/debt.

#758 adds tables loaded through 64-bit pointers. Table FMASK entries still fault. #5461 and #5472 also change the disk format, and #5620 extends the bindless-table path this replaces. #5550's T# that is both queried and sampled is one column here (its test is ported), as is #5188's T# read at a user-data offset, whose test now checks the record each key selects. #4783's key reservation and #5408's key-miss trace count the image shapes this adds to content keys.

Tested

Based on main 066b58f. Windows 11 (MinGW-w64 GCC 15.2.0) and Linux: configure -DBUILD_TESTING=ON, full build, --target libs, ctest.

agc_driver_image_table, _cache, _reuse, _cache_bounds (new: both bounds), agc_driver_bindless_images, agc_shader_bindless (new: shared keyed columns, masked offsets, #5550's queried and sampled T#), agc_driver_key_miss and agc_driver_graphics (new: table words and shapes in content keys) and agc_shader_disk_cache (plans with table columns and key domains round trip) pass on both. The table tests don't build on main; _cache_bounds fails without the two bounds.

Not tested: native RADV, Linux with write tracking, console behaviour.

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 3080 Laptop GPU, driver 610.62, Windows 11; llvmpipe (Mesa 25.2.8), WSL Ubuntu 24.04
  • AI-assisted: yes

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for this. The Linux job fails in the new agc_driver_image_table test on all three of #757, #758 and #759 (run 37374351259):

AGC graphics: uint texels through a linear sampler: lane 0 is 0x00000001, expected 0x00001000

I don't think the table path is at fault here. The check just before it, that record 4 gives the same words as a direct binding of the same T# and S#, passed in that run, so the direct binding returns 0x00000001 on lavapipe too. Record 4 pairs LinearWrap with a 32_UINT image, and requiresPointSampler forces point filtering only for the signed integer class (and conversions and depth), so an unsigned integer image reaches Vulkan with a linear filter. R32_UINT has no SAMPLED_IMAGE_FILTER_LINEAR support, so the result is undefined: NVIDIA happens to return the point sample, lavapipe doesn't.

Either of these should make it pass everywhere:

  • treat the unsigned integer class like the signed one in requiresPointSampler (and in the table entries' point-filtered S# copy), so both paths give the point sample on every driver; or
  • keep the direct-binding comparison for record 4 and drop its absolute check, leaving the uint point result to record 1.

The first also fixes direct bindings on lavapipe, so I'd prefer it, but it's your call. The other rows pass, and Windows is green.

@Celegans12
Celegans12 force-pushed the feat/agc-image-tables branch from 7c75f0e to b9c9cf0 Compare October 6, 2026 00:19
@Celegans12

Copy link
Copy Markdown
Contributor Author

Thanks, that was it. I went with the first option. #757, #758 and #759 are force-pushed onto main cb33f85.

  • requiresPointSampler now covers the Uint class like Sint. The table paths (an S# column of a table entry, and a direct T# with a table S#) use the same rule, so every integer view reaches Vulkan with the point-filtered copy. This also changes main's direct binding path for Uint images, as the description now says.
  • A driver-independent check was added to agc_driver_image_table, because NVIDIA and the Intel iGPU here return the centre texel even with a linear sampler. The check asserts that word 2 of the sampler bound for the direct Uint + LinearWrap pair, and of each table entry's integer copy, is the point word (0x01000000, not 0x00500000).
    • It fails without the change.
    • New rows cover a Uint direct T# under a linear table S#.
  • The wave32 per-wave case now uses its neighbours' gate (subgroup size 32 only), since llvmpipe never reached it before.
  • The perf(agc): baseline shader recompilation on registration #639 overlap note now says that perf(agc): baseline shader recompilation on registration #639 moves this rule (Sint only), so whichever lands second has to keep Uint in it.

Windows: ctest 335/335 (#757), 335/335 (#758) and 336/336 (#759).

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

Copy link
Copy Markdown
Collaborator

Thanks. The Uint rule now covers direct bindings, table entries and both mixed pairings (direct S# with a table image, table S# with a direct image), and the sampler-word checks don't depend on the driver.

Two things:

  • The chain now conflicts with main after fix(agc): emulate comparison sampling of color textures in the shader #668 landed (comparison sampling of color textures): IrMetadata/Resources.hpp, ResourceMaterializer.cpp, ShaderDiskCache.cpp and SpirvImageEmitter.cpp. It needs one more rebase.
  • A Uint image that shares an S# with a float image now takes a second sampler slot. A shader close to the 32-sampler limit (ResourceMaterializer.cpp:907) could therefore throw where main didn't. That seems unlikely to matter, but it may be worth a few words in the TechnicalDebt line.

@Celegans12
Celegans12 force-pushed the feat/agc-image-tables branch from b9c9cf0 to 734ef85 Compare October 6, 2026 02:49
@Celegans12

Copy link
Copy Markdown
Contributor Author

Rebased the chain onto main fe98276, over #668. Windows: ctest 342/342 (#757), 342/342 (#758) and 343/343 (#759); #757 alone also passes with -DANYPS5_ENABLE_SPIRV_TOOLS=ON.

  • fix(agc): emulate comparison sampling of color textures in the shader #668 for T#s outside tables: unchanged. agc_driver_emulated_color_compare passes as merged. The four conflicts were textual, and emulatedCompare is encoded next to tableView. FormatVersion stays 5, since fix(agc): emulate comparison sampling of color textures in the shader #668 left main at 4.
  • Where the two meet, the PR now throws at record time instead of being silently wrong:
    • A compare-sampled color T# whose S# comes from a table: the zeroed table slot would have produced compare function NEVER, point filtering and wrap.
    • A compare-sampled color texture inside a table: before, it reached the driver's stale "requires an R32 float or R16 unorm depth texture".
    • R32 float and R16 unorm entries keep the native comparison. agc_shader_memory has rows for each case.
  • Sampler slots: added a TechnicalDebt line. An integer image's point-filtered copy takes its own sampler slot, so a shader that pairs one S# with float and integer images reaches ShaderInfo::MaxSamplers one slot sooner than on main.
  • lavapipe: the same builds also ran on Mesa 26.2.4 llvmpipe here (8-wide subgroups). Everything passes except agc_driver_flat_store. It needs about 6 minutes on Windows llvmpipe, which is past the 240 s limit, and it passes when given the time.

@Celegans12

Copy link
Copy Markdown
Contributor Author

Correction to the lavapipe note above. Under identical load, with main's and this chain's test binaries running side by side on Mesa 26.2.4 llvmpipe, this chain is about 45% slower in both slow tests:

  • agc_driver_global_vcc_base: 94 s on main, 137 s here.
  • agc_driver_flat_store: 259 s on main, 365 s here.

NVIDIA shows no difference, and neither test uses image tables, so this is a per-dispatch or per-compile cost that #757 adds everywhere. It is not the environment. I'm tracking it down and will fix it on this branch before asking for another look.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for the rebase. I went through the range-diff against the previous heads: the #668 merge keeps emulatedCompare next to the table view, FormatVersion 5 still invalidates the cache, and the new record-time errors only fire where a compare-sampled color texture meets a table (table S#, or a color entry inside a table), with R32 float and R16 unorm entries still comparing natively. Failing there is better than the silent NEVER/point/wrap sampler. The sampler-slot TechnicalDebt line covers the second point.

Thanks also for the lavapipe correction. It matches CI: #758's Linux job (run 37406080556) timed out in agc_driver_flat_store at 120 s, while main took 66 s for that test on its last run. I'll wait for the fix before running the chain in PPSA21564, since a per-dispatch or per-compile cost would show up in its frame rate too.

@Celegans12
Celegans12 force-pushed the feat/agc-image-tables branch from 734ef85 to 1219876 Compare October 6, 2026 04:42
@Celegans12

Copy link
Copy Markdown
Contributor Author

Fixed and force-pushed. #757, #758 and #759 are on main 109a328; the #685 rename in Dispatch.cpp was the only conflict.

  • Cause: RecordBdaFault computed the two fault-address words (OpUConvert of the address, and of the address shifted right by 32) before claiming the fault slot with OpAtomicCompareExchange, instead of inside the won block as on main.

    • The instruction count was unchanged, but this order reached about 1090 inlined fault-record sites in agc_driver_global_vcc_base.
    • llvmpipe's LLVM passes took about 50% longer on it. With GALLIVM_PERF=nopt both versions take the same time.
    • Shader execution, recompile counts, pipelines and per-dispatch work were unchanged.
  • Fix: both RecordBdaFault and RecordBdaFaultWords now claim the slot first and compute the words inside the won block. Callers of RecordBdaFault emit main's SPIR-V again: the wave32 module of agc_driver_global_vcc_base is byte-identical to main's.

    • agc_shader_memory gains a host-only check that the claim comes before any address conversion.
  • Side by side on llvmpipe (Mesa 26.2.4, same load):

    Test main this chain before the fix
    agc_driver_global_vcc_base 70.4 s 70.4 s 102.2 s
    agc_driver_flat_store 185 s 187 s 283 s
  • Tests: Windows ctest 354/354 (feat(agc): implement image and sampler tables indexed at runtime offsets #757), 354/354 (feat(agc): implement image tables loaded through 64-bit pointers #758) and 355/355 (feat(agc): implement deferred faults for reads through null SRT pointers #759), and the same on lavapipe.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

Thanks for tracking that down. The range-diff against the previous heads only moves the fault-word conversions into the won block (through the RecordFault template), adds verifyFaultSlotClaimedFirst, and keeps the #685 rename in Dispatch.cpp; #758 is unchanged. CI agrees with your numbers: agc_driver_flat_store took 67.5 s (#757), 74.4 s (#758) and 78.4 s (#759) on the new heads, against 88.2 s on main 109a328's own run, and agc_driver_global_vcc_base 27.6 to 30.5 s against 32.1 s.

I'll run the chain through PPSA21564 next (cold and warm shader cache) and report back here.

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

github-actions Bot commented Oct 14, 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
#1193 core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp L22-23, L25-27
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp L29-30, L446-447, L1026-1027, L1042, L1048-1056
#4943 core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp L34
core/shader/recompiler/ShaderDiskCache.cpp L87-89
#5181 core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ResourcePlan.hpp L13
#5371 core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/Resources.hpp L59-61
core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp L416-427
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp L195-197, L586-587
#5375 core/shader/recompiler/ShaderDiskCache.cpp L141-142
#5461 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp L170
core/shader/recompiler/ShaderDiskCache.cpp L141-142
#5472 core/shader/recompiler/Optimization/src/DescriptorBindingBuilder.cpp L170
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp L1037-1038
core/shader/recompiler/ShaderDiskCache.cpp L141-142
#5473 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp L1080-1101
#5496 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp L12, L683-684
#5524 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp L12, L682-684
#5554 core/libs/prx/libSceAgcDriver/Graphics/src/DepthSurface.cpp L12, L682-684
#5609 core/shader/recompiler/Optimization/include/Optimization/SrtWalker.hpp L34
core/shader/recompiler/ShaderDiskCache.cpp L87-89
#5620 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp L1358
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp L22-37
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp L317-338, L380-399
core/shader/recompiler/Optimization/src/ResourceTracker.cpp L433-463, L485, L498, L540-547, L557-585
core/shader/recompiler/ShaderDiskCache.cpp L110-111, L150, L967-975, L1106, L1109-1118
#5804 core/libs/prx/libSceAgcDriver/Graphics/include/Pipeline.hpp L127
core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp L703
core/libs/prx/libSceAgcDriver/Graphics/src/ShaderValidation.cpp L154, L330-331, L597, L618
core/libs/prx/libSceAgcDriver/tests/Graphics.cpp L26
#5820 core/libs/prx/libSceAgcDriver/tests/ShaderDiskCache.cpp L1358
core/shader/recompiler/IntermediateRepresentation/include/IntermediateRepresentation/IrMetadata/ControlFlowInfo.hpp L22-37
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp L317-338, L344-369, L380-399
core/shader/recompiler/Optimization/src/ResourceTracker.cpp L433-463, L485, L498, L540-547, L557-585
core/shader/recompiler/ShaderDiskCache.cpp L110-111, L150, L967-975, L1106, L1109-1118
#5875 core/shader/recompiler/ShaderDiskCache.cpp L86-89
#5899 core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp L220

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review of d9e47d1 (#757) with #758's 34525fd on top, merged onto main 84b01f9 (clean merge). Linux, RADV, -DANYPS5_ENABLE_SPIRV_TOOLS=ON, libSceAgc + libSceAgcDriver scope: 572/573 ctest.

  • agc_driver_image_table, _cache, _cache_bounds, _reuse, agc_driver_bindless_images, agc_shader_bindless, agc_shader_memory, agc_shader_disk_cache, agc_driver_graphics, agc_driver_key_miss, agc_sparse_run_import and _copied pass. In agc_driver_image_table the poisoned entries (outside the snapshot, not an image, unmapped surface, sRGB decode, table written by its dispatch) each reach the host as a thrown image table: fault. Only the per-wave wave32 case is skipped, since RADV's subgroup is 64.
  • The one failure, agc_driver_tessellation (SIGSEGV), fails the same way on main 84b01f9 built alone, so it isn't this PR.

All four points from my last review are addressed: the ARCHITECTURE.md section, both fold-ins (with their tests in agc_shader_bindless), the order (#4757 closed, #4883 rebased on) and the final numbers (disk format 32/33, runtime ABI 12). Selected poison entries throw through BdaResources::CheckFault, so nothing that main refused now returns a plausible value, and every unbound case has a debt line.

On @oneandonlydean's memory question: tibzejoker's 4dcf657 run shows both caches sitting at their new caps (dispatch cache ~254 MiB, resource cache 1,024 entries, only 20 table T# misses per 10 s). The ~1 GB/min that remains in GTA isn't in the structures this PR adds, and Guardians stays flat at 11.6 GiB. The CPU copies of sampled textures are bounded by the VRAM-derived budget, which is a follow-up that isn't specific to tables. The snapshot cost (Village 4.5 ms, GTA ~11 % of worker 0) is a performance follow-up (SleglJan's snapshot reuse plus the unwritten-range skip), not a correctness issue.

Approving both. #758 goes in after #757. The full Build (Windows) is dispatched on the final heads before the merge.

@Tamalero

Tamalero commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

@Celegans12, Guardians of the Galaxy (PPSA01748) on main bebff8b0e, which has this PR, #758 and #4475 merged. The only local change is one NpManager stub.

Better than before. All the menu text now renders, which it didn't on the older heads. The title goes through the language, brightness and adjustments screens, plays its intro video, and starts loading the first level.

It still stops during that load, on a shape I'd separate from the 0x4378/0x4380 one you described:

shader 0x3b94a7500 stage 5: GetImageResource dword 0 is not a valid runtime value; chain:
LoadAddressU32@pc=0x84(
  GetAddressResource(LoadAddressU32@pc=0x70(
    GetAddressResource(ReadConstBuffer@pc=0x30(GetBufferResource(GetUserData…),
                       ShiftLeftLogical32(ReadFirstLane(GetInterpolationParameter(…)), …)),
                       ReadConstBuffer@pc=0x30(…same…)), …),
  LoadAddressU32@pc=0x70(…))

The key is a readfirstlane of an interpolated attribute (a flat per-primitive id). It indexes a constant buffer that holds a pointer. That pointer leads to a second pointer, which leads to the T#. So it's two pointer hops, where #758 covers one. Yesterday's run, on the older heads, stopped on a second pixel shader with exactly the same chain at different pcs (0x3b9421600, pc 0x144 / 0x118 / 0x94). The 0x4378/0x4380 shaders (key from an ImageRead) still show up as unplanned at registration but haven't been the stop.

On the request: I'd rather not attach it. The serialized request carries the title's shader binary, and I keep game code off public threads. If you open a follow-up for either shape, I can run it on this title the same day and post the result, or paste more of AnyPS5's own planner output from the log.

Details
  • Linux, RTX 4090 (driver 615.78.08), i9-14900K. Relinked --to-intel against this build; fresh shader cache (format change).
  • Registration on this run: 21 shaders unplanned. 10 have a program address outside the shader registers (prepared on use), 8 are s_swappc_b64 to a computed target (feat(shader): link callees of calls whose target is loaded from a table #4706's territory), and 3 are the 0x4378/0x4380 shape. Plus 11 notices of "unsupported registered shader type 8".
  • RSS was about 10 GiB during the load. The text-strip re-creation I mentioned earlier dropped from about 5,400 to about 780 creations by the menus.

Tested and written by Claude (AI assistant) on a contributor's machine.

image Note, there seems to be bug unrelated to this thread on the language availability. Will be reported later (chunks)

@EagleBearerYS

Copy link
Copy Markdown
Contributor

@Celegans12, Village numbers for the snapshot-reuse follow-up. Resident Evil Village (PPSA01556) on main e34c479, which has this PR, plus the commits Village needs on this PC, on an RTX 3070 Laptop (driver 617.42) under Windows 11. The extra commits are #5703, #5851, #5815, #1186 and #3864, plus local workarounds for shader preparation, sample counts and sample locations.

From one run from boot to first control with APS5_PROFILE_DRAW=1, where the queue-0 worker is 100% busy throughout:

Phase 10 s windows Snapshots per 10 s [image-table] time per 10 s Per snapshot Narrowing applied
Title 77 445 2.4 s 5.4 ms 0 of 34,294
First 3D scene 72 116 1.3 s 11.2 ms 0 of 8,350
The scene's fade 52 74 1.4 s 18.4 ms 0 of 3,858

Each table has 32,768 keys. In a Tracy capture of the title with local zones, the draw stage's capture, which reads each column's records, is 55% of the worker's own time, about 100k words read per capture. resolveImageTables takes another 15%, 2.9 ms per draw. Both of those numbers come from profiled runs.

Without any profiling (only APS5_TRACE_SECONDS), a trace of the load into the first 3D scene has Shader.DrawStage at 171 ms of the 332 ms per frame that queue 0's draw packets take.

So keeping a column while its words are unchanged, and skipping its read while tables.ranges has no write since the last snapshot, would reach a large share of the worker's time in this title.

I can test SleglJan's perf/image-table-snapshot-reuse, or the follow-up PR when it's up, on Village and post the same numbers before and after.

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

Labels

agc libSceAgc* build docs shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.