Repository navigation
Raw global memory lookups reuse the range they last chose: about 3 ms less GPU a frame in Ocarina of Time's Kokiri Forest - #1464
TheekshanaA wants to merge 1 commit into
Conversation
… Time's 2,228 raw-memory shaders take about 3 ms less GPU a frame in Kokiri Forest (chain 15a7cccf7a6b) Each 32-bit word a shader reads through an address traced to no storage buffer goes through GlobalToStorage.TryGenerateRawStorageOp's helper on its own: the 2 MiB chunk's hint, and on a miss the unrolled 13-step search of the GMEM map. Shaders read structures word after word from one range, so all but the first lookup choose the entry the first one chose. dmsh_wide.py raw_ranges, the last step before dmsh_build.py compiles a shader: each function that calls the helper keeps the range its last lookup chose (dm_raw_range: start, end, entry), and an address inside it takes that entry without the hint or the search. What follows the choice (the end check, the delta, the read or write) is unchanged. The GMEM map's ranges are disjoint, so the search would choose the same entry, and the cache lives in one invocation, so the table cannot change under it. A helper of another shape leaves the shader as it is. Ocarina of Time v131072 on a MacBook Pro M5 Pro, chain adec7d81d970, the 2,228 libraries with the helper replaced by loose ones (identical to what raw_ranges writes for each), the same scripted seven-minute route into Kokiri Forest with warmed pipeline archives: GPU per frame p50 17.0 -> 14.1 ms, frame time p50 / p95 17.3 / 18.4 -> 16.5 / 18.1 ms, frames dropped on screen 9 -> 0 per 300. Five screenshots at the spawn point match the stock build. Check: dm/gfx/tests/test_dmsh_raw_ranges.py (the helper's search behind the cache, the call sites, shapes left alone, the compile), test_dmsh_wide.py, test_nsp_chain.py, test_docs.py. Not run: regress.py on other games (this Mac has no other game on this chain), and a full shader build of the new chain (measured through loose libraries instead). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016V593AxbpRKaaSGzeQSmEk
|
@TheekshanaA thanks, this is the cache we hoped for. Three things:
|
|
@lunks thanks, and good to have an independent number. 2. Confirmed for the mesh and task shaders. I ran this branch's
So the hook does see the source after 3. Pool tables: will do. I'll port it onto main's walk as its own PR, carrying the notes it needs rather than depending on #1387, with a unit test and before/after numbers on main. |
|
@lunks a small update on point 3: I won't get to the pool-table port right away. If you or anyone else wants to take it first, please do. The branch is |
|
@TheekshanaA I'll take the pool-table port. I'm rebasing your two commits from |
…na of Time's 2,241 raw-memory shaders take about 2 ms less GPU a frame at the Kokiri Forest spawn point (chain cfcc3a82c78b, PR #1464) Each 32-bit word a shader reads or writes through an address traced to no storage buffer found its pool on its own: the hint of its 2 MiB chunk, and on a miss the 13-step search of the pool table. A shader reads a structure word after word from one pool, so all but the first lookup of a run chose the entry the first one chose. GlobalToStorage.TryGenerateRawStorageOp: each helper keeps the entry it chose last in a private word of the invocation (ResourceManager.AddRawLastEntryMemory, zero at the start through a SPIR-V null initializer, MemoryDefinition.ZeroInit). An address inside that entry's range takes it without the hint or the search; a hint or a search records its choice. Loads, stores and atomics all do it. The table's ranges are disjoint and the host binds one table per draw or dispatch, so the search would choose the same entry; entry 0 is empty, so an invocation's first lookup searches. dmsh_meshstage.py keeps thread variables spirv-cross declares ahead of the stage memory in dm_stage_body (98 mesh and 11 task shaders of Ocarina of Time failed the mesh stage without it). PR #1464 did the same as a text rewrite of spirv-cross's MSL (dmsh_wide.py raw_ranges), loads only, and silent when the generator's output changes shape; this is the same cache where the helper is generated. Ocarina of Time on an M6 Mac, main's host, the 2,241 shaders of this chain laid over main's libraries (the 2,592 other shaders built with them came out with the same MSL; the reflection differs only in its chain field), the scripted walk from save_ref with warmed pipeline archives, two runs a side: GPU span p50 at the spawn point (same view, about 2,500 draws) 16.53 / 16.41 -> 14.11 / 14.57 ms, over the walk 15.7 -> 14.0 ms. Screenshots at the spawn point and red lines match. Open: frame time p95 over the walk 19 -> 26 ms (runs ended in different places, the game's dynamic resolution took the headroom, 8 GB of swap in use); the PR's register-held range not compared on this Mac. Check: dm/gfx/tests/test_dmsh_raw_last.py (new: the helper's shape, and its reads on the GPU; a helper without the cached entry's end check fails 768 of 1,024), test_dmsh_meshstage.py, the translator tests (DMSH_TEST_XLATE: ampere_alu, ampere_mem, control_flow, maxwell_ftz, geometry, image_atomic_bounds, txq_type), test_nsp_chain.py, test_xlate_anycpu.py, test_docs.py. Regression: Ocarina of Time only, as the A/B above (regress.py would translate its 42,853 shaders for both chains); Donkey Kong Bananza, the second game chosen (raw stores and the write log), was not run: its drive was unmounted during the session. Co-authored-by: Nimesh Ambepitiya <10327965+TheekshanaA@users.noreply.github.com>
…he entry they chose last (0f510fd), on this tree's hinted pool table The translator's helper keeps its last entry (upstream) ahead of this tree's two-word hint and counted search; PR seedds#1464's MSL rewrite (raw_ranges) no longer runs, as the two caches would stack. test_dmsh_raw_last.py builds this tree's table. Chain c63e56412497. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @TheekshanaA. Your cache is on main as 0f510fd, with you as co-author, so I'm closing this PR without merging its code. Main builds the same idea into the translator ( Our numbers, from Ocarina of Time on an M6 Mac with the 2,241 shaders of the new chain: GPU p50 at the spawn point went from about 16.5 to 14.1–14.6 ms, and over the walk from 15.7 to 14.0 ms. That is about 2 ms against your 3 ms. Two things are still open:
The pool-table port is with @PucaVaz, who said above that they'll open it as its own PR. |
|
@seedds @TheekshanaA the pool-table port is up as #1489, on main 0f510fd with TheekshanaA as co-author. It carries the table patching from #1387 that main didn't have, adapted to main's walk; the PR lists what changed on the way. On Ocarina of Time (four alternating scripted runs on a 24 GB M4 from the Link's house autosave), whole table builds went from 24,000–44,000 per run to about 100, and the submit thread's CPU time per frame from 12–15 ms to 3–5 ms. |
…ng walked whole: registrations, memory pools added or removed, retired IDs at a new submission and storage takeovers each leave a note pool_gpu walked a table of handles traced to no slot whole at any change of g_reg_gen or g_ser_gen, though each change reaches only a few of its IDs. On main (6121fd7), TheekshanaA measured about 53 s of a seven-minute walk through Ocarina of Time's Kokiri Forest spent on 61,000 such builds of its 33,025-entry texture table (PR seedds#1405, seedds#1464). The game registers something between most of its draws, its streamed textures map and unmap virtual pool runs (vrun_set) dozens of times a second, finalized descriptors stay registered, and textures take over each other's storage (alias_take_over, about 8,400 times in six minutes), which bumped g_ser_gen with no note and so also sent every texture ID down the slow path. This ports TheekshanaA's branch oot-pool-table-notes (9579768, 9d486f8) to main's walk. Those commits sat on the table patching of seedds#1299 / seedds#1387, which main did not take, so the patching comes with them: - A registration records its generation and ID (g_reg_log), a storage change its serial (g_ser_log, beside the notes ser_sync takes). A table built at an earlier generation takes again only the IDs they name, by the walk's own rules (pool_entry), and is kept when no entry changes, else copied and patched. - A memory pool added or removed records REG_POOL and no ID: a table takes again only the IDs it found without storage (pool_snap.dead), and treg_gone notes each ID of a finalized pool. A note's generation is stored last (release) and read first (acquire), as pool changes come from threads that do not hold g_rmu. - A table keeps the IDs whose entries are views of retired registrations (pool_snap.trans and .sub); the next submission takes only those again. - A takeover records a note that names no texture (ser_note(0)). - Past POOL_IDS_MAX IDs on a list, a note the ring has overwritten, a bump without a note, a pool that grew or a sparse mapping, the table is built whole as before. The log line says why tables were built whole and how many were patched or kept; the power lines gain "[nvn] pool tables up to frame ...". What main's walk changes on the way: - A patch makes textures only within what the frame may still make (pool_room, taken out of pool_walk_limit), as a walk does. A texture left out counts in the table's left, and the walk goes on next frame as before. - Patched tables take and give their buffers through pool_table_take / pool_table_give, as walked ones do. - A retired registration's view that the read scan writes in place (pool_seen_scan) is listed as transient too, and a table holding such views from an earlier submission does not stand under POOL_SUB_MS. Before, nothing made a table take those IDs again after the submission whose end drops the views. - alias_sweep, and seedds#1387's exception that kept a retired view of a shared texture past its submission, are not needed: main drops every retired view at the submission's end, so the next submission takes those IDs again. - A sampler table follows registrations alone. Checked: t_pool_notes (new, 67 checks). A pool elsewhere keeps the table, the pool that brings an ID's storage patches it, a finalized pool's ID holds the dummy, a bump without a note builds it whole, a retired ID and a takeover are taken again at the next submission without a build, a registration past the budget is left out by a patch, and a read written in place is taken again; patched tables match tables built whole. Without the takeover note, the retake at a new submission or the in-place record, it fails (2, 4 and 2 checks). make -C dm/gfx/tests GAME=0400486024DEC000 for the 35 tests that reach registrations, pool tables, serial notes and the sharing table (t_pool_chunks, t_feedback, t_sparse_tiles, t_image_alias and t_nvn among them): all pass. The Mac host builds, and the two changed files compile for the iPhone SDK. clang-tidy (LLVM 22, not CI's 23) finds nothing new in them. Ocarina of Time, four scripted runs on an M4 with 24 GB, alternating main (88c7e03) and this change, from the autosave at Link's house (180 s in the house, Saria's greeting, 300 s on the balcony): the table was built whole 44,000 and 24,000 times per run on main (67 s and 72 s), about 100 times with this change (about 0.2 s), while about 86,000 tables were patched and 63,000 kept. The submit thread's CPU time per frame (p50) went from 12-15 ms to 3-5 ms; frame p50 is the GPU's and did not move, p99 in the house went from 34.6 and 41.0 ms to 24.2 and 25.6 ms, and frames over 50 ms walking from 23 and 48 to 3 and 2. Same picture, footprint and red lines. Co-authored-by: TheekshanaA <10327965+TheekshanaA@users.noreply.github.com>
Upstream main up to 88c7e03 (Ocarina of Time from upstream's landing), the fuzz crash fix in shader_split.py. seedds#1464 and the HMMA title-movie speed-up wait for the next rebase. scripts/release_check.py check 0.12.0 passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
…t: Ocarina of Time's 2,241 raw-memory shaders take about 2 ms less GPU a frame at the Kokiri Forest spawn point (chain cfcc3a82c78b, PR seedds#1464)" This reverts commit 0f510fd.
…ng walked whole: registrations, memory pools added or removed, retired IDs at a new submission and storage takeovers each leave a note pool_gpu walked a table of handles traced to no slot whole at any change of g_reg_gen or g_ser_gen, though each change reaches only a few of its IDs. On main (6121fd7), TheekshanaA measured about 53 s of a seven-minute walk through Ocarina of Time's Kokiri Forest spent on 61,000 such builds of its 33,025-entry texture table (PR seedds#1405, seedds#1464). The game registers something between most of its draws, its streamed textures map and unmap virtual pool runs (vrun_set) dozens of times a second, finalized descriptors stay registered, and textures take over each other's storage (alias_take_over, about 8,400 times in six minutes), which bumped g_ser_gen with no note and so also sent every texture ID down the slow path. This ports TheekshanaA's branch oot-pool-table-notes (9579768, 9d486f8) to main's walk. Those commits sat on the table patching of seedds#1299 / seedds#1387, which main did not take, so the patching comes with them: - A registration records its generation and ID (g_reg_log), a storage change its serial (g_ser_log, beside the notes ser_sync takes). A table built at an earlier generation takes again only the IDs they name, by the walk's own rules (pool_entry), and is kept when no entry changes, else copied and patched. - A memory pool added or removed records REG_POOL and no ID: a table takes again only the IDs it found without storage (pool_snap.dead), and treg_gone notes each ID of a finalized pool. A note's generation is stored last (release) and read first (acquire), as pool changes come from threads that do not hold g_rmu. - A table keeps the IDs whose entries are views of retired registrations (pool_snap.trans and .sub); the next submission takes only those again. - A takeover records a note that names no texture (ser_note(0)). - Past POOL_IDS_MAX IDs on a list, a note the ring has overwritten, a bump without a note, a pool that grew or a sparse mapping, the table is built whole as before. The log line says why tables were built whole and how many were patched or kept; the power lines gain "[nvn] pool tables up to frame ...". What main's walk changes on the way: - A patch makes textures only within what the frame may still make (pool_room, taken out of pool_walk_limit), as a walk does. A texture left out counts in the table's left, and the walk goes on next frame as before. - Patched tables take and give their buffers through pool_table_take / pool_table_give, as walked ones do. - A retired registration's view that the read scan writes in place (pool_seen_scan) is listed as transient too, and a table holding such views from an earlier submission does not stand under POOL_SUB_MS. Before, nothing made a table take those IDs again after the submission whose end drops the views. - alias_sweep, and seedds#1387's exception that kept a retired view of a shared texture past its submission, are not needed: main drops every retired view at the submission's end, so the next submission takes those IDs again. - A sampler table follows registrations alone. Checked: t_pool_notes (new, 67 checks). A pool elsewhere keeps the table, the pool that brings an ID's storage patches it, a finalized pool's ID holds the dummy, a bump without a note builds it whole, a retired ID and a takeover are taken again at the next submission without a build, a registration past the budget is left out by a patch, and a read written in place is taken again; patched tables match tables built whole. Without the takeover note, the retake at a new submission or the in-place record, it fails (2, 4 and 2 checks). make -C dm/gfx/tests GAME=0400486024DEC000 for the 35 tests that reach registrations, pool tables, serial notes and the sharing table (t_pool_chunks, t_feedback, t_sparse_tiles, t_image_alias and t_nvn among them): all pass. The Mac host builds, and the two changed files compile for the iPhone SDK. clang-tidy (LLVM 22, not CI's 23) finds nothing new in them. Ocarina of Time, four scripted runs on an M4 with 24 GB, alternating main (88c7e03) and this change, from the autosave at Link's house (180 s in the house, Saria's greeting, 300 s on the balcony): the table was built whole 44,000 and 24,000 times per run on main (67 s and 72 s), about 100 times with this change (about 0.2 s), while about 86,000 tables were patched and 63,000 kept. The submit thread's CPU time per frame (p50) went from 12-15 ms to 3-5 ms; frame p50 is the GPU's and did not move, p99 in the house went from 34.6 and 41.0 ms to 24.2 and 25.6 ms, and frames over 50 ms walking from 23 and 48 to 3 and 2. Same picture, footprint and red lines. Co-authored-by: TheekshanaA <10327965+TheekshanaA@users.noreply.github.com>
…ng walked whole: registrations, memory pool changes, retired IDs at a new submission and storage takeovers each leave a note; Ocarina of Time's village walk builds its 33,025-entry table 16 times instead of over 4,000 (PR #1489) pool_gpu walked a table of handles traced to no slot whole at any change of g_reg_gen or g_ser_gen, though each change reaches only a few of its IDs. Ocarina of Time registers something between most of its draws, its streamed textures map and unmap virtual pool runs dozens of times a second, finalized descriptors stay registered, and textures take over each other's storage (alias_take_over), which bumped g_ser_gen with no note and so also sent every texture ID down the slow path. TheekshanaA measured about 53 s of a seven-minute walk through Kokiri Forest spent on 61,000 such builds (PR #1405, #1464). This is TheekshanaA's branch oot-pool-table-notes, ported to main's walk by PucaVaz, with the table patching of #1299 / #1387 it sat on: - A registration records its generation and ID (g_reg_log), a storage change its serial (g_ser_log, beside the notes ser_sync takes). A table built at an earlier generation takes again only the IDs they name, by the walk's own rules (pool_entry), and is kept when no entry changes, else copied and patched. - A memory pool added or removed records REG_POOL and no ID: a table takes again only the IDs it found without storage (pool_snap.dead); treg_gone notes each ID of a finalized pool. - A table keeps the IDs whose entries are views of retired registrations, those the read scan writes in place among them (pool_snap.trans); the next submission takes only those again, and such a table does not stand under POOL_SUB_MS. - A takeover records a note that names no texture (ser_note(0)): it changes no serial's texture (an entry a live serial holds is not evicted, serial_table_holds; one a finalized serial holds is only unref'd). A live sRGB / linear twin's view of an evicted texture is found stale by tex_mtl_ and dropped (serial_del), a note naming it. - A patch makes textures only within what the frame may still make (pool_room). Past POOL_IDS_MAX IDs, a note the ring has overwritten, a bump without a note, a resized table or a sparse mapping, the table is built whole as before. The log line says why; the power lines gain "[nvn] pool tables up to frame ...". On landing: the comment and the reference entry name serial_table_holds (#1439 replaced the serial table walk) and describe the sRGB / linear twin case; a registration note is read as a sequence lock (reg_read: its generation is checked before and after its ID, and the writer clears it first), as memory pool changes write notes from threads that do not hold g_rmu and a note the ring overwrote during the read could be taken torn. Checked: t_pool_notes 81 checks (patched tables compared with tables built whole), t_pool_chunks 102, t_alias_takeover 28, t_gmem_words 16, t_feedback 159, t_image_alias 329, t_sparse_tiles 4,253, t_cpu_tex --metal 68, all 0 failed; test_docs; the Mac host builds and the iOS compile check passes (261 files). Ocarina of Time on an M-series Mac, hosts at 51cc8bc with and without this (main's chain has no libraries on disk for it), the village walk from save_ref: tables built whole 4,000+ (5.85 s+) on the base, 12 and 16 (0.11 s, 0.12 s) with this; submit p50 in the village 11-16 ms on the base, about 7-12 ms with this; the same red lines and no GPU error. Vertical stripes in both sides' shots at the same balcony points come from that old-chain setup, not this change. The author measured submit p50 12-15 -> 3-5 ms and frames over 50 ms walking 23 and 48 -> 3 and 2 (M4, two runs per side). No second game: none local has libraries for main's chain, and the one whose regress runs reach these tables (Paper Mario: The Thousand-Year Door) needs 11,861 shaders translated first. Co-authored-by: PucaVaz <65235669+PucaVaz@users.noreply.github.com> Co-authored-by: TheekshanaA <10327965+TheekshanaA@users.noreply.github.com>
…na of Time's 2,241 raw-memory shaders take about 2 ms less GPU a frame at the Kokiri Forest spawn point (chain cfcc3a82c78b, PR seedds#1464) Each 32-bit word a shader reads or writes through an address traced to no storage buffer found its pool on its own: the hint of its 2 MiB chunk, and on a miss the 13-step search of the pool table. A shader reads a structure word after word from one pool, so all but the first lookup of a run chose the entry the first one chose. GlobalToStorage.TryGenerateRawStorageOp: each helper keeps the entry it chose last in a private word of the invocation (ResourceManager.AddRawLastEntryMemory, zero at the start through a SPIR-V null initializer, MemoryDefinition.ZeroInit). An address inside that entry's range takes it without the hint or the search; a hint or a search records its choice. Loads, stores and atomics all do it. The table's ranges are disjoint and the host binds one table per draw or dispatch, so the search would choose the same entry; entry 0 is empty, so an invocation's first lookup searches. dmsh_meshstage.py keeps thread variables spirv-cross declares ahead of the stage memory in dm_stage_body (98 mesh and 11 task shaders of Ocarina of Time failed the mesh stage without it). PR seedds#1464 did the same as a text rewrite of spirv-cross's MSL (dmsh_wide.py raw_ranges), loads only, and silent when the generator's output changes shape; this is the same cache where the helper is generated. Ocarina of Time on an M6 Mac, main's host, the 2,241 shaders of this chain laid over main's libraries (the 2,592 other shaders built with them came out with the same MSL; the reflection differs only in its chain field), the scripted walk from save_ref with warmed pipeline archives, two runs a side: GPU span p50 at the spawn point (same view, about 2,500 draws) 16.53 / 16.41 -> 14.11 / 14.57 ms, over the walk 15.7 -> 14.0 ms. Screenshots at the spawn point and red lines match. Open: frame time p95 over the walk 19 -> 26 ms (runs ended in different places, the game's dynamic resolution took the headroom, 8 GB of swap in use); the PR's register-held range not compared on this Mac. Check: dm/gfx/tests/test_dmsh_raw_last.py (new: the helper's shape, and its reads on the GPU; a helper without the cached entry's end check fails 768 of 1,024), test_dmsh_meshstage.py, the translator tests (DMSH_TEST_XLATE: ampere_alu, ampere_mem, control_flow, maxwell_ftz, geometry, image_atomic_bounds, txq_type), test_nsp_chain.py, test_xlate_anycpu.py, test_docs.py. Regression: Ocarina of Time only, as the A/B above (regress.py would translate its 42,853 shaders for both chains); Donkey Kong Bananza, the second game chosen (raw stores and the write log), was not run: its drive was unmounted during the session. Co-authored-by: Nimesh Ambepitiya <10327965+TheekshanaA@users.noreply.github.com> (cherry picked from commit 0f510fd)
…na of Time's 2,241 raw-memory shaders take about 2 ms less GPU a frame at the Kokiri Forest spawn point (chain cfcc3a82c78b, PR seedds#1464) Each 32-bit word a shader reads or writes through an address traced to no storage buffer found its pool on its own: the hint of its 2 MiB chunk, and on a miss the 13-step search of the pool table. A shader reads a structure word after word from one pool, so all but the first lookup of a run chose the entry the first one chose. GlobalToStorage.TryGenerateRawStorageOp: each helper keeps the entry it chose last in a private word of the invocation (ResourceManager.AddRawLastEntryMemory, zero at the start through a SPIR-V null initializer, MemoryDefinition.ZeroInit). An address inside that entry's range takes it without the hint or the search; a hint or a search records its choice. Loads, stores and atomics all do it. The table's ranges are disjoint and the host binds one table per draw or dispatch, so the search would choose the same entry; entry 0 is empty, so an invocation's first lookup searches. dmsh_meshstage.py keeps thread variables spirv-cross declares ahead of the stage memory in dm_stage_body (98 mesh and 11 task shaders of Ocarina of Time failed the mesh stage without it). PR seedds#1464 did the same as a text rewrite of spirv-cross's MSL (dmsh_wide.py raw_ranges), loads only, and silent when the generator's output changes shape; this is the same cache where the helper is generated. Ocarina of Time on an M6 Mac, main's host, the 2,241 shaders of this chain laid over main's libraries (the 2,592 other shaders built with them came out with the same MSL; the reflection differs only in its chain field), the scripted walk from save_ref with warmed pipeline archives, two runs a side: GPU span p50 at the spawn point (same view, about 2,500 draws) 16.53 / 16.41 -> 14.11 / 14.57 ms, over the walk 15.7 -> 14.0 ms. Screenshots at the spawn point and red lines match. Open: frame time p95 over the walk 19 -> 26 ms (runs ended in different places, the game's dynamic resolution took the headroom, 8 GB of swap in use); the PR's register-held range not compared on this Mac. Check: dm/gfx/tests/test_dmsh_raw_last.py (new: the helper's shape, and its reads on the GPU; a helper without the cached entry's end check fails 768 of 1,024), test_dmsh_meshstage.py, the translator tests (DMSH_TEST_XLATE: ampere_alu, ampere_mem, control_flow, maxwell_ftz, geometry, image_atomic_bounds, txq_type), test_nsp_chain.py, test_xlate_anycpu.py, test_docs.py. Regression: Ocarina of Time only, as the A/B above (regress.py would translate its 42,853 shaders for both chains); Donkey Kong Bananza, the second game chosen (raw stores and the write log), was not run: its drive was unmounted during the session. Co-authored-by: Nimesh Ambepitiya <10327965+TheekshanaA@users.noreply.github.com>
…ng walked whole: registrations, memory pool changes, retired IDs at a new submission and storage takeovers each leave a note; Ocarina of Time's village walk builds its 33,025-entry table 16 times instead of over 4,000 (PR seedds#1489) pool_gpu walked a table of handles traced to no slot whole at any change of g_reg_gen or g_ser_gen, though each change reaches only a few of its IDs. Ocarina of Time registers something between most of its draws, its streamed textures map and unmap virtual pool runs dozens of times a second, finalized descriptors stay registered, and textures take over each other's storage (alias_take_over), which bumped g_ser_gen with no note and so also sent every texture ID down the slow path. TheekshanaA measured about 53 s of a seven-minute walk through Kokiri Forest spent on 61,000 such builds (PR seedds#1405, seedds#1464). This is TheekshanaA's branch oot-pool-table-notes, ported to main's walk by PucaVaz, with the table patching of seedds#1299 / seedds#1387 it sat on: - A registration records its generation and ID (g_reg_log), a storage change its serial (g_ser_log, beside the notes ser_sync takes). A table built at an earlier generation takes again only the IDs they name, by the walk's own rules (pool_entry), and is kept when no entry changes, else copied and patched. - A memory pool added or removed records REG_POOL and no ID: a table takes again only the IDs it found without storage (pool_snap.dead); treg_gone notes each ID of a finalized pool. - A table keeps the IDs whose entries are views of retired registrations, those the read scan writes in place among them (pool_snap.trans); the next submission takes only those again, and such a table does not stand under POOL_SUB_MS. - A takeover records a note that names no texture (ser_note(0)): it changes no serial's texture (an entry a live serial holds is not evicted, serial_table_holds; one a finalized serial holds is only unref'd). A live sRGB / linear twin's view of an evicted texture is found stale by tex_mtl_ and dropped (serial_del), a note naming it. - A patch makes textures only within what the frame may still make (pool_room). Past POOL_IDS_MAX IDs, a note the ring has overwritten, a bump without a note, a resized table or a sparse mapping, the table is built whole as before. The log line says why; the power lines gain "[nvn] pool tables up to frame ...". On landing: the comment and the reference entry name serial_table_holds (seedds#1439 replaced the serial table walk) and describe the sRGB / linear twin case; a registration note is read as a sequence lock (reg_read: its generation is checked before and after its ID, and the writer clears it first), as memory pool changes write notes from threads that do not hold g_rmu and a note the ring overwrote during the read could be taken torn. Checked: t_pool_notes 81 checks (patched tables compared with tables built whole), t_pool_chunks 102, t_alias_takeover 28, t_gmem_words 16, t_feedback 159, t_image_alias 329, t_sparse_tiles 4,253, t_cpu_tex --metal 68, all 0 failed; test_docs; the Mac host builds and the iOS compile check passes (261 files). Ocarina of Time on an M-series Mac, hosts at 66e6e4c with and without this (main's chain has no libraries on disk for it), the village walk from save_ref: tables built whole 4,000+ (5.85 s+) on the base, 12 and 16 (0.11 s, 0.12 s) with this; submit p50 in the village 11-16 ms on the base, about 7-12 ms with this; the same red lines and no GPU error. Vertical stripes in both sides' shots at the same balcony points come from that old-chain setup, not this change. The author measured submit p50 12-15 -> 3-5 ms and frames over 50 ms walking 23 and 48 -> 3 and 2 (M4, two runs per side). No second game: none local has libraries for main's chain, and the one whose regress runs reach these tables (Paper Mario: The Thousand-Year Door) needs 11,861 shaders translated first. Co-authored-by: PucaVaz <65235669+PucaVaz@users.noreply.github.com> Co-authored-by: TheekshanaA <10327965+TheekshanaA@users.noreply.github.com>
Follow-up to the measurements in #1405 (comment). As asked there, this is the per-invocation range cache as a step of the shader build.
What changes
Each 32-bit word a shader reads through an address traced to no storage buffer goes through
GlobalToStorage.TryGenerateRawStorageOp's helper on its own: the 2 MiB chunk's hint, and on a miss the unrolled 13-step search of the GMEM map. Shaders read structures word after word from one range, so all but the first lookup choose the entry the first one chose.dmsh_wide.pyraw_rangesruns as the last step beforedmsh_build.pycompiles a shader:struct dm_raw_range { ulong s; ulong e; int i; }, declared once per shader).nvn_mtl.m), so the search would choose the same entry. The cache also lives in one invocation, so the table can't change underneath it.It's hooked in just before
compile_targets(), so it sees the final source (mesh-stage, vertex-entry and fragment-variant rewrites included), which is the form I measured. The chain header is regenerated:adec7d81d970→15a7cccf7a6b.Measurements
Ocarina of Time v131072, MacBook Pro M5 Pro, macOS 27.2, on main
6121fd7(chainadec7d81d970). The game's 2,228 libraries that contain the helper were replaced by looseb-<chain>/<key>_b.metallibfiles compiled from exactly whatraw_rangeswrites for each one; I checked all 2,228 against the patched source. Same scripted seven-minute route into Kokiri Forest, pipeline archives warmed by an earlier run, stats from frame 4200 on:Five screenshots at the spawn point (frames 4300–4420) match between the two builds.
Checks
dm/gfx/tests/test_dmsh_raw_ranges.py(new): the helper's search behind the cache, the call sites sharing their function's cache, shapes left alone, and the compile with the build's flagstest_dmsh_wide.py,dm/tools/tests/test_nsp_chain.py,dm/tools/tests/test_docs.pydocs/reference/shaders.mdentry, the symptom index, and a line in the Ocarina noteNot run:
regress.pyon other games (this Mac has no other game on this chain), and a full shader build of the new chain (measured through loose libraries instead).Separately, the pool-table host change from that comment is on
oot-pool-table-notes. It's built on #1387's patching, which main didn't take, so it isn't a PR. On main, the same route still spent about 53 s of seven minutes walking pool tables (61,000 builds). I can port it if that approach is wanted.🤖 Generated with Claude Code
https://claude.ai/code/session_016V593AxbpRKaaSGzeQSmEk