Skip to content

Compute pipelines in the pipeline archives: compiled offline for every compute shader of the map, and captured into the device's archive when the bundle lacks one - #1307

Closed
lunks wants to merge 6 commits into
seedds:mainfrom
lunks:pr/compute-archive

Conversation

@lunks

@lunks lunks commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

What and why

Who needs it: Star Fox, whose note lists "a pso_manifest.tsv has not been recorded" open, and any game in bring-up without a recorded prewarm list. Their compute pipelines are built on the device at the first dispatch, at every start when the bundled archive lacks them. Star Fox has 233 compute shaders. No issue reports it.

The host builds every compute pipeline on the device, through the MTLDevice, at its first dispatch or at the prewarm's C rows. Two consequences:

  • A game without a recorded prewarm list (a provisional profile, any game in bring-up) has nothing in the app's pipeline archive for its compute shaders (archive.tsv: bases only) and compiles them at their first dispatches.
  • A pipeline the bundled archive does not hold (no bundle, as in a headless boot; a shader outside the map; an OS update the bundle's binaries do not survive) is built again at every start, in full whenever the system's shader cache has dropped it: the MTLDevice build goes through no pipeline serializer, so pso_archive never holds one.

A compute pipeline depends on its function alone (no threadgroup size or other option; the dispatch gives the threadgroup size), so it can be compiled without a play session.

  1. pso_offline.py gives the archive a compute pipeline descriptor for each compute shader's <key>_b: the prewarm list's C rows first, then every compute row of shader_map.tsv (--shader-map, --manifest, --no-compute), leaving out functions with function constants (the host specializes those). They compile in groups of their own (batches.<arch>/c<index>, 16 each), cached by their inputs and merged with the bases by metal-pack. The host's new cps_new looks <key>_b up in the bundle first and builds it on a miss. The profile's tables are the default, so iOS archives (build_ipa.sh) get them too.
  2. On a miss, cps_new looks in the device's own archive files, and after building through the MTLDevice (for the dispatch, as before) compiles the pipeline again through the Metal 4 compiler into the current flush period on a serial utility queue (cps_harvest, arch_capture_cs), so the next start finds it. The capture stays off the dispatch: building the dispatch's pipeline through it took 95 ms for Star Fox's first 5, against 1.2 ms through the MTLDevice. A period with compute pipelines is a plain file, never a p<seq> one; compaction captures them again as it does bases.

The log counts them (compute pipelines so far: N from the archive bundled with the app, M from this device's archive, K built, also in the pipelines up to frame line). Reference entries: Compute pipelines compiled at first use and Compute pipelines outside the bundled archive built at every start.

Checked

  • Tests (M5 Pro, macOS 27.0, on these commits over main 056e41f): make -C dm/host/tests GAME=040074A01BF12000 run passes. make -C dm/gfx/tests GAME=040074A01BF12000 run passes (t_pso_bundle 69 checks, t_pso_archive 111 checks, t_pso_warm 99 checks, all 0 failed). t_pso_bundle builds an archive with compute shaders: the map's come from it with nothing built; one with constants and one outside the map are built, and only the latter is captured. t_pso_archive (compute_pipelines): built, captured in the background, written to a plain file, taken from it without a build, captured again at compaction; a function without a descriptor is never looked up.
  • Tool tests with the repo's venv (lz4, zstd): test_pso_offline.py (new Compute class and group tests), test_nsp_mac_app.py, test_nsp_incremental.py, test_nsp_nup.py, test_docs.py: 219 tests pass (the three NSP tests skip 32 cases without a zstd module in the system Python and pass with the venv's: 164 tests). docs_symptoms.py --check passes.
  • Measurements, all on Star Fox (0400E88025B1A000, v131072, then with a provisional profile; chain ae59da13e481), M5 Pro, macOS 27.0, on our fork with other host changes (among them HMMA/IMMA through TensorOps, whose <key>_bt libraries were kept out of the archive; that code is not in this PR):
    • Offline: 233 compute pipelines beside 3 bases, a 35 MB archive, built in 16 groups in 268 s against 453 s as one metal-tt run (background priority, load averages 19-26 from other jobs); unchanged inputs reuse it in 0.3 s. Two 90 s headless boots to the title with it took all 7 of their compute pipelines from it and built none.
    • Device archive, 90 s headless boots to the title without the bundle: before, 5 built at the first start (1.2 ms) and 9 at the second (2.2 ms), none kept; after, 5 built at the first start and written by the flushes, then 8 from the device's archive at the second (1.4 ms of lookups) and none built. Same frame reached, same [RED] lines.
    • With a warm system shader cache the lookups and the builds cost about the same; the gain is at a cold cache or a first start.
  • Not run on this branch: no game boot (no Star Fox or other run of these exact commits), no iOS device, no frame-time comparison in play.
  • Devices and OS: M5 Pro, macOS 27.0.
  • regress.py check: not run. It reaches every game with compute shaders (the app's archive grows by their pipelines, and the device's archive files by their captures); Bananza and Star Fox would be the first to check.
  • Lint: lint_python.py and lint_c.py not run (ruff, pyright and the pinned clang-tidy are not installed here).

Docs

  • Both findings are in their docs/reference/shaders.md entries, with lines in Star Fox's note, and python3 dm/tools/docs_symptoms.py was run (the symptom index); docs/development.md describes the compute pipelines in the archive.
  • A chain tool changed: not applicable (pso_offline.py is not a chain tool).
  • No game files, keys or data extracted from a game in the change or the attachments

lunks added 2 commits November 5, 2026 04:28
…te shader of the shader map, which the host takes at the first dispatch: a provisional profile, whose prewarm list names none, no longer builds them all on the device

A compute pipeline depends on its function alone: the host builds it from the function (newComputePipelineStateWithFunction, no threadgroup size or other option; the dispatch gives the threadgroup size), so it can be compiled at build time without a recorded play session. pso_offline.py now gives the archive a compute pipeline descriptor naming the function of each compute shader's <key>_b: the prewarm list's C rows first, then every row of stage compute in shader_map.tsv (--shader-map, --manifest, --no-compute), leaving out functions with function constants as the host specializes those. The compute pipelines compile in groups of their own (batches.<arch>/c<index>, 16 each, --jobs at a time, half the logical CPUs by default), each cached by its inputs and merged with the bases by metal-pack, so new pairs leave them compiled and the reverse. The host's cps_new looks <key>_b up in the bundled archive first (cps_bundle), at the first dispatch and at the prewarm's C lines, and builds it on a miss; the prewarm's last lines and the 'pipelines up to frame' line count both.

Star Fox (0400E88025B1A000, provisional profile, chain ae59da13e481, M5 Pro, measured on a fork whose host also carries other changes): 233 compute pipelines beside 3 bases, a 35 MB archive, 268 s in 16 groups against 453 s as one metal-tt run (842 / 829 s of CPU, background priority, load averages 19-26 from other jobs); unchanged inputs reuse it in 0.3 s. Two 90 s headless boots to the title with it took all 7 of their compute pipelines from the archive and built none.

Checked: test_pso_offline.py (new Compute class and group tests), test_docs.py, docs_symptoms.py --check; t_pso_bundle (compute pipelines from the archive; a shader with constants or outside the map built).
… the device's pipeline archive: Star Fox's second headless start took its 8 from it and built none

A compute pipeline that missed the archive bundled with the app (no bundle, a shader outside the shader map, an OS update the bundle's binaries do not survive) was built through the MTLDevice, which no pipeline serializer sees, so pso_archive never held one and every start built it again, in full whenever the system's shader cache had dropped it. cps_new now looks such a pipeline up in the bundle, then in the device's own archive files; on a miss it builds it through the MTLDevice for the dispatch, as before, and compiles it again through the Metal 4 compiler into the current flush period on a serial utility queue (cps_harvest, arch_capture_cs), so the flush writes it. A period with compute pipelines is a plain file, never a p<seq> one, so the lookups skip the prebuild's files, and compaction captures a compute pipeline the prewarm takes from an old file again, as it does bases. Functions without a Metal 4 descriptor (constants, the embedded kernels) are neither looked up nor captured.

The capture stays off the dispatch: a first version that built the dispatch's pipeline through it took 95 ms for Star Fox's first 5 compute pipelines, against 1.2 ms through the MTLDevice, which the system's shader cache serves. Headless boots of 90 s to the title without the bundle (M5 Pro, macOS 27.0, on a fork with other host changes), each series with its own archive and prewarm list: before, 5 built at the first start (1.2 ms) and 9 at the second (2.2 ms), none kept; after, 5 built at the first start (0.6 ms) and written by the flushes, then 8 from the device's archive at the second (1.4 ms of lookups) and none built; the same frame and the same [RED] lines. The log counts them (compute pipelines so far: N from the archive bundled with the app, M from this device's archive, K built, with the lookup and build times), also in the pipelines up to frame line. t_pso_archive checks the capture, the plain file, the second lookup, compaction and that a function without a descriptor is never looked up; t_pso_bundle that a shader outside the map is captured and one with constants is not.
@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) 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) tools Tools and scripts (dm/tools, scripts) docs Documentation game: 0400E88025B1A000 A game profile labels Nov 5, 2026
lunks added 2 commits November 5, 2026 20:03
… not "?": test_dmsh_abi counts "?" as the binding kind DMSH_KIND_UNKNOWN and failed on its third use in nvn_mtl.m
pso_offline.py: main's real_libraries (libraries of one file get a temporary folder for metal-tt) now also copies the compute pipelines' libraries, and the compute pipelines go through it in compile_batch and in main().
lunks added 2 commits November 6, 2026 03:26
nvn_mtl.m: arch_capture_cs (compute pipelines into the archive) and main's mapped read of .mtl4a files both stay.

Checked: check_t_pso_archive (111 checks, 0 failed), check_t_pso_bundle (69), test_pso_offline.py, with GAME=01007EF00011E000.
nvn_mtl.m, t_pso_archive.m: main's specialization-descriptor ownership note (88c7e03) and this branch's compute-pipeline archive line and check both stay.

Checked: check_t_pso_archive (123 checks, 0 failed), check_t_pso_bundle (69), with GAME=01007EF00011E000.
seedds added a commit that referenced this pull request Nov 7, 2026
…y compute shader of the map, and captured into the device's archive when the bundle lacks one (PR #1307)

pso_offline.py gives the bundled archive a compute pipeline for each compute shader (the prewarm list's C rows, then the shader map's compute rows; functions with constants left out). The host's cps_new looks a compute pipeline up in the bundle, then in the device's archive files. On a miss it builds through the MTLDevice for the dispatch, as before, and compiles it again into the flush period on a background queue, so the next start finds it.

plan_compute now reads the libraries through romfs.Path, as plan does. With pathlib.Path, a one-file library (b-<chain>.img, what dmsh_build.py writes) left out every compute shader as having no reflection, and the archive held none (test_pipelines_from_a_library_image).

Ocarina of Time (chain f9f01e4f9994, M6): 4,125 compute pipelines in a 480 MB archive. With it, all 103 of a route's compute pipelines came from the bundle and none were built. Without it, the first start wrote 88 into the device's archive and the second took 88 from it. PR #1522 measured the cold-cache gain: 195 s of submit-thread pipelines to 0.9 s. Return of the Obra Dinn quick regress: PASS (its route dispatches no compute).

Co-authored-by: Pedro Nascimento <91118+lunks@users.noreply.github.com>
@seedds

seedds commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Landed as 30fb213, with you as co-author. One fix on top: plan_compute read the libraries through pathlib.Path. With a one-file library (b-<chain>.img, what dmsh_build.py writes now), it left out every compute shader as having no reflection, so the archive held none. It reads through romfs.Path now, as plan does, and test_pipelines_from_a_library_image covers it.

Measured on Ocarina of Time (v131072, M6, warm Metal cache):

  • Archive: 4,125 compute pipelines, 480 MB, 204 s.
  • With it: all 103 compute pipelines of a route from a save to the village came from the bundle; none were built.
  • Without it: the first start's flushes wrote 88 compute pipelines into the device's archive, and the second start took 88 from it.

The cold-cache gain is #1522's measurement (195 s of submit-thread pipelines down to 0.9 s), cited in the docs. Return of the Obra Dinn quick regress: PASS. Its route dispatches no compute, so it only shows nothing else changed.

@seedds seedds closed this Nov 7, 2026
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 7, 2026
…merges

clang-tidy found one more core.NullDereference in nvn_mtl.m (tex_cpu_content's
first-use log line read t->pool->flags without a pool, seedds#1604), and one more
nullability.NullablePassedToNonnull each in t_copy_alias.m (a kernel function
looked up by name, seedds#1556) and t_pso_archive.m (the archive path from a boxed
C string, seedds#1307). The log line prints 0 for a texture without a pool; the
tests stop when the lookup fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK6QbhFrzr2Vop9t34k
ksolenk38ze added a commit to ksolenk38ze/nxnative that referenced this pull request Nov 7, 2026
…loading

Merge adjacent GPU memory pools with contiguous guest virtual addresses and
matching GPU address deltas into single gmem_map run entries (up to
GMEM_RUN_MAX = 1,024 runs) backed by a reusable MTLBuffer ring
(gmem_table_take / gmem_table_give), and reuse g_gmem_buf across encode_args_
calls within a submission since gmem_commit_patch updates the table at commit
time. Also zero-fill
unmapped virtual pool spans with PROT_READ | PROT_WRITE in vpool_map so late
CPU reads do not fault.

For MTL4Archive pipeline caches (related to PRs seedds#1307, seedds#1488, seedds#1493, seedds#1546),
separate vertex-descriptor (vd) and pulling (non-vd) archive epochs so
specialized vertex-descriptor pipelines are not written into the pulling archive
set; guard against macOS 27's crash in MTL4ArchiveImpl::newArchiveReplyForKey
when deserializing an archive entry with a zero-length function reflection
string; preserve the active OP_CALL return stack when starting
shader_prefetch_rest and pso_predict_rest_; and support falling back to the
bundled shader chain directory when DMSH_CHAIN advances.

Verified in The Legend of Zelda: Ocarina of Time (0400486024DEC000) on Apple
M-series (macOS).
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
…y compute shader of the map, and captured into the device's archive when the bundle lacks one (PR seedds#1307)

pso_offline.py gives the bundled archive a compute pipeline for each compute shader (the prewarm list's C rows, then the shader map's compute rows; functions with constants left out). The host's cps_new looks a compute pipeline up in the bundle, then in the device's archive files. On a miss it builds through the MTLDevice for the dispatch, as before, and compiles it again into the flush period on a background queue, so the next start finds it.

plan_compute now reads the libraries through romfs.Path, as plan does. With pathlib.Path, a one-file library (b-<chain>.img, what dmsh_build.py writes) left out every compute shader as having no reflection, and the archive held none (test_pipelines_from_a_library_image).

Ocarina of Time (chain f9f01e4f9994, M6): 4,125 compute pipelines in a 480 MB archive. With it, all 103 of a route's compute pipelines came from the bundle and none were built. Without it, the first start wrote 88 into the device's archive and the second took 88 from it. PR seedds#1522 measured the cold-cache gain: 195 s of submit-thread pipelines to 0.9 s. Return of the Obra Dinn quick regress: PASS (its route dispatches no compute).

Co-authored-by: Pedro Nascimento <91118+lunks@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: 0400E88025B1A000 A game profile ios The iPhone and iPad app (dm/ios) nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) packing Building an IPA or a Mac app: the NSP script, extraction, the shader split, the packers tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants