Skip to content

perf(recompiler): scan image-table key buffers in bulk - #7101

Merged
oneandonlydean merged 1 commit into
boykopovar:mainfrom
XairRrR:perf/recompiler-bulk-table-key-scan
Oct 17, 2026
Merged

oneandonlydean merged 1 commit into
boykopovar:mainfrom
XairRrR:perf/recompiler-bulk-table-key-scan

Conversation

@XairRrR

@XairRrR XairRrR commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

What

TableSnapshotter::Narrow reads the key buffer of an image-table column to find which records the shader can select. It read one key at a time: a PageAccessible lookup and a single-word readMemory per key. In Uncharted: Legacy of Thieves Collection (PPSA05684), gameplay scans about 20,000 keys per table snapshot.

Changes (ResourceMaterializer.cpp only):

  • When the runtime has a bulk reader (readWords) and the key stride is a multiple of 4 and at most 64 bytes, the scan checks each page of the key span once and reads the span with one readWords call. With a stride of 64 bytes or less every page in the span holds a key, so the pages checked are the ones the word-by-word scan checked. Other strides, and runtimes without readWords, keep the word-by-word scan.
  • A key maps to its record without a division when key * stride cannot wrap. A key that wraps takes the old path (relative % stride, relative / stride), so wrapped keys still select the record the 32-bit address arithmetic reaches.
  • Once one key is known to fall outside, the ScalarBufferDword test is skipped for later keys.

Measured in PPSA05684 gameplay with APS5_PROFILE_DRAW=1, image-table time per snapshot: about 6.5 ms before, 5 ms after. That build also carried other local changes (the same scene, warm shader cache); this branch on its own was not run in the title.

Overlap with open PRs:

Tested

Arch Linux (kernel 7.2.9-zen), NVIDIA RTX 4070 Ti SUPER, driver 615.78.08, GCC 16.2.1, Release, BUILD_TESTING=ON.

  • New verifyImageTableBulkKeyScan in agc_shader_bindless (agc_shader_memory_tests --bindless). It captures the material table shader twice, once with only readMemory and once with readWords too. It checks that:
    • without a bulk reader the three keys are read one word each;
    • with one, no key is read word by word;
    • both snapshots are equal, and the codes match the keys. One key (0x08000003) wraps when multiplied by the 32-byte stride and is the only one that selects record 3.
  • Red before: with main's ResourceMaterializer.cpp it fails with image table bulk key scan: the keys were read word by word although a bulk reader exists.
  • Negative control: with the wrapped key mapped outside instead of to its record, it fails with image table bulk key scan: the narrowed codes are wrong.
  • ctest -j4 -R "image_table|bindless|agc_shader_": the 15 built tests pass (agc_driver_image_table, _cache, _cache_bounds, _reuse, agc_driver_bindless_images, agc_shader_bindless, agc_shader_memory, agc_shader_srt_read_patching, and the other agc_shader_* tests of agc_shader_memory_tests).
  • python3 tools/check_conventions.py --base origin/main: passed.

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 RTX 4070 Ti SUPER, driver 615.78.08, Arch Linux (kernel 7.2.9-zen)
  • AI-assisted: yes

@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 6243 core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp L508-512

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 4339 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 5371 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 5500 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 5958 core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 6177 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6222 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6235 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6412 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 6454 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 6645 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6946 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 6947 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 6973 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6987 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6989 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6995 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 7002 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 7223 core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@XairRrR full CI on 4ca1dc6 fails one Windows check that comes from this PR: prx_thread_local_destructors.

On Windows a thread_local destructor in a module runs after emulated TLS
freed the object; keep such objects in HostThreadLocal:

  libSceAgcDriver.prx: ShaderRecompiler::(anon)::TableSnapshotter::Narrow(const TableColumn&, ColumnRecords&) registers a thread_local destructor

That's the new thread_local std::vector<std::uint32_t> span; in ResourceMaterializer.cpp. The fix is the project's HostThreadLocal helper (prx/libc/include/HostThreadLocal.hpp). DrawRegisterKey.cpp has an example: HostThreadLocal<Type, Tag>() returns a reference you can keep using the same way. A plain local vector also works if the reuse across calls isn't important.

The other two Windows failures in that run (conventions_function_bodies timeouts) are a known runner-load flake, not this PR. Linux is green.

@github-actions github-actions Bot added shader core/shader agc libSceAgc* labels Oct 17, 2026
Narrowing a table column read its key buffer one key at a time, with a
page lookup and a single-word read per key; PPSA05684 scans about 20,000
keys per table snapshot. The scan now checks each page once and reads the
keys with the bulk reader, and maps a key to its record without dividing
when the multiplication cannot wrap.

Gameplay, image-table time per snapshot: about 6.5 ms before, 5 ms after.

AI-assisted: yes
@XairRrR
XairRrR force-pushed the perf/recompiler-bulk-table-key-scan branch from 4ca1dc6 to bf0c51e Compare October 17, 2026 15:28
@XairRrR

XairRrR commented Oct 17, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @oneandonlydean, good catch. Fixed in bf0c51e (amended): the key words are now read into a scratch vector that is a member of TableSnapshotter, so it is reused across the columns of one snapshot and there is no thread_local left in ResourceMaterializer.cpp. agc_shader_bindless, agc_shader_memory and the agc_driver_image_table* tests pass on Linux, nm shows no __cxa_thread_atexit reference in the object any more, and the conventions check passes.

@oneandonlydean
oneandonlydean merged commit b39e858 into boykopovar:main Oct 17, 2026
8 checks passed
oneandonlydean pushed a commit that referenced this pull request Oct 18, 2026
Main's #7101 reads a key buffer's words in one call where this branch
names the reason a narrowing is skipped. Keep main's bulk read and
count both of its unmapped-page skips as unmapped.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agc libSceAgc* shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants