Skip to content

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

Closed
TheekshanaA wants to merge 1 commit into
seedds:mainfrom
TheekshanaA:raw-lookup-ranges
Closed

TheekshanaA wants to merge 1 commit into
seedds:mainfrom
TheekshanaA:raw-lookup-ranges

Conversation

@TheekshanaA

Copy link
Copy Markdown
Contributor

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.py raw_ranges runs as the last step before dmsh_build.py compiles a shader:

  • Each function that calls the helper keeps the range its last lookup chose (struct dm_raw_range { ulong s; ulong e; int i; }, declared once per shader).
  • An address inside that range takes the cached entry without the hint or the search.
  • Everything after the choice (the end check, the delta, the read or write) is unchanged.
  • It's safe because the GMEM map's ranges are disjoint (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.
  • A helper of any other shape leaves the shader unchanged.

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 (chain adec7d81d970). The game's 2,228 libraries that contain the helper were replaced by loose b-<chain>/<key>_b.metallib files compiled from exactly what raw_ranges writes 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:

main main + raw_ranges
GPU per frame p50 / p95 17.0 / 17.6 ms 14.1 / 14.6 ms
frame time p50 / p95 17.3 / 18.4 ms 16.5 / 18.1 ms
frames dropped on screen per 300 9 0
frame 6500's encoders, first start to last end 17.5 ms 15.0 ms

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 flags
  • test_dmsh_wide.py, dm/tools/tests/test_nsp_chain.py, dm/tools/tests/test_docs.py
  • Docs: a docs/reference/shaders.md entry, the symptom index, and a line in the Ocarina note

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

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

… 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
@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation shader chain The shader translation chain (DMSH_CHAIN): a change rebuilds every game's shaders game: 0400486024DEC000 A game labels Nov 6, 2026
@lunks

lunks commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

@TheekshanaA thanks, this is the cache we hoped for. Three things:

  1. An independent measurement that agrees with yours. The same idea built on our side (an M5 Pro, a Hyrule Field save, 720p with the temporal scaler): GPU p50 from 15.65 to 13.6–13.9 ms, with the same picture.
  2. One bug we hit that your hook order may avoid. A pass that adds a local before a task or mesh kernel's stage memory broke dmsh_meshstage's _KERNEL match for 109 shaders, so they were left unconverted. raw_ranges runs after the mesh rewrite, so I expect it doesn't. Could you confirm that on your build, for example by checking that the mesh and task shaders that contain the helper still compile with their stage rewrite intact?
  3. The pool-table change on oot-pool-table-notes (about 53 s of a seven-minute walk spent building pool tables on main, 61,000 builds) is worth a pull request. If it needs Add a Switch 2 profile for The Legend of Zelda: Ocarina of Time #1387's patching, a PR that carries what it needs, or one that describes the dependency, would be welcome.

@TheekshanaA

Copy link
Copy Markdown
Contributor Author

@lunks thanks, and good to have an independent number.

2. Confirmed for the mesh and task shaders. I ran this branch's dmsh_build.py on the 109 Ocarina shaders that are mesh or task stages and contain the helper (98 mesh, 11 task), passing them as keys:

  • Translation: 0 failed.
  • Stage rewrite: all 109 keep their [[mesh]] / [[object]] entry points and their stage body, and all compile.
  • Same output: each one is byte-identical to raw_ranges applied to the stock build's final MSL for that key.

So the hook does see the source after dmsh_meshstage.to_stage: it runs just before compile_targets(), after the stage, vertex-entry and fragment-variant rewrites, and _KERNEL never sees a cache local. Those 109 libraries were also among the loose ones in the measured runs.

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.

@TheekshanaA

Copy link
Copy Markdown
Contributor Author

@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 oot-pool-table-notes: two commits with the unit test (t_pool_notes) on top of #1387's patching. On main, the same route spent about 53 s of a seven-minute walk building pool tables (61,000 builds). If nobody has picked it up by the time I'm back to it, I'll port it then.

@PucaVaz

PucaVaz commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

@TheekshanaA I'll take the pool-table port. I'm rebasing your two commits from oot-pool-table-notes (9579768, 9d486f8) onto current main, where they conflict in nvn_mtl.m with the Ocarina base port. I'll open the PR with you as co-author, with t_pool_notes and pool-table build counts before and after from an Ocarina run.

seedds added a commit that referenced this pull request Nov 6, 2026
…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>
fpedrolucas95 added a commit to fpedrolucas95/nxnative that referenced this pull request Nov 6, 2026
…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>
@seedds

seedds commented Nov 6, 2026

Copy link
Copy Markdown
Owner

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 (GlobalToStorage.TryGenerateRawStorageOp) instead of rewriting the MSL text. That way it also covers stores and atomics, and it can't silently stop applying if spirv-cross's output changes shape. Each helper keeps the entry it chose last in a private word of the invocation. The mesh and task bug @lunks mentioned is fixed in dmsh_meshstage.py in the same commit.

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:

  • We haven't compared your register-held range with main's approach on this Mac, so it could account for part of that gap. If you measure it on main, the numbers are welcome.
  • Frame time p95 over the walk went from 19 to 26 ms. The runs ended in different places, the game's dynamic resolution took the headroom and 8 GB of swap was in use, so we don't yet know whether this change caused it.

The pool-table port is with @PucaVaz, who said above that they'll open it as its own PR.

@PucaVaz

PucaVaz commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

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

javierdv7 pushed a commit to javierdv7/nxnative that referenced this pull request Nov 6, 2026
…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>
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
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
@gnosticgeek gnosticgeek mentioned this pull request Nov 6, 2026
3 tasks done
john62036203-ops added a commit to john62036203-ops/NXNativeWindows that referenced this pull request Nov 6, 2026
…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.
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
…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>
seedds added a commit that referenced this pull request Nov 6, 2026
…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>
cheemu21 pushed a commit to cheemu21/kid-with-sword-remake that referenced this pull request Nov 6, 2026
…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)
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
…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>
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400486024DEC000 A game nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) shader chain The shader translation chain (DMSH_CHAIN): a change rebuilds every game's shaders

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants