Skip to content

perf(shader): reuse an image table column's records while its words are unchanged - #6440

Open
SleglJan wants to merge 1 commit into
boykopovar:mainfrom
SleglJan:pr/table-column-memo
Open

SleglJan wants to merge 1 commit into
boykopovar:mainfrom
SleglJan:pr/table-column-memo

Conversation

@SleglJan

@SleglJan SleglJan commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

What

An image table snapshot reads every record of every column again on each materialization: it locates each record's dwords, answers page accessibility, reads the runs, assembles a descriptor per record and indexes it. In 007 First Light's menu (PPSA11386), with columns of up to 19k records, snapshots take 7.78 s of every 10 s on main (5,209 snapshots, 1.49 ms each).

Each column now keeps a memo of its last read: the column itself (TableColumn), the narrowed record states it started from, the layout, the words it read, and their distinct descriptors with their available-dword masks. The layout covers which records are read whole or by dword, the runs, the states of the records left unread, and every page accessibility query the layout made with its answer.

  • Lookup: the memo is found under the shader's code address, the column's descriptor source, the table's base, size and key count, and a hash of the narrowed states.
  • Verification: the memo is used only when its column equals the column being read (TableColumn::operator==), its narrowed states equal the current ones element by element, and every recorded accessibility query gives the same answer again, in order. The layout is then reused.
  • Reuse: when the words read through the layout are also equal, the records take the stored descriptors, so there is one index lookup per distinct descriptor instead of one per record. The words are always read, so a changed table is never reused.
  • Admission and bounds:
    • A column is kept only from the second time its key is seen, so a table read once costs one plain read and no memory.
    • Up to 512 columns and an estimated 256 MiB are kept, least recently used out.
    • The [image-table] profile line counts hits, reused layouts and misses with the columns and MiB kept.
    • ResourceMaterializer::ColumnMemoCounts() returns the totals, for the tests.
  • Not covered: indirect columns (pointer chains) are not memoised.

Related:

Tested

  • agc_shader_memory: new cases for

    • an accessibility change under the same key (a page made inaccessible, then restored);
    • two shaders at one code address over one heap with different strides (each gets its own records);
    • a pointer column changed between reads;
    • a partial record's available-dword mask on a hit.
      Each read asserts whether it was a hit, a reused layout or a miss.
  • ctest agc|shader|recompiler on lavapipe (Mesa 26.2.4, LLVM 23.1.1; Arch Linux container, GCC 16.2.1), this change on main 4186f06: 590 of 599 pass, the same 9 failures as main built the same way (5 known ones plus 4 that need write watching, which the container lacks).

  • Rebased onto main f1af6c5 (2026-10-18). Only tests/ShaderMemory.cpp conflicted, where fix(recompiler): fold chained immediate addends of image-table record offsets #7325 added verifyChainedTableOffset at the same spot, and both tests are kept; the other files are unchanged. ctest ^agc on lavapipe, in an Arch Linux container with the same Mesa, LLVM and GCC and also without write watching: 663 of 676 pass. The 13 failures are the same as on main f1af6c5 built the same way: the 9 above, plus agc_driver_msaa_dcc_clear_2, _4, _8 and agc_driver_native_depth_bda_clear, which need write watching.

  • RTX 3080 (NVIDIA 615.78.08), CachyOS (Linux 7.2.10), GCC 16.2.1, Release, ctest agc|shader|recompiler: 599 of 600 pass. The failure, agc_driver_store_over_untracked, fails on Linux main too (reported on fix(agc): save pending storage results in every 64 KiB block that leaves the write watch #5671).

  • A local benchmark (200 captures of one shader with a 19k-entry image table; not part of this PR), 6 runs per side alternated on main 4186f06, on a Ryzen 7 5700X:

    Case main capture (µs) this change (µs)
    table unchanged 1947–2003 838–851
    one record changed before every capture 2057–2106 1371–1420
    table base moved every capture (never reused) 2070–2172 1914–1969

    Materialization time and peak RSS were equal to main in all three.

  • 007 First Light (PPSA11386), main menu, no input, warm caches, RTX 3080 as above.

{"commit":"699643f682cc","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"200:40"},"gpu":"NVIDIA GeForce RTX 3080","driver":"NVIDIA 615.78.08","os":"Linux 7.2.10-1-cachyos","resolution":"3840x2160","window":{"spec":"frames 200:40","first_present":200,"ms":30545.996},"presents":40,"frame_ms":{"p50":771.669,"p90":796.05,"p99":808.454,"max":808.454},"gpu_busy_ms_per_present":150.099,"worker_busy_pct":{"queue worker 0x0":99.952,"queue worker 0x20":98.291,"queue worker 0x38":98.295,"queue worker 0x28":98.843,"queue worker 0x30":98.844},"top_phases_ms_per_frame":{"Driver.Packet":3771.015,"Driver.Packet.memory_wait":2809.317,"Driver.Packet.draw":669.359,"Driver.Draw":669.227,"Driver.Draw.shader_compile_and_link":638.826,"Shader.DrawStage":622.789,"Shader.DrawStage.capture_resources":491.826,"Driver.Packet.dispatch":282.911},"trace_dropped":false,"trace":"aps5-trace-699643f682cc-2.json.gz"}
{"commit":"699643f682cc","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"200:40"},"gpu":"NVIDIA GeForce RTX 3080","driver":"NVIDIA 615.78.08","os":"Linux 7.2.10-1-cachyos","resolution":"3840x2160","window":{"spec":"frames 200:40","first_present":200,"ms":30478.711},"presents":40,"frame_ms":{"p50":763.671,"p90":785.542,"p99":804.787,"max":804.787},"gpu_busy_ms_per_present":154.606,"worker_busy_pct":{"queue worker 0x0":99.954,"queue worker 0x20":97.463,"queue worker 0x28":98.561,"queue worker 0x38":97.461,"queue worker 0x30":98.563},"top_phases_ms_per_frame":{"Driver.Packet":3746.547,"Driver.Packet.memory_wait":2792.384,"Driver.Packet.draw":667.504,"Driver.Draw":667.38,"Driver.Draw.shader_compile_and_link":634.349,"Shader.DrawStage":623.035,"Shader.DrawStage.capture_resources":496.436,"Driver.Packet.dispatch":277.717},"trace_dropped":false,"trace":"aps5-trace-699643f682cc-2.json.gz"}
{"commit":"699643f682cc","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"200:40"},"gpu":"NVIDIA GeForce RTX 3080","driver":"NVIDIA 615.78.08","os":"Linux 7.2.10-1-cachyos","resolution":"3840x2160","window":{"spec":"frames 200:40","first_present":200,"ms":30755.968},"presents":40,"frame_ms":{"p50":770.945,"p90":797.25,"p99":803.52,"max":803.52},"gpu_busy_ms_per_present":155.539,"worker_busy_pct":{"queue worker 0x0":99.955,"queue worker 0x20":97.574,"queue worker 0x28":98.892,"queue worker 0x30":98.894,"queue worker 0x38":97.936},"top_phases_ms_per_frame":{"Driver.Packet":3789.414,"Driver.Packet.memory_wait":2817.807,"Driver.Packet.draw":669.362,"Driver.Draw":669.231,"Driver.Draw.shader_compile_and_link":639.041,"Shader.DrawStage":629.266,"Shader.DrawStage.capture_resources":499.374,"Driver.Packet.dispatch":294.376},"trace_dropped":false,"trace":"aps5-trace-699643f682cc-2.json.gz"}
{"commit":"17b7e9e8b233","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"200:40"},"gpu":"NVIDIA GeForce RTX 3080","driver":"NVIDIA 615.78.08","os":"Linux 7.2.10-1-cachyos","resolution":"3840x2160","window":{"spec":"frames 200:40","first_present":200,"ms":58820.01},"presents":40,"frame_ms":{"p50":1463.749,"p90":1498.044,"p99":1542.307,"max":1542.307},"gpu_busy_ms_per_present":162.081,"worker_busy_pct":{"queue worker 0x0":99.975,"queue worker 0x20":97.526,"queue worker 0x28":98.61,"queue worker 0x38":97.625,"queue worker 0x30":98.614},"top_phases_ms_per_frame":{"Driver.Packet":7238.663,"Driver.Packet.memory_wait":5568.945,"Driver.Packet.draw":1364.972,"Driver.Draw":1364.833,"Driver.Draw.shader_compile_and_link":1340.145,"Shader.DrawStage":1333.044,"Shader.DrawStage.capture_resources":1199.99,"Driver.Packet.dispatch":296.511},"trace_dropped":false,"trace":"aps5-trace-17b7e9e8b233-2.json.gz"}
{"commit":"17b7e9e8b233","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"200:40"},"gpu":"NVIDIA GeForce RTX 3080","driver":"NVIDIA 615.78.08","os":"Linux 7.2.10-1-cachyos","resolution":"3840x2160","window":{"spec":"frames 200:40","first_present":200,"ms":56026.433},"presents":40,"frame_ms":{"p50":1397.877,"p90":1433.603,"p99":1447.574,"max":1447.574},"gpu_busy_ms_per_present":160.538,"worker_busy_pct":{"queue worker 0x0":99.972,"queue worker 0x20":97.533,"queue worker 0x28":98.63,"queue worker 0x30":98.635,"queue worker 0x38":97.646},"top_phases_ms_per_frame":{"Driver.Packet":6894.023,"Driver.Packet.memory_wait":5289.778,"Driver.Packet.draw":1289.66,"Driver.Draw":1289.518,"Driver.Draw.shader_compile_and_link":1266.412,"Shader.DrawStage":1258.559,"Shader.DrawStage.capture_resources":1123.283,"Driver.Packet.dispatch":301.492},"trace_dropped":false,"trace":"aps5-trace-17b7e9e8b233-2.json.gz"}
{"commit":"17b7e9e8b233","dirty":false,"env":{"APS5_TRACE_DIR":"<path>","APS5_TRACE_FRAMES":"200:40"},"gpu":"NVIDIA GeForce RTX 3080","driver":"NVIDIA 615.78.08","os":"Linux 7.2.10-1-cachyos","resolution":"3840x2160","window":{"spec":"frames 200:40","first_present":200,"ms":55599.282},"presents":40,"frame_ms":{"p50":1388.475,"p90":1440.71,"p99":1476.353,"max":1476.353},"gpu_busy_ms_per_present":161.73,"worker_busy_pct":{"queue worker 0x0":99.974,"queue worker 0x20":97.688,"queue worker 0x28":98.752,"queue worker 0x30":98.753,"queue worker 0x38":97.689},"top_phases_ms_per_frame":{"Driver.Packet":6848.013,"Driver.Packet.memory_wait":5254.326,"Driver.Packet.draw":1276.462,"Driver.Draw":1276.318,"Driver.Draw.shader_compile_and_link":1254.896,"Shader.DrawStage":1244.992,"Shader.DrawStage.capture_resources":1112.862,"Driver.Packet.dispatch":308.044},"trace_dropped":false,"trace":"aps5-trace-17b7e9e8b233-2.json.gz"}

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: none (performance; 007 First Light's menu numbers above)
  • GPUs tested: NVIDIA RTX 3080, driver 615.78.08, CachyOS (Linux 7.2.10); lavapipe (Mesa 26.2.4, LLVM 23.1.1), Arch Linux container
  • AI-assisted: yes (Claude Code)

🤖 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 7395 core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp L929-959, L969-970

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 5371 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 5500 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6177 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6218 core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 6235 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6280 core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 6312 core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 6454 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 6488 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 6612 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6645 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6683 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6941 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 6947 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 6977 core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 7002 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 7085 core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 7223 core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp
PR 7388 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
PR 7642 core/libs/prx/libSceAgcDriver/tests/ShaderMemory.cpp
core/shader/recompiler/Optimization/include/Optimization/ResourceMaterializer.hpp
core/shader/recompiler/Optimization/src/ResourceMaterializer.cpp

@github-actions github-actions Bot added shader core/shader agc libSceAgc* labels Oct 16, 2026
@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
@SleglJan
SleglJan force-pushed the pr/table-column-memo branch from cdb140f to 727a810 Compare October 18, 2026 01:09
@SleglJan

Copy link
Copy Markdown
Contributor Author

Rebased onto main f1af6c5 (727a810). The only conflict was in tests/ShaderMemory.cpp, where #7325 added verifyChainedTableOffset at the same spot. Both tests are kept, and the ResourceMaterializer change is unchanged.

The memo still compares:

ctest ^agc on lavapipe (Arch Linux container without write watching): 663 of 676 pass, the same 13 failures as main f1af6c5 built the same way. Twelve of them report that they need write watching. The other one is agc_driver_storage_write_back_overhang, which also failed in the earlier lavapipe run listed in the PR body.

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
@SleglJan
SleglJan force-pushed the pr/table-column-memo branch from 727a810 to 341a085 Compare October 18, 2026 02:40
@github-actions github-actions Bot added perf Performance change needs rebase Conflicts with main, rebase needed and removed needs rebase Conflicts with main, rebase needed labels Oct 18, 2026
…re unchanged

A table snapshot read every record of every column again on each materialization: locate its dwords,
answer page accessibility, read the runs, assemble a descriptor per record and index it. In 007
First Light's menu, with columns of up to 19k records, snapshots took 7.78 s of every 10 s (5,209
snapshots, 1.49 ms each). With the memo they take 0.62 ms each and the menu runs at 1.31 frames/s
instead of 0.71 (10,490 hits and 0 misses per 10 s, 38 columns and 29.8 MiB kept).

A column now keeps a memo of its last read: the column itself (TableColumn), the narrowed record
states it started from, the layout (which records are read whole or by dword, the runs, the states
of the records left unread, and every page accessibility query the layout made with its answer), the
words it read, and their distinct descriptors with their available-dword masks.

The memo is stored under the shader's code address, the column's descriptor source, the table's
base, size and key count, and a hash of the narrowed states. That key only finds a candidate. The
candidate is used only when its column equals the column being read under TableColumn's operator==
(every field that places or reads a record; the key bound and the key domains act only through the
key count and the narrowed states), its narrowed states equal the current ones element by element,
and every recorded accessibility query gives the same answer again, in order. The layout is then
reused. When the words read through it are also equal, the records take the stored descriptors: one
index lookup per distinct descriptor instead of one per record. The words are always read, so a
changed table is never reused.

A column is kept only from the second time its key is seen: the last 4096 keys read without a memo
are remembered, so a table read once (or whose base moves every frame) costs one plain read and no
memory. Up to 512 columns and an estimated 256 MiB are kept, least recently used out. A memo that
only refreshes the words shares the layout of the memo it replaces, and a query that repeats the one
recorded just before it is recorded once. The [image-table] profile line counts hits, reused layouts
and misses with the columns and MiB kept, and ResourceMaterializer::ColumnMemoCounts() returns the
totals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@SleglJan
SleglJan force-pushed the pr/table-column-memo branch from 341a085 to 81c1a5f Compare October 18, 2026 21:38
@github-actions github-actions Bot removed the needs rebase Conflicts with main, rebase needed label Oct 18, 2026
@adriadam10 adriadam10 self-assigned this Oct 18, 2026

@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.

The memo looks sound: the key plus TableColumn::operator==, the narrowed states and the replayed accessibility checks cover what the layout reads, and the words are always read again. The tests assert hit / layout reused / miss on each read, and they would fail without the change.

Two things before this can merge:

  • docs/dev/ARCHITECTURE.md: this adds a cache to the image table snapshot, but the TableSnapshotter paragraph (the one starting "At every invocation, TableSnapshotter ... reads each column's records") doesn't mention it. Please add a sentence under it: the per-column memo, what it is keyed and verified on, that the words are always re-read, and its bounds (512 columns, 256 MiB, LRU, admitted on the second sighting).
  • Conflict with #7395: it also rewrites the column read in ResourceMaterializer.cpp (merge-tree conflicts in that file). Whichever lands second needs a rebase, so please rebase once the other is on main.

This branch has not been deployed

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

Labels

agc libSceAgc* perf Performance change shader core/shader

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants