Skip to content

Release 0.13.0: performance release - #37

Merged
gnosticgeek merged 65 commits into
mainfrom
release-0.13.0
Nov 6, 2026
Merged

gnosticgeek merged 65 commits into
mainfrom
release-0.13.0

Conversation

@gnosticgeek

Copy link
Copy Markdown
Owner

Requested by Tom · project thread

Summary

A performance release; full notes are in CHANGELOG.md under 0.13.0.

Testing

  • Translator rebuilt and host built on the Mac; all host and gfx tests pass except test_host_msl and test_uepak_iostore, which also fail on 0.12.0.
  • t_codec_video (MKW fix) passed on the Mac.
  • scripts/release_check.py lint and check 0.13.0 pass.
  • A full from-scratch Ocarina of Time build on 0.13.0 is finishing on the Mac; the shader stage completed (42,848 shaders). Released ahead of it at Tom's request; anything it turns up goes into 0.13.1.

Generated by Claude Code

PucaVaz and others added 30 commits November 6, 2026 00:09
…leaves the files a shader build writes alone

After a full Ocarina of Time build, mds_stores stayed at 100-156% CPU for
more than 40 minutes with nothing else running, and mdfind found the build
folder's .metal sources. Spotlight indexes every file under the home folder
and reads the text of the .metal sources. The shader step writes a file per
shader variant into shader/cache/b-<chain>/ before it packs them into one:
195,157 for Ocarina of Time, 42,850 of them .metal. That build was made
before the libraries and the compile cache became one file each, so its
folder kept them, with 311,903 more files in the cache. On macOS 27 only a
folder whose name ends in .noindex keeps Spotlight out; a
.metadata_never_index file inside does not (lunks, PR seedds#1355, which names the
temporary folder so).

The shader folder is SHADER = 'shader.noindex'. A shader/ from before moves
there before any step reads it. A rename keeps every file and its times,
and no step's key holds the folder's path, so the split, the libraries and
the compile cache are reused and nothing is translated again. The first
build after the move packs the Mac app again, as pack_app.py's key names its
inputs' paths. When both names exist (an older checkout made shader/
again), shader/ stays and a [NOTE] line names it. bench.py, regress.py and
bug_report.py read either name.

Measured on a MacBook Air M4, macOS 27.2, with APFS clones of that build
folder's subfolders in a folder Spotlight indexes, as the CPU of mds,
mds_stores and mdworker_shared above idle until it settled:
- a sixteenth of b-<chain>/ (11,973 files): 152 s, at up to 772%; about
  2,500 s for the whole folder a shader build writes. Deleting them: under 1 s.
- a sixteenth of the old compile cache (19,839 files): 210 s; about 3,300 s
  for the whole.
- pso/ (1,162 files) 33 s; patched_*/, profile/, tools/, font/ and app/
  (45 files) 4 s; logs/ (15 files, one of 277 MB) 3 s; extract/ (a 26 GB
  RomFS.img) 2 s.
So the shader folder is where the indexing is, and the other folders keep
their names: logs/ and profile/ are also paths front ends read. A game
benchmark ran in another session during most of this.

Checked: CI's tool-test loop passes but for test_tree_tar.py's
test_packs_a_quiet_folder_and_keeps_its_attributes, which fails the same on
main on this Mac; lint_python.py has no new finding. The shader package
and keys of that build, cloned: pkg.outputs.key matches split_output_key
before and after the move. No build was run with this change.

Front-ends: interface: the build folder's shader folder is shader.noindex/ instead of shader/ (cache/, ios/, vk/, gl/, pkg/ and the keys inside it, and the Mac run command's DM_MSL_DIR); a shader/ from before moves there at the next build.

_Drafted with Claude; reviewed by me._
… deadlocks against the zstd command's pipes

Feeding the zstd CLI through stdin while capturing stdout deadlocks on macOS once a frame is larger than the pipe buffers: the worker's blocking write waits for zstd to read, while zstd's output writer waits for the worker to drain stdout. Ocarina of Time's sound archives hit it at 88% of the split (164,256 of 186,587 files), and the build hung for good. compression.zstd (Python 3.14, backports.zstd before) is already used for dictionary frames; it decompresses frames without one too. test_split_zstd.py covers files and bounded RomFS entries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
wake() signalled the woken thread's condition while the waker held G, so the woken thread woke into G still held
and slept again on it (_pthread_mutex_firstfit_lock_slow under its cond_wait: 5-7% of Breath of the Wild's main
thread on the Mac). wake() now lists the thread (up to 8 per waker) and g_unlock signals it after the unlock; a
thread that waits on a condition with G signals its list first, and thr_free waits out a signal in flight.
BotW, Mac, three A/B pairs: lock slow path 51-70 -> 18 samples, guest threads 503-512 -> 494-498 ms/s of CPU
(main thread 112-114 -> 108-110), CPU energy 1.21 -> 1.18 W. DM_WAKE_UNLOCKED=0 restores the old order.

Discord: https://discord.com/channels/1491224015434678394/1524915513606803549
tex_mtl_'s stale sRGB / linear view check asked conformsToProtocol:MTLTexture of every serial-table object; it walks
the class's protocol lists. obj_is_tex keeps the answer for the last 8 classes (the serial table holds NvPkg and a
few texture classes). Also mtl_tex_gpu_owned, the GPU-owned test and tex_upgrade's holder check.
With no segment on the GPU and DM_NVN_PASS_SPLIT draws (256) encoded since the last cut, the open pass is ended
(attachments stored by rp_end as at every other mid-pass end), the segment committed, and the next draw opens the
pass again with loads; at most DM_NVN_PASS_SPLIT_MAX (2) splits a pass. A pass reopened by a split does not run
the dispatch-hoist scan again.
…epare after one look; the tile plan looks textures up only for attachments

fb_prepare and fb_tile_plan looked up every fragment texture binding of every draw (a lock and a texture each,
with a retain and release in the plan). A first pass over the bindings' serials under one lock returns for draws
that sample none of the pass's attachments, and the plan takes a texture only for a binding that names one.
Mac, BotW, snapshots on: about 12,020 -> 11,930 instructions a draw (11,030 with DM_NVN_FB_SNAP=0).
…e those slots

72% of the argument tables BotW writes follow only a uniform buffer rebind on that stage. A per-stage
generation of every other argument change (g_args_ogen) lets such a table be copied from the shader's
last one in the pass, with only its uniform buffer slots made again (not for shaders that read handles
or pointers out of a uniform buffer, cb_only). The geometry and mesh paths that force a stage's table
again move the generation too. DM_NVN_CB_CHECK=1 writes the full table as well and logs a difference;
DM_NVN_CB_FAST=0 turns the path off. Measured on the Xipzer fork (BotW, Mac, encode thread on):
11,090 -> 10,670 instructions a draw (-4%).
Each returns an object something else already holds: the serial table (tex_put, or the NvPkg in it)
for tex_mtl, rp->view for tex_by_id, g_enc for rp_begin_ex. Their returns go through void * and a
macro bridges them back, so no retain / autorelease / claim per call. Objects replaced in those tables
are released after the GPU is done with them (defer_obj_release), so a returned pointer outlives the
replay that asked. Measured on the Xipzer fork (Mac, BotW at 130%, encode thread on): tex_mtl about
-2% instructions a draw, tex_by_id and rp_begin_ex 10,565 -> about 10,440.
…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>
…shaders: Ocarina of Time's 222 such pools, 334 MB, which the GPU never reads

keys_hash (hle_nvn.c) checks that a shader's code lies in a memory pool before it
hashes it, and asked mtl_addr, which also puts the pool into the residency set.
The shaders run translated, so the GPU never reads that code. mtl_pool_holds is
the same pool lookup without the residency; a pool the GPU reads through a
binding or an address is still made resident there.

t_pool_holds checks the lookup and that asking makes no pool resident. The
reference entry is in docs/reference/memory.md, with its line in the symptom
index and Ocarina of Time's note.
…d of a compare-and-swap loop

A task or mesh shader writes its primitive indices and count as bytes, which the
translator's shared memory byte store turned into a compare-and-swap loop on the
word. The 32 lanes of a workgroup write neighbouring bytes, so four lanes contend
for each word and the loop ran about four times a store, with a divergent retry
each time. Ocarina of Time's foliage mesh shaders have ten such stores a lane over
tens of thousands of workgroups a frame: 8.3-8.6 ms of GPU a frame on an M3 Pro,
a third of the frame, whatever the resolution.

GenerateSharedStore now clears the field with an atomic AND and sets it with an
atomic OR. Lanes writing other bytes of the word commute with both, so the word
ends as the loop left it, without retries. Two lanes writing the same byte at once
get a mix of their values where the loop gave one of the two; the hardware's order
between them is undefined as well. The helper also serves 16-bit stores and the
shared memory of compute shaders. A chain change (7b8c971669dc): translator
rebuilt, lock and header updated, the StageMemory test checks the new form.

DM_NVN_SKIP_MESH=1 drops every task / mesh draw, as DM_NVN_SKIP_FS drops draws by
fragment shader: how the cost was isolated.

Measured on an M3 Pro (18 GB), Kokiri Forest, the pass holding the foliage:
8.3-8.6 ms -> 3.5-4.0 ms a frame; the game's resolution controller then holds
1440x810 at 48 fps where it held 854x480 at 36-40 fps.
…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>
…logue and first control at 30 fps with no red line (profile 0100276009872000, note, games table row, gallery)
…e title music for minutes: pso_manifest.tsv seeds the prepare step

On a Mac that has no pso_user.tsv yet and a cold Metal shader cache, the first title frame built 11 pipelines and loaded 12 shader libraries inside one submission: 252.6 s on an M3 Pro (macOS 27.2) with MTLCompilerService at 100 %, while the window kept presenting the last frame, black, and the title music played (issue seedds#1477; the same symptom reported on an M4 Max). The profile's seed list was header-only, so the prepare step had nothing to build before the first frame.

The seed list is this Mac's pso_user.tsv, recorded on v131072 (chain adec7d81d970) with no lists present, so everything was built on the spot: 9,620 lines (8,376 shaders, 1,112 render pipelines, 132 compute pipelines) over boot, the title and its movie, a new game and the opening, Link's house, Kokiri Forest and a save load. With those built behind the Preparing-shaders screen, or in the archive, the same start reaches the title in about 20 s.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ture: the comment says what holds that, and t_pool_notes checks a live sRGB / linear twin

alias_take_over skips an entry a live serial holds (serial_holds on g_by_serial) and only unrefs one a finalized
serial holds through g_ghost, so no serial resolves differently. The comment said only a retired ID could still
hold a view of an evicted texture; a live sRGB / linear twin can too, as its serial holds a view of the shared
texture rather than the texture. tex_mtl_ finds such a view stale once its parent has left the sharing table and
drops it (serial_del), which notes that serial, so the patch takes the twin's ID again.

t_pool_notes gains that case (81 checks, 0 failed): ID 8 is the sRGB twin of a texture whose ID was finalized;
a texture of another description takes the storage over and evicts the shared texture; in that submission the
twin's view is dropped and made again, no table is built whole, no ID goes down the slow path, and the patched
table matches a table built whole.
…rina of Time no longer aborts when a save loads with DM_NVN_ENC_THREAD=1 (the iOS default) (PR seedds#1458)

nvn_geometry.inc encodes an indirect task / mesh draw with
drawMeshThreadgroupsWithIndirectBuffer:threadsPerObjectThreadgroup:
threadsPerMeshThreadgroup:. With the encode thread on, the encoder is the
recorder DmRecEnc, which had a stand-in only for the direct mesh draw, so
forwardingTargetForSelector: stopped the host (SIGABRT under mtk_encode) at
Ocarina of Time's first indirect one: on an iPad, and on the Mac with the
encode thread on (issue seedds#1265).

RO_R_MESH_IND records the argument buffer's GPU address and the threads per
object and mesh threadgroup, as RO_R_MESH does for the direct draw, and
replays the indirect call. t_rec_draws (CPU only, now in the CI list) checks
that the recorder has a stand-in for each of the seven draws the NVN layer
encodes; it fails on main and passes with this change.

Co-authored-by: Adrian Smith <242086076+aksmith-git@users.noreply.github.com>
…s, and A loads where its LDSM read it: Ocarina of Time's title movie network takes 11.4 ms of compute a frame instead of 14.8 on an M6 (26.7 to 18.9 on an M4; chain 23a1ea1db092, PR seedds#1456)

Each dmsh_hmma16816_f16 call took A, B and C through the SIMD-group's scratch block and D back through it, three barriers a call, although most of these HMMAs continue an accumulation nothing else reads, share A with the call before, and take A straight from an LDSM. TensorPass.Prepare now gives each HMMA flags (an accumulator slot for a chain, A kept, C = 0, A loaded where its LDSM read it), each a promise about values the translator's own body computes anyway; dmsh_build.py's hmma_msl keeps the slots and A's matrices in each calling function's registers and loads A from the caller's shared memory. An LDSM in a compute shader is a MatrixLoad a matrix until then.

The divergence analysis that decides whether an LDSM's row address is the same in every lane plus a known per-lane part also treats a loop header's phis as divergent when the loop has several back edges and any branch in it depends on the lane: lanes that took different back edges bring the header different values. It changes none of Ocarina of Time's 71 HMMA shaders.

Checked: test_dmsh_hmma.py (GPU against a CPU reference, and the translator's flags), the graphics tool tests; Ocarina of Time's 42,854 shaders build as on main (7 + 2 known failures), title A/B on an M6 (compute 14.78 to 11.41 ms, GPU span 15.63 to 11.87), title and opening movie clean on both hosts; Donkey Kong Bananza's 23,638 shaders build, its opening, cutscenes and first punch render cleanly with no red line (its NDSR upscaler takes the chain flags). Not run: gameplay A/Bs, the iPhone.

Co-authored-by: yangyidong <63278156+sheepyd@users.noreply.github.com>
Keep bound color and depth descriptions in queue-owned storage before a submission can publish completion. Reusing completed command memory no longer overwrites the texture snapshots retained by queue state.

Add NVN/Metal pixel regressions for inherited bindings, multiple queues, nested recordings, views, depth and the last color attachment. Document the Rhythm Heaven Groove kaeru crash and the verified fixed Mac app.

AI-assisted.
…ed texture in one lookup instead of walking the serial table: Ocarina of Time's long frames in Kokiri Forest fall from 332 to 212 (PR seedds#1439)

For each sharing-table entry a takeover could remove, alias_take_over called serial_holds(&g_by_serial,
...), a walk over the serial table's whole capacity. With thousands of serials and shared textures, every
new shared texture walked the table once per overlapping entry, and in Ocarina of Time, where the texture
pool's walks make hundreds of textures in a submission, that walk was the submit thread's hottest function
while frames took up to a second.

A count per object of the serial-table entries holding it (g_by_serial_held) is now kept by every writer
of the serial table (serial_put, serial_del, tex_put and the upgrade's in-place replacement), and
alias_take_over asks it in one lookup. An entry holds its object, so a counted address is not reused
while counted. The reference entry notes that a change letting objects hold shared textures another way
(PR seedds#1404's views and packaged members) must count those holds too.

t_alias_takeover: live-held entries stay, unheld ones go after ALIAS_IDLE submissions, two serials keep
an object held until both go, and a takeover past 1,000 held entries with 20,000 serials takes 0.03 ms
instead of 4.3 ms. Ocarina of Time on an M4 Pro, loading a Kokiri Forest save through Saria's dialogue
and a walk: a host asking both the count and the walk compared them 393,216 times with no difference;
[longframe] lines 332 -> 212, frame p99 72.8 -> 38.8 ms (one run each, hosts on the build's chain at
88c7e03). Measured by the author on an M5 Max and by a commenter on an M3 Max (frames over 100 ms
85 -> 2). Regress: Breath of the Wild's build is in the packed layout regress.py does not read, so
Ocarina ran instead.

Co-authored-by: enjoyerofslop <281774948+enjoyerofslop@users.noreply.github.com>
…assification goes: a pool walk in between handed out an NvPkg as the texture, and Ocarina of Time stopped with "-[NvPkg buffer]: unrecognized selector" in tex_by_id (PR seedds#1420)

nvnMemoryPoolFinalize (a game thread) ran mtl_alias_drop_pool, which dropped the pool's textures from the CPU-written set (cpu_tex_drop_pool) before it marked the pool's texture IDs gone under g_rmu (treg_drop_pool_views). A pool walk on the submit thread (pool_gpu -> pool_entry -> tex_by_id) holds g_rmu; an ID of that pool reached in between was not gone, the pool's magic was still set and the texture no longer CPU-written, so tex_mtl_ skipped the packaged path and returned the texture table's entry as it was: the NvPkg wrapper, not an MTLTexture. tex_by_id's base.buffer threw. Reported on PR seedds#1299 (M3 Max, 412 s into Kokiri Forest, right after a save) and on PR seedds#1420 (M4 Pro, Deku Tree, about 40 minutes).

The IDs are now marked gone first: a walk holding g_rmu finishes while the classification is still there, or finds the ID gone and takes tex_made, which unwraps the NvPkg. PR seedds#1420 found where the NvPkg escaped and unwrapped it in tex_mtl_; that guard is not taken, as the state no longer arises.

Checked: t_cpu_tex --metal 68 checks, 0 failed; its new finalize_during_walk (holds g_rmu while another thread finalizes the pool) fails 2 checks without the fix. t_cpu_storage 32, 0 failed; test_docs. Ocarina of Time, the save_ref village walk (about 230 s) with this host and with main's on the same chain and libraries: no crash on either, the same three red lines, p50 15-16 ms on both. The crash itself was not reproduced.

Co-authored-by: Pedro Nascimento <91118+lunks@users.noreply.github.com>
…etween a raw shader's encode and its command buffer's commit is no longer missing from the table the GPU reads, where its loads read 0 (PR seedds#1427)

An argument block held the address of the raw pool table that was current when it was encoded (gmem_map_gpu). A pool made after that, on another thread while the submission was encoded, was not in that table, though the GPU reads the block only after the commit, as NVN's GPU reads memory when it runs. Raw reads of that pool read 0: the failure that hung agl's task shaders in Ocarina of Time.

The table words are noted as they are encoded (gmem_slot_note) and, at each commit of the command buffer, whole or segment (mtl_sub_commit, sub_cut), pointed at a table of the pools as they are then (gmem_commit_patch). A new table is built when the pools changed, not refilled in place, as earlier segments may be reading the old one. A block's write log follows its table (NvGmlLog), so gml_read decodes its bits against the table the shader searched. The memory report counts the words and those that were out of date (raw pool table words at commit). The idea of rewriting the words at commit is from seedds#1407 (@uncraftablexd).

On the author's stack, loading Ocarina of Time's outdoor save, 5,729 of 154,107 words held an older table by frame 5,100 and 2,582 held the current one behind the pools. Whether this window is behind issue seedds#1326's page faults is open: an address the table misses is skipped, not faulted, and the pools near the M1 Max faults were made 8-41 s before them.

Checked: t_gmem_words 16 checks, 0 failed; t_pool_chunks 102, 0 failed; test_docs, test_gfx_tests_makefile, docs_symptoms --check. Donkey Kong Bananza (raw stores and the write log), hosts at 6d928fd with and without this (the build's libraries are chain cfcc3a82c78b), fresh save into the mining tunnel, 210 s each on an M6: no GPU error on either, the same 4 VP9 red lines, frame p50 16.66 ms on both, 23 long-frame lines each; the patch rewrote 1 of 6,035 words.

Co-authored-by: Pedro Nascimento <91118+lunks@users.noreply.github.com>
…level at 30 fps: the layer's frame history lists the presents, so Unreal's first full render flush no longer waits for ever after the logo movie (profile 0100A7C01B792000, note, games table row, gallery)

Unreal Engine 5's Switch RHI learns which frames reached the screen only from
nn::vi::ListFrameHistory: its Viewport Thread (main+0x142afb0) reads it after
each vsync and takes the first shown entry as the latest flip, which completes
the frame-end fences that wait for the swap chain (r.GTSyncType 2). The host
answered with no entries, so the first FlushRenderingCommands (FFrameEndSync,
main+0x15015f0) waited for ever right after the Xbox logo movie: the game
thread polled its fence each millisecond and nothing was presented again.
hle_vi.c now keeps the last 8 presents (recorded at each NVN present, numbered
as GetLatestFrameNumber) and fills FrameInfo as the SDK does (sdk 20.5.15
+0x4510a0): status 2 once the present queue handed a frame on and the system
did not drop it, its present tick from the drawable's presented time or the
time it was first seen handed on; a null buffer gets the history's size, 8.
Octopath Traveler 0 and OCTOPATH TRAVELER II call it from the same RHI code
and got no entries before; no regression run covers them (skipped).

nx_extract's --html read every base HtmlDocument NCA alone, and the base
game's 16 KB one is sparse like its Program NCA: the build stopped as a
damaged NSP although every NCA's hash matches. A sparse base HtmlDocument NCA
is no longer read alone.

Two particle vertex shaders pick a buffer texture at run time from c2's
texture and sampler tables only (0x170-0x188, 0x568 / 0x578): tex_dyn_slots on
(the title's campfire flame was black).

Tested on an M4 Pro: logo movie, title, first-run screens, Play Offline, the
opening cutscene and first control in Escape from Squid Coast, walking and
attacking, 30 fps, no [RED] line, 2.6 GB resident. New host test t_vi_history;
host, NSP, profile and docs suites pass. Regression check skipped at the
user's request.
…inished shaders wait in cas.db and the image is written from there, so Octopath Traveler 0's build holds at most 328 files while it runs and about 65 when stopped, not 68,230 (chain c8849b34b7b0)

dmsh_build.py wrote each shader's outputs into b-<chain>/ and packed them into
the image only at the end, restored every cache hit into that folder before
translating anything, and had the translator write every missed shader's
working files into one temporary folder that went only on a normal exit, so
a stopped build kept them and every later build made another beside it.

Now a finished shader's files go into the compile cache under stage/<output
folder>/ and leave the disk (commit); reads of another shader's outputs
(vs_pad's paired pixel MSL, the mesh step) take them from there (output); at
the end publish_library writes the image from the staged outputs (with a
partial build's image under it, less the keys it built) through
shader_lib.write, which now takes a function per file, and the folder and the
stage go. When the image cannot be written the staged outputs go back into
the folder. Cache hits are restored when their shader is finished. Shaders are
translated and finished in batches of 4 x J, the next batch translated while
this one is finished, and a shader's working files go as soon as it is done.
Each working folder holds its build's process ID; a later build removes one
whose process is gone (sweep_work). The NSP script's progress comes from a
file the build rewrites (DMSH_PROGRESS) instead of counting its files; the NSP
script, regress.py and bug_report.py read the image where they read the
folder; shader_cache.Store gains delete_prefix.

The outputs are unchanged: Minecraft Dungeons II's 61,655 library files and
Octopath Traveler 0's 41,880 are byte for byte those of chain cfcc3a82c78b
(reflection without its chain field), apart from _b_cas.txt. Measured on an
M4 Pro, every shader translated again: Octopath Traveler 0, at most 328 files
under shader/ and tmp/ (12 outside a build), the step 2:38; stopped with
SIGKILL, 40 files in b-<chain>/ and 7 in tmp/, which the next run cleared
before writing the same library in 59 s. A partial build for the Mac and the
iPhone leaves one image each. Shader, NSP, regress and docs tests and the
Python lint pass; no game run (the host is unchanged by this commit).
… uploads fit their 256 bytes of command memory again, so opening Adventure no longer jumps to 0 (PR seedds#1450)

SetCopyRowStride / SetCopyImageStride were recorded as 24-byte OP_COPY_STRIDE commands. In NVN they are
command buffer state that a copy reads when it is recorded (GetCopyRowStride reads them back) and take no
command memory. Super Smash Bros. Ultimate 13.0.5 records each texture upload from a stack command buffer
with 0x100 bytes of command memory and no out-of-memory handler (main+0x3664960, main+0x3794430): the two
strides plus the copy (200 bytes and 16 kept for a JUMP) came to 264 bytes, the host called nn::gfx's
memory callback (main+0x31be0), and it called the unset handler at +0xd0 of its CommandBuffer: pc=0,
lr=main+0x31c14.

- cmd_t keeps copy_row / copy_image; Set/GetCopy*Stride set and read them without a record.
- A buffer <-> texture copy carries the strides after its texture snapshot (p_copy_strides, rec_copy)
  only when one is set, so copies with tight strides keep their size.
- The replay reads them from the record (copy_strides); OP_COPY_STRIDE is still replayed from older
  captures. The capture layout gains a line: older replayers refuse new captures, this one reads old ones.
- t_nvn 5c records Smash's sequence in 256 bytes with a callback that counts any call; t_capture checks
  the layout before the tail stays readable.

Confirmed on the iPhone: Adventure opens and plays. Ocarina of Time A/B on the Mac (village walk, the
only game with libraries for the current chain): the same copies with the same strides, no crash, no new
red line.

Co-authored-by: Ray McDonald <54215218+Raymon82777@users.noreply.github.com>
…s 259 nested brackets, and the BAHS split no longer reads a control section's words as a program entry; a decoder's busy answer is no red line (chain 844bf1a5ea8f, PRs seedds#1414, seedds#1357)

The build left 9 of 42,854 shaders out. Six foliage compute shaders used ATOMG.CAS (0x3a9), which the translator refused (PR seedds#1414: AAtomgCas; also ATOMG / RED INC and DEC as a compare-and-swap loop, and desc[URb][Ra.64], where URb is a memory descriptor and not part of the address; no Ocarina instruction uses that form or INC / DEC). Two agl_technique_pfx compute shaders nest 259 brackets past Metal's 256 (PR seedds#1357: -fbracket-depth=1024 in every library compile, which changes no output within the old limit).

The ninth, 68bbe98b3bfd9876 ("Ampere instruction 481 at 0x0"), was no shader. bahs_shaders scans the table area 4 bytes at a time, and an NVN2 control section's +0x38..+0x47 (instruction length, c1 start and end, code length: L, 0, L, L) reads as an entry whose control is the next program's and whose code is whatever lies at L: here a compute control with a vertex shader's code, whose SPH reached the translator as an instruction. An entry that lies inside a control section another entry names, measured by that section's own length, is dropped. Ocarina's map loses six such rows (agl and terrain files), none ever loaded, and tera_common gives 175 variation pairs once its lowest false entry no longer hides its binary array. Splatoon Raiders' map has the same three agl keys; it, Mario Kart World and Donkey Kong Bananza need their tables split again, which their notes say.

nn::codec's refusal of a packet with 12 pictures waiting returns 3603, which NintendoSDK_movie expects: detail::SubmitDecoderInputBuffer turns exactly that result into its retry code 0x7a and VideoDecoder::DecodeAsync hands it to the player (Ocarina's subsdk0 +0x10fd20, +0x103840). It is logged plainly now (Ocarina AV1, Kirby Air Riders VP9).

Checked: the 9 shaders rebuilt on chain 28ecc05ed4ac, before the rebase onto 9c2fb0c (8 ok, the ninth gone from the map); test_dmsh_ampere_alu.py, test_shader_pch.py, test_nsp_chain.py, the new test_split_bahs_entries.py (the false entry is taken on the previous splitter), test_docs.py; the re-split's tables against the profile's (only the six rows and the renumbering differ); the Mac host builds. Not run: the full shader build on this chain (stopped at 12,534 of 42,848), a game run, regress.py.

Co-authored-by: Copak731 <224629733+Copak731@users.noreply.github.com>
Co-authored-by: Pedro Nascimento <91118+lunks@users.noreply.github.com>
…stead of compiling them at the first dispatch

A compute shader's pipeline was made with newComputePipelineStateWithFunction
at its first dispatch, on the submit thread: no stand-in exists for a dispatch,
as the hybrid pipelines are for draws, and the pipeline archives held render
bases only, so the Metal shader cache was all that stood between a dispatch and
a cold compile (20-40 ms warm, 100-1,200 ms cold, a title movie network kernel
58-83 s). Ocarina of Time's effects bring about 20 new compute shaders with each
area: four frames of 2.2-4.4 s in a 25-minute session, and a cold-cache route
to Kokiri Forest spent 195 s of its submit thread in pipelines, 155 s of it in
the title's twelve kernels.

cps_make takes a compute pipeline from the archives first (the bundled one, then
this Mac's files; about 0.3 ms), else compiles it through the flush period's
compiler so the flush writes it into the archive, else the device as before; the
prewarm's C items, the dispatch and the prefetch go through it. pso_offline.py
puts every compute shader of the shader map into the bundled archive (--compute,
the profile's map with --game; metal-tt's compute_pipeline_descriptors), so a
packed app never compiles a compute pipeline at play: 4,117 pipelines, 0.5 GB,
8 minutes on an M3 Pro.

mtl_prog_prefetch also builds a compute pipeline when the game makes the
program, through the same path, and a dispatch that comes first takes a queued
build over instead of waiting behind the hundreds a scene load queues
(DM_NVN_PREFETCH_CPS=0 turns that part off). Render pipeline events over
100 ms on the submit thread are named in the log.
Launch Mac metal and metallib by absolute path, share one pixel-shader body across the thin entry points, and cut the readable-tile library from that same compile. bf_fn looks up main0_f and keeps the per-shader specialization list. The bracket-depth flag stays on the metal command and out of the compile record. Chain 711dca74fb82. The posix_spawn capture files are read only after they exist, and fetch metadata is stored only when fold returns a dict, so the Python lint ratchet stays at its baseline. Cold own_compilers at J=8 on an Apple M2: Mario Kart 8 Deluxe 22:46 to 14:47, Super Mario Odyssey 30:02 to 22:23.
…two nil archives guarded in t_pso_stall, baseline 237 -> 220

- nvn_mtl.m: NOLINT with the reason on eight analyzer findings in landed code: a shift by __builtin_ctz of a nonzero word, indices bounded by the line before (addressed ranges, vertex streams, g_mrb, g_mrbuf), and two objects held by hm_put_id maps
- t_depth16_clear, t_dlss_mfx, t_ndve_movie, t_pool_heap, t_upscaler_mfx: g_tex_live keeps the address a texture was initialized at; mtl_tex_release reads it through vm_read_overwrite and checks magic and serial, so a helper returning its tex_t by value is safe (NOLINT StackAddressEscape)
- t_feedback: the bf_key offsets are int8_t on purpose (-1 = none)
- t_pso_stall: an archive that does not open is left out instead of added as nil
- lint_c_baseline.tsv: tidy --update (11 file/check pairs had fewer findings; t_view_swizzle back below main's)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
…t (host warnings ratchet)

mtl_tex_init passed its const tex_t * to nvn_hm_put's void * value, a new
-Wincompatible-pointer-types-discards-qualifiers warning from the 0.12.0
upstream merge (upstream stores (void *)1 there). The map only reads the
address back (mtl_tex_release, as a const tex_t *), so the cast is safe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
… xinerqu, CHANGELOG disk-peak note

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK3qWJVi2jG3Gz4i1eY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK3qWJVi2jG3Gz4i1eY
@github-actions github-actions Bot added host The Horizon host: SVCs, services, threads, memory (dm/host) nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) opengl NVIDIA's desktop OpenGL on Metal (dm/gfx/gl) packing Building an IPA or a Mac app: the NSP script, extraction, the shader split, the packers ios The iPhone and iPad app (dm/ios); on an issue, seen on one tools Tools and scripts (dm/tools, scripts) docs Documentation ci Continuous integration and repository settings (.github) shader chain The shader translation chain (DMSH_CHAIN): a change rebuilds every game's shaders game: 0100000000010000 Super Mario Odyssey game: 010014E00DB56000 Digimon Story Cyber Sleuth: Complete Edition game: 0100152000022000 Mario Kart 8 Deluxe game: 0100276009872000 A game profile new game A game main has no profile for yet: a pull request adding one, or an issue about one game: 01005270232F2000 Octopath Traveler 0 game: 0100535012974000 Hades game: 01006A800016E000 Super Smash Bros. Ultimate game: 0100A7C01B792000 A game profile game: 0100D9F01D474000 Rhythm Heaven Groove game: 0400380020056000 Splatoon Raiders game: 0400486024DEC000 The Legend of Zelda: Ocarina of Time game: 040074A01BF12000 Donkey Kong Bananza game: 040077201BE50000 A game profile game: 0400C3F00006E000 Mario Kart World labels Nov 6, 2026
…ASan stack-use-after-return in the sanitizer job)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK3qWJVi2jG3Gz4i1eY
…mcmp of tex_t (clang-tidy bugprone-suspicious-memory-comparison)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK3qWJVi2jG3Gz4i1eY
…clang-tidy nullability)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK3qWJVi2jG3Gz4i1eY
@gnosticgeek
gnosticgeek merged commit 84bdbd2 into main Nov 6, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Continuous integration and repository settings (.github) docs Documentation game: 0100000000010000 Super Mario Odyssey game: 0100152000022000 Mario Kart 8 Deluxe game: 0100276009872000 A game profile game: 0100535012974000 Hades game: 0400380020056000 Splatoon Raiders game: 0100A7C01B792000 A game profile game: 0100D9F01D474000 Rhythm Heaven Groove game: 0400C3F00006E000 Mario Kart World game: 01006A800016E000 Super Smash Bros. Ultimate game: 010014E00DB56000 Digimon Story Cyber Sleuth: Complete Edition game: 040074A01BF12000 Donkey Kong Bananza game: 040077201BE50000 A game profile game: 0400486024DEC000 The Legend of Zelda: Ocarina of Time game: 01005270232F2000 Octopath Traveler 0 host The Horizon host: SVCs, services, threads, memory (dm/host) ios The iPhone and iPad app (dm/ios); on an issue, seen on one new game A game main has no profile for yet: a pull request adding one, or an issue about one nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) opengl NVIDIA's desktop OpenGL on Metal (dm/gfx/gl) packing Building an IPA or a Mac app: the NSP script, extraction, the shader split, the packers 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.

6 participants