Skip to content

Reduce Ocarina of Time compute cost with shared raw-memory lookups - #1405

Closed
nanotchec wants to merge 5 commits into
seedds:mainfrom
nanotchec:codex/raw-global-load-quads
Closed

nanotchec wants to merge 5 commits into
seedds:mainfrom
nanotchec:codex/raw-global-load-quads

Conversation

@nanotchec

@nanotchec nanotchec commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Current applicability

Main now uses a private per-invocation cache for raw pool lookups (commit 0f510fd, following #1464). Its scalar helper takes a fourth argument, the private cached entry. This PR still matches the earlier three-argument helper. Running the current PR pass on the post-cache Ocarina of Time leaf compute source (50d3050cfa7cded5) leaves all 253 calls byte-identical and generates zero four-word groups. This also affects the PR's previous merge base c229bd7, which already contained the private cache.

The conflict against main b2d46b8 is limited to the generated shader-chain header. Regenerating it would leave the compatibility gap. The measurements and output validations below document the earlier local integration and pre-cache shader helper. Incremental benefit over main's cache remains unmeasured.

Earlier implementation

Ampere wide loads can arrive in Metal as four scalar raw global loads. Each word independently searches the guest pool table before reading the translated address. This change adds an experimental build pass that lets four adjacent reads share one lookup.

Enable it with DMSH_RAW_LOAD_QUADS=1 when generating the shader chain header and building the libraries and matching host. It is off by default. The mode is part of the shader chain and cache fingerprints.

The pass is limited to ordinary Ampere compute. It verifies the reflected table binding and the fixed-table scalar helper, then recognizes four ordered 32-bit reads at offsets 0, 4, 8 and 12 with only address copies between the reads. The fast path requires a complete, four-byte-aligned 16-byte span inside the winning interval, before any next start. This preserves the scalar resolver's priority when entries overlap. Other cases call the original scalar resolver four times. Kernels with a raw write-log binding stay on the original path. Writes to ordinary bound resources remain supported.

Historical measurements

Local Ocarina of Time v131072 testing used an Apple M3 Max, macOS 27.0.1 and Xcode 27.2 (main build ID e0f3ccfaf50065ae4ea01eb5aeb13b1a). With only one compute library replaced, same-frame comparisons saved about 1.16 ms in the initial compute encoder. The forward window showed 28.048 to 29.050 fps, and the reverse run had different visibility.

With the full opted-in chain, the initial compute encoder saved about 2.3 ms at the same frame and dispatch signature in both test orders. Whole-frame GPU medians, weighted over matched pass counts and shown frames, improved by 1.98 ms forward and 1.80 ms in reverse. The forward run showed 28.235 to 29.602 fps with all 651 frames displayed. The reverse candidate displayed 647 of 651 frames, with four drops.

At the direct-present 30 fps target, median enqueue-to-display time increased by approximately 61 ms in the forward run and 39 ms in reverse. This interval begins after input and simulation. The pass remains off by default while presentation pacing is investigated. The installed app was left on the stock chain.

These measurements used the earlier local Ocarina integration and its 8,192-entry table. The original PR base 865cc35 used a 4,096-entry fixed table. The update onto 88c7e03 adapts the pass to the official Ocarina profile and its tagged 8,192-entry table. The timings above do not measure that updated base.

Historical validation

  • On the updated 88c7e03 base, the actual Ocarina leaf compute shader 50d3050cfa7cded5 compiled with and without the pass. The adapted pass preserved the tagged hint, the 13-step fallback search, and the original scalar fallback. It transformed 232 of 253 scalar calls into 58 groups, with identical reflection apart from the chain identifier. Synthetic matcher, chain-header, documentation and pinned Python lint checks passed. The updated base still needs a full opted-in shader build and runtime comparison.
  • Synthetic matcher tests cover both fixed table sizes, unsigned carry, optional chain selection and rejection of unexpected helpers or intervening side effects. The CPU resolver oracle compares each word and the physical read sequence across 96,161 synthetic inputs.
  • On the earlier local integration, the 64-thread twin replay matched all 18 whole output buffers against the original kernel with identical inputs. That base’s pass-generated metallib is byte-identical to the metallib used in this replay and the forward and reverse comparisons.
  • Shader-chain and code-fingerprint tests, the targeted Python lint comparison and the graphics test suite on the local Mac passed. The graphics suite used the Super Mario Bros. Wonder profile for its fixtures.
  • Both local chains built all 42,853 shaders. Static regression comparison of stock 670e154b2faa and opted-in ef214d417e26 found 552 changed compute shaders, with no new compile failures, reflection changes, variant changes or pipeline-interface regressions.
  • Five changed compute shaders passed all 30 same-input whole-output comparisons across three Kokiri frames, covering 1,112,064 bytes in each comparison arm. The test covers these dispatches and their observed inputs.
  • The local Ocarina full regression smoke check completed with only the expected shader changes. Both runs reached frame 3350 within the 145-second deadline and completed the 165-second route. The two Kokiri screenshots were within the harness tolerance, and the candidate had no new error categories.
  • The Link’s Awakening 1.1.0 static negative control passed with 9,698 shaders on both revisions, zero changed MSL and zero new pipeline-interface findings. Its 26 packaged compute records are NVN1, outside the opt-in Ampere path. No gameplay run was performed for this control.
  • The repository impact scan flagged three Switch 1 usage manifests through broad NVN shader rules (Breath of the Wild, Pokémon Scarlet and Tears of the Kingdom). The pass itself requires an ordinary Ampere compute shader. The scan had no usage manifest for 141 profiles, so it does not establish affected shader counts or runtime coverage for those games.

Ocarina is the only locally available game that reaches this opt-in Ampere path. Other Switch 2 games have not been run. The functional regression below used the existing installed game modules and two matched local renderer snapshots; its time-window cadence is separate from the controlled measurements above. CHANGED is expected for the 552 compute sources; there were no new failures or interface changes.

Full local Ocarina regression report

regress.py check (full): CHANGED

Baseline installed-source 655ec95e414e (+ uncommitted changes), candidate raw-load-quads-source 655ec95e414e (+ uncommitted changes); Apple M3 Max, macOS 27.0.1, Xcode 27.2; 545.6 s

game status shaders (base → cand) newly failing newly building changed MSL interface (new / known) boot fps p50 frame p99 ms peak MB prepare s new RED
Ocarina of Time 0400486024DEC000 CHANGED 42853/42853 → 42853/42853 0 0 552 0 / 0 ok 27.4 → 28.5 59.21 → 41.84 14789 → 14877 0.36 → 0.62 0

Ocarina of Time 0400486024DEC000: CHANGED

  • CHANGED: 552 shaders changed (MSL 552, reflection 0, variants 0)
    • changed shaders: 001f62d77a5447e6, 00359b6009aed142, 0041cb1d10d87b8c, 0193bf98ac2101db, 020fdeffc388ba6d, 02f639fd402d35ba, 0342ab5e045200ee, 039de553a54efefc, 03a0adf10e22ddc9, 03b4b2bd24119d23, 03e1d6d1364b6f66, 04755edba751a394, 049d5dc0ec7c210c, 04e1a4df6753b760, 050869474dbd21e5, 058b08b97630e7db, 05c7844dba186699, 0628b8488becf892, 064e18cd4b24e333, 0697f6e16bf526f9, 06bcedbac50aaf10, 06cfa96f4a317e98, 06ed8b50947110fc, 06f48465990a41b8, 08cc06c7e15267c4, 08d0392fe2401bb9, 08e6000a09048dcb, 091a75326533bbd8, 0937361d55942510, 0961b791a728035a, 09a560381d118cc3, 0a18567e6b12210b, 0a2d45552b89c293, 0a516d3f55e17ebe, 0b8a29367c147be5, 0c0a96c73c61723f, 0c56e04c5ccd88a5, 0cb0612e43b1fed9, 0e664f4cf7eb6ef2, 0e6ab77faa833e34 …
    • screenshots (s: dHash bits / RGB diff / baseline noise): 126: 2/2.38/0, 143: 0/1.52/0
    • baseline: exit None, first frame 2.0 s, max frame 4543, reached True, prepare 0.36 s, fps 27.4, p50/p99 37.27/59.21 ms, peak 14789 MB
    • candidate: exit None, first frame 2.5 s, max frame 4683, reached True, prepare 0.62 s, fps 28.5, p50/p99 36.21/41.84 ms, peak 14877 MB
    • time: chain 47.9 s, smoke 497.3 s
Link’s Awakening static negative control

regress.py chain passed on Apple M3 Max, macOS 27.0.1 and Xcode 27.2 in 784.7 s. Baseline 865cc35fad70 used chain 223f10d34f97. Candidate 3fe33bc3997a used chain 5ba43d2a3f37. The worktree’s only uncommitted change was regenerating dmsh_chain.h with DMSH_RAW_LOAD_QUADS=1.

Game Shaders, base → candidate Newly failing Changed MSL New interface findings
Link’s Awakening 01006BB00C6F0000 9,698/9,698 → 9,698/9,698 0 0 0

Front-ends: This is an optional build-time experiment. Keep DMSH_RAW_LOAD_QUADS=1 consistent for chain-header generation, shader libraries and the matching host; it is not a runtime-only switch.

Front-ends: DMSH_RAW_LOAD_QUADS=1 is an optional build-time experiment. Use the same setting for chain-header generation, shader libraries and the matching host. It remains disabled by default.
@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 labels Nov 5, 2026
@github-actions github-actions Bot added the tools Tools and scripts (dm/tools, scripts) label Nov 5, 2026
@nanotchec
nanotchec marked this pull request as ready for review November 6, 2026 00:45
fpedrolucas95 added a commit to fpedrolucas95/nxnative that referenced this pull request Nov 6, 2026
…eedds#1405, opt-in DMSH_RAW_LOAD_QUADS=1), extended to this tree's hinted pool table

seedds#1405's matcher knew the fixed table walked by an unrolled binary search and addresses written as base + 4 / 8 / 12; this tree's translator writes the hinted table
(DMSH_GMEM_HINT_SHIFT: the 2 MiB chunk's hint, else a search over the count in entry 0) and Ocarina of Time's words as as_type<int>(x) + 16..28 with a carry each, so on
the chain built with the option none of the 727 compute shaders with raw loads changed. The pass now verifies the hinted helper line by line, takes addresses as a 64-bit
value plus k, k + 4, k + 8, k + 12, and in the hinted form shares the lookup only within one chunk and reads the next entry only below the table's count. A sampled
shader gets 59 shared lookups and compiles; not measured in a game. Off by default; the option changes the chain version (dmsh_chain.py, dmsh_build.py passes the shift).

Front-ends: DMSH_RAW_LOAD_QUADS=1 must be the same for dmsh_chain.py --header, the shader libraries and the host build; it is not a runtime switch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TheekshanaA

Copy link
Copy Markdown
Contributor

Some measurements that may be useful for this PR: in Kokiri Forest the raw-memory lookups cost the most in mesh and vertex shaders, which this pass leaves out. A small per-invocation range cache covers every stage.

Setup: MacBook Pro M5 Pro, macOS 27.2, Ocarina of Time v131072 built from #1387 (chain 702900bd9404, helper in its do … while form), a scripted route into Kokiri Forest, DM_NVN_GPUPROF on one frame (6500).

1. The foliage draw dominates the frame when the game's dynamic resolution is active

With DM_NVN_GPU_TS=segment, the game sees real GPU times and drops its 3D target to 683x384, but one 52-draw pass at that size still took 10.9 ms. That pass uses mesh shader 73dc0b90c4082e43_b (Foliage.Product.110, 135 raw-memory lookups, each a binary search over the GMEM map) with fragment 4a1bf8a23dd76493_b. DM_NVN_SKIP_FS=4a1bf8a23dd76493 brought that frame's render encoders from 22.2 ms to 6.2 ms.

The foliage reads its per-blade data word by word (+0, +4, +8, …) from one range. So I let each invocation keep the range its last search found and skip the search when the next address falls inside it:

struct dm_gmc { ulong s; ulong e; int i; };
// in the helper (int _N(lo, hi, table, thread dm_gmc& c)), around the binary search:
ulong a = as_type<ulong>(uint2(uint(lo), uint(hi)));
int idx = 0;
if (a - c.s < c.e - c.s) idx = c.i;
else { /* the original do … while search */
       int r = idx * 8; c.s = as_type<ulong>(uint2(t._m0[r], t._m0[r + 1])); c.e = as_type<ulong>(uint2(t._m0[r + 2], t._m0[r + 3])); c.i = idx; }
/* the original end check, delta and read, unchanged */

It relies on the GMEM map's ranges being disjoint, as nvn_mtl.m builds them. The cache lives in one invocation, so the table can't change underneath it.

With only that shader replaced (the host takes a loose b-<chain>/<key>_b.metallib before the .img), the 683x384 foliage pass went from 10.9 ms to 2.1 ms. At the full 1536x864 target (default timestamps), it went from 8.3 to 7.6 ms; there the grass fragments dominate.

2. The helper is everywhere

Of the 42,853 shaders in this build, 2,234 contain it: 801 vertex, 719 compute, 605 fragment and 98 mesh. Some call it 971 times (the largest foliage mesh shaders) or 759 times (a family of vertex shaders). This PR's pass covers ordinary compute only, and it looks for the unrolled fixed-table form, so it matches none of the shaders in this build.

With the cache in all 2,234 helpers (loose metallibs over the .img, same host, same route, default timestamps, game at its full 1536x864 target):

stock range cache everywhere
frame time p50 / p95 23.7 / 25.5 ms 16.7 / 18.0 ms
GPU per frame (p50) 23.4 ms 13.9 ms
frames dropped on screen per 300 81 9

So the game holds 60 fps there. Screenshots from before the player moves (frame 4350) match the stock build: same geometry, foliage and NPCs.

Also worth knowing: this game's speed seems to follow its frame rate. At 42 fps the same timed stick input covered noticeably less ground than at 60 fps, so below 60 the game appears to run in slow motion.

3. One more observation

DM_NVN_GPU_TS=segment lets the game's dynamic resolution react, but with GPU times this high it settles at 683x384, roughly a third of the resolution per axis, for about 2 ms of frame time.

Separately, I have a host change that stops memory pool add/remove, retired IDs at a new submission and storage takeovers from rebuilding the pool tables whole. It brought a six-minute walk from about 9,600 table rebuilds (26 s) to 14 (0.3 s), with a unit test. It builds on #1299 / #1387's patching. I can open it as a PR if that's useful.

@lunks

lunks commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

@TheekshanaA thanks for the numbers: the per-invocation range cache (10.9 → 2.1 ms on the foliage pass, 23.7 → 16.7 ms p50 over the route) and the host change that cut the pool-table rebuilds from about 9,600 to 14 are both very welcome. Could you open each as a pull request, or push them to a branch and link it here? For the cache, the helper's text (where dm_gmc is declared and which generator emits it) is what we need, and the loose-metallib way you tried it is fine as the test. #1360 also changes the search loop of that helper (a count-driven loop), so it would sit around it.

@TheekshanaA

Copy link
Copy Markdown
Contributor

@lunks thanks! Both are up now.

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>
Aeonitis pushed a commit to Aeonitis/nxnative that referenced this pull request Nov 6, 2026
…er and shader chain, the branch's host services, movie clock, packing and Ocarina of Time additions

Upstream reached about 60 fps in Kokiri Forest with its own versions of most of the branch's merged PRs, so where both changed the same code upstream's is kept:
- The shader chain is upstream's (translator, lock, dmsh tools, Ryujinx sources), plus the branch's bracket-depth flattening (dmsh_depth.py, test_dmsh_depth.py); chain c732699d4fdb.
  Not carried: the LDSM subgroup fence (seedds#1376, needs a translator rebuild; upstream's HMMA/LDSM rework reports clean movies) and the shared raw-memory lookups (seedds#1405, superseded by upstream's last-entry lookups).
- The renderer (dm/gfx/nvn, dm/gfx/msl) and its tests are upstream's. Not carried: seedds#1390-seedds#1394, seedds#1404, seedds#1408, seedds#1409, seedds#1417, seedds#1418, seedds#1420, seedds#1427, seedds#1320, seedds#1331, seedds#1332, seedds#1368, seedds#1285 renderer parts, pool_skip_retired, and the branch's warning fixes and prewarm progress there. They stay on oot-integration.
- Ocarina of Time's profile is upstream's, with the branch's 12 movie compute rows in pso_manifest.tsv, PLAN.md and PR-AUDIT.md.
- shader_split.py is upstream's plus the BAHS record check (test_split_bahs_records.py).
- Host: upstream's EventFd, PlayReport send, audio lock and vibration answers; the branch's service sessions (hle_sf, hle_prepo), sync waits, AV1 movie clock and parallel decode, diag_abort frame fix and focus control are kept.
- Scripts: shader.noindex, tmp.noindex, notes outside the shader key, bench and regress changes.

Checked: the Mac host builds (GAME=0400486024DEC000); host unit tests pass; the Python tool tests pass on Python 3.13 except test_dmsh_pool_arrays, test_nro_pack, test_nsp_dump and test_nsp_incremental, which fail on upstream main as well. Not run: the full shader build on this chain, gameplay.
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>
@nanotchec

Copy link
Copy Markdown
Contributor Author

Closing this experiment because its rewrite no longer applies to the current translator output, rather than resolving only the generated chain-header conflict and presenting it as integration-ready.

Rechecked against main d8b2b64: raw_load_quads recognizes the old three-argument raw-memory helper. Main has used the per-invocation last-entry cache since 0f510fd, with an additional private cache argument, and now also has translator-level vector loads covered by test_dmsh_raw_last.py::RawVector. The PR body already records that its rewrite left all 253 calls in the post-cache Ocarina leaf shader unchanged.

This does not establish that every potential benefit of grouping loads has been exhausted. It means this patch, as written, does not deliver that benefit on current main. Its historical GPU savings were measured on an earlier chain and also came with worse enqueue-to-display latency. Any successor needs a new implementation and a fresh correctness/performance comparison against current main; those older results should not be reused as current validation.

The experiment and measurements remain available in this PR and its branch. No installed game or saves were changed during this audit.

@nanotchec nanotchec closed this Nov 9, 2026
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 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 tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants