Skip to content

Ocarina of Time: prewarm native mesh pipelines before attacks and enemies appear - #1368

Closed
nanotchec wants to merge 2 commits into
seedds:mainfrom
nanotchec:codex/native-mesh-pso-prewarm
Closed

nanotchec wants to merge 2 commits into
seedds:mainfrom
nanotchec:codex/native-mesh-pso-prewarm

Conversation

@nanotchec

@nanotchec nanotchec commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Ocarina of Time can freeze when an attack, enemy or piece of foliage first needs a native task/mesh pipeline. The regular render prewarm did not cover these pipelines. Record successful native pipelines as M rows and prepare them through the existing workers before gameplay. An unprepared state still builds synchronously; geometry is never skipped to hide a stall.

The change uses the same descriptor path for prewarm and live draws, validates serialized keys and shader stages, and claims warmed pipelines across equivalent shader-library loads while keeping different depth entries separate. It adds 73 observed Ocarina states and an optional scripts/prewarm_native_mesh.py tool that expands coverage from the player's own packaged BNSH programs and compatible observed render states. Generated game data are not committed.

Validation

Current base: official main d8b2b64a1fc5. Its CI repairs are retained; the branch now contains only the native-prewarm feature and its duplicate-load correction.

On the rebased sources, Apple M3 Max:

  • Native prewarm: 123 checks with MTLDevice and 123 with Metal 4, Metal validation enabled.
  • Native first use: 42 checks each with direct and threaded encoding.
  • Serialized pipeline-key tests pass. Generator and documentation tests: 17 passed. Diff check is clean.
  • GitHub checks are rerunning on this revision; the preceding revision passed all nine checks. Previous green results are not presented as results for this new head.

Earlier combined-host gameplay on base 028a263, chain 3f35b514643e, used a copy of the player's v131072 save at Hyrule Castle. After expanded preparation, the player reported no further freezes during attacks and rapid movement; the log recorded zero native mesh demand builds and zero renderer red errors. This is bounded gameplay coverage, not a pristine-current-main benchmark or a promise of zero stutter everywhere.

The expansion reproduced 1,286 source triples and 7,716 prospective mesh states. Preparing this broad set took about 151 seconds. One immediate warm restart took 5.08 seconds, but later launches again took about 152–164 seconds and reported driver compiled-variant footprint pressure. Persistent native-mesh archive reuse remains a separate open issue: the broad list is optional and no consistently fast restart is claimed.

Regression impact includes Breath of the Wild, Tears of the Kingdom and Pokémon Scarlet through shared NVN callers. The requested multi-game harness cannot run because only packaged apps remain locally, with no standalone Mac build folders. No cross-game runtime or iOS execution validation is claimed. The feature-specific GPU fixtures exercise both native and ordinary render first-use behavior.

Front-ends: existing launch preparation reads M rows automatically. Broader coverage is generated explicitly with the optional tool; no new default setting is required. No game assets, keys or saves are included.

@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) tools Tools and scripts (dm/tools, scripts) docs Documentation labels Nov 5, 2026
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 5, 2026
…d built before their first draw (Ocarina of Time foliage)
@nanotchec
nanotchec marked this pull request as ready for review November 6, 2026 00:26
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
…act with native mesh pipeline prewarming

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g

# Conflicts:
#	dm/gfx/tests/t_pso_skey.m
fpedrolucas95 pushed a commit to fpedrolucas95/nxnative that referenced this pull request Nov 6, 2026
…ws intact with native mesh pipeline prewarming
gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 6, 2026
) replaces the pull request heads

Upstream landed Ocarina of Time in main (chain adec7d81d970: "holds 60 fps in
Kokiri Forest, 4 GB less") and closed the pull requests it folded in, with
reworded and partly different code. Mixing a pull request head with its landed
version hunk by hunk silently broke things (two GlobalToStorage tables, a
duplicate AF2ip, two mtk_pso definitions), so the rule here is one version per
feature:

- Pull requests upstream closed (seedds#1299, seedds#1300, seedds#1312, seedds#1343, seedds#1345, seedds#1353,
  seedds#1354, seedds#1359, seedds#1360, seedds#1373, seedds#1428, seedds#1430, seedds#1434): their hunks are taken out
  of our side before the three-way merge, so upstream's landed version stands
  alone. Gone with them: bindless pool table patching (reg_bump), the ghost
  table's per-submission hold, the growing raw pool table and its write-log
  sizing, seedds#1428's pool_buf spares (upstream's pool_table_take is the same
  idea), the ixc-based indirect mesh draw (upstream uses mdx), and their tests
  (t_pool_table, t_mesh_indirect, t_gmem_raw, finalized_bindless_pool,
  test_bnsh_nvn2, test_ndve).
- Open pull requests that upstream's landing supersedes: seedds#1394 (upstream's
  mtl_pool_addressed) and seedds#1393 pool_skip_retired (upstream's budgeted pool
  walk; the profile does not use it) are dropped with their tests and docs.
  seedds#1368's mesh pipeline prewarm is dropped too: upstream kept the background
  build in nvn_geometry.inc, and the prewarm's mtk_build / mtk_claim /
  g_warm_mps went with it.
- Kept, as upstream does not have them: NXForge's own features (GPU watchdog,
  present filters and anti-aliasing, aniso floor, quick settings, pipeline
  archive compaction, upload breakdown, memory report lines) and the still
  open pull requests seedds#1304, seedds#1305, seedds#1308, seedds#1320, seedds#1331, seedds#1332, seedds#1355, seedds#1356,
  seedds#1369, seedds#1376, seedds#1377, seedds#1379, seedds#1382, seedds#1390, seedds#1391, seedds#1392, seedds#1414, seedds#1420,
  seedds#1436, seedds#1439, seedds#1446, seedds#1453.
- Combined where both sides changed the same lines: GW_N 1 << 16 with seedds#1320's
  wide-write intervals (GW_PAGES = SBW_WPAGES, 128 slots), the CPU upload log
  with upstream's linear upload route, arch_open_end before the compaction
  limit, cbn_prog in geo_gs_one (seedds#1382).
- Translator: upstream's sources and binaries, plus seedds#1414's
  InstEmitAmpereMem diff and the LDSM SubgroupBarrier in
  InstEmitAmpereTensor.cs. The binaries and dmsh_chain.lock are upstream's
  until the Mac rebuilds them (dotnet publish, tool_lock --update-lock,
  dmsh_chain --header); dmsh_chain.h is regenerated for now.
- RED_ONCE keys: upstream took 153 and 154, so the present anti-aliasing lines
  move to 156 and 157 and seedds#1420's packaged-entry line to 158 (array 159).
- Host: hle_net.c and t_net_eventfd.c are upstream's (its eventfd poll waits
  out the whole timeout, which our 5d7920c fixed another way).
- Kept ours: dm/tools/romfs_image.py and its test, so the 26 GB RomFS image is
  not packed again.

dm/tools tests on Linux: same failures as on 875449e (Linux-only quirks),
plus test_nlg4 and test_dmsh_pool_arrays, which fail on upstream main too.
Not yet built on the Mac.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK29T18U8p1c9LsVY8g
@aksmith-git

Copy link
Copy Markdown
Contributor

We made the same change on our Ocarina of Time builds, so I won't open a second PR for it. Here are numbers that may help this one.

Format. Our M rows are the same as yours: M, the task key or -, then the pso_skey hex with the mesh shader in the vertex place and no third function, and the same pso_mesh_skey_parse checks. #1488 adds an Ocarina of Time seed list with 58 such rows, recorded on an iPad Pro (M5) and the Mac. Main skips them today; with this PR they are prepared.

Mac (M4 Pro, handheld, 5 minutes from a Kokiri Forest save, DM_PSO_MTL4=0, prepare step on):

  • second start without M rows read: 56 task / mesh pipelines built in play. Main builds them in the background and leaves their draws out until they are ready, so foliage and shadows were missing meanwhile.
  • with M rows read: pipeline prewarm: 56 task / mesh pipelines (58 with the full list). The first task / mesh draw drew straight away, and 0 were built in play.

iPad Pro (M5, iOS 27): on a cold Metal cache a task / mesh pipeline took 1.9-5 s to build, and 0.1-45 ms once cached. Before rows were read, 56 recorded M rows in the iPad's own pso_user.tsv were ignored at each start.

One difference: our version kept main's background build, with the draw left out until the pipeline is ready, and adds only the recording and the prewarm hand-over (warm_claim puts a prewarmed pipeline into g_pso_mtk when the live shader objects match). Yours builds on a miss before the draw. With a seed list the miss is rare either way. Ours also put pso_user_record under a lock, since the background queue writes the row. That doesn't apply to yours if the row is written on the submit thread.

john62036203-ops pushed a commit to john62036203-ops/NXNativeWindows that referenced this pull request Nov 6, 2026
…so a first start prepares its pipelines before the first frame instead of freezing for them in play

8,355 shaders, 1,227 render, 144 compute and 58 task / mesh pipelines, merged
from an iPad Pro's pso_user.tsv over several hours of play and the Mac's
DM_PSO_RECORD runs; every shader named is in shader_map.tsv and every R row
parses. The bundled list was empty. On the Mac (M4 Pro, DM_PSO_MTL4=0, a
5-minute route from a save) a fresh start compiled 1,056 pipelines on the
submit thread (52 s, one frame of 17.8 s entering Kokiri Forest); with the
list it prepares it in 5.1 s and builds 6 in play (1.1 s), frames over 100 ms
45 -> 5. The M rows wait for a prewarm that builds task / mesh pipelines
(PR seedds#1368); main's skips them.
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.
@jonpage0

jonpage0 commented Nov 6, 2026

Copy link
Copy Markdown

M3 Max prewarm validation (2026-11-06): with this PR, #1307, and the union of #1488/#1493 plus the locally recorded list (9,846 seed rows), our V3 warm run logs pipeline prewarm: 63 native task / mesh pipelines. The prepare step handled 9,852 total seed+user rows in 1.18 s on 14 workers: 8,376 shader entries, 1,268 render entries and 145 compute entries.

The Kokiri Forest save loaded and the moving route completed with clean exit 0. This is 30-core M3 Max, Metal4 and encoding thread on. These are combined-build observations, not an isolated FPS measurement of #1368. The subsequent V4 cold test logs 58 native task/mesh prewarms from the bundled union seed and also reaches the forest with foliage present.

gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 7, 2026
…/ mesh pipelines built at their first draw and prewarmed from the recorded list; Ocarina of Time's foliage no longer draws without colour until a background build finishes

Applied as the PR's diff against its merge base, not its merge commits, so no older main commits come with it. test_dmsh_pool_arrays keeps NXForge's commented assertions (same checks).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK3qWJVi2jG3Gz4i1eY
@padsiwono

Copy link
Copy Markdown
Contributor

Cold-cache numbers from an M5 Max (macOS 26.6.2), Ocarina of Time v131072, main 490859ea with this PR ported on top (the net diff against 88c7e03f applied cleanly apart from the test_dmsh_pool_arrays.py expectation). They add to aksmith-git's and jonpage0's runs above, which were warm or had the rows.

Without the PR, Kokiri Forest whites out at first sight until the foliage mesh pipelines finish in the background: 117 to 132 frames with a warm Metal cache, and 861 frames, still white when the run ended, with the cache folder moved aside.

With the PR and no M rows there were no white frames in four runs, cold and warm. On a cold cache, though, the 63 builds the forest entry needs run one after another on the submit thread: 59.5 s in all, one frame of 42.9 s. So building on a miss swaps the whiteout for a long freeze until the rows exist.

With 88 M rows in the user list and the prepare step on (DM_PSO_PREPARE=1, the window app's path), all 88 were built before the first frame, nothing was built at a draw, and the longest frame was 281 ms. The whole list took 76.6 s on 18 workers, cold. Headless without the prepare step, the serial background prewarm never reached the M rows at the end of the 24,000-row list.

The seed list that landed in 540f7977 has no M rows (#1488's 58 were not taken, as main skipped them then), so once this lands a first start still needs them in the profile's pso_manifest.tsv. Ours are 88 rows from three routes through Kokiri Forest; 52 of them match #1488's, 36 are new. I can attach them or open a follow-up for the manifest, whichever you prefer.

gnosticgeek pushed a commit to gnosticgeek/nxforge that referenced this pull request Nov 8, 2026
Drops NXForge's copy of nxnative seedds#1368 (mesh prewarm) for upstream's own fix, and
NXForge's post-first-frame recapture for upstream's period-based compaction (keeps
PSO_ARCH_COMPACT 24 and DM_PSO_ARCH_COMPACT). Shader chain 98747cde2289. The engine
docs no longer describe NXForge's own scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qteK4U7UAc5p3k4mJJhPJ
@nanotchec nanotchec changed the title Keep Ocarina of Time foliage draws intact with native mesh pipeline prewarming Ocarina of Time: prewarm native mesh pipelines before attacks and enemies appear Nov 8, 2026
@Amost-

Amost- commented Nov 8, 2026

Copy link
Copy Markdown
Contributor

Prewarmed mesh pipelines are often dropped: native mesh prewarm: live shader variant differs

Following up on ycarogabriel-lgtm's numbers above: without a prewarm, first-use mesh builds cost an M1 103.5 s per walk with a cold Metal cache. On this PR as it stands, part of the prewarm's work is thrown away and falls back to those first-draw builds.

One interaction with the program-creation prefetch. mtl_prog_prefetch is already in this PR's base 88c7e03f, and the PR's head 3bd9a5d3 has the same code:

  • warm_claim installs a prewarmed M pipeline only when vs, fs and ts are the same NvShader objects that g_shcache holds for those keys.
  • mtl_prog_prefetch (from nvnProgramSetShaders) loads the same keys again through shader_load and enqueues them as kind-0 items. It only checks progpf_first, which warm_shader never marks, so it gets objects of its own.
  • When one of those kind-0 items (or a draw's own load) puts its instance in g_shcache before the M item is claimed, the M item's objects differ, and the warmed pipeline is dropped with that log line. This happens easily, since the prewarm runs in the background for about 100 s.
  • mtk_pso then compiles the pipeline at its first draw. The M row is already in pso_user.tsv, so the next start drops it the same way.

On my M2 Pro this shows as 0–29 such lines a run (six runs of 3–6 minutes with the M rows in pso_user.tsv, carrying this PR on main 028a2631). With macOS's shader cache warm, each first-draw rebuild costs 0.1–0.4 ms, at most about 1 ms a run, so the cost is small. I haven't measured a drop with that cache cold.

A possible fix: in the kind-3 claim, call mtk_claim with the live g_shcache objects instead of rejecting them, as the kind-1 render path already does. Both instances come from the same library and entry (shader_load(key, stage, 0) in both).

@ycarogabriel-lgtm

Copy link
Copy Markdown

Thanks for the detailed trace, @Amost-. We went and read our side to see whether the same thing can happen with an archive-first path (M1 16 GB, local branch of open PRs on an older main, so treat this as a code reading plus one earlier run, not a test of your claim).

What we found in our code:

  • The lookup that finds a mesh pipeline in a Metal 4 archive is keyed by the pipeline descriptor (library + function name + constants), not by NvShader identity. Any NvShader loaded from the same library resolves to the same archived pipeline, so a prefetch-created instance does not matter there.
  • The in-memory table in front of it (g_pso_mtk) is keyed by the NvShader* pointers and the task's MTLFunction, so it has the same orphaning you describe: if the prefetch puts its own instance in first, the draw builds a different key and the warmed entry is never hit.
  • In our case a miss in that table falls through to the archive lookup, which instantiates without compiling. That matches what we measured earlier: 56 mesh pipelines taken from the archive and 0 compiled in game over a 300 s run. So for us the loss is the in-memory instance plus one archive lookup per pipeline at first use, not a rebuild. We have not measured the cost of that lookup.

Not tested: your suggested fix (claiming with the live g_shcache objects). One alternative we have not tried either is keying the table by the shader key (pso_skey) instead of pointers, which would make both paths agree without touching claim order.

Question for you: with the M rows present, do the 0-29 drops per run change between a cold and a warm macOS shader cache on your M2 Pro? We only have an M1 and would not trust our own cold-cache numbers beyond that one machine.

@padsiwono

Copy link
Copy Markdown
Contributor

An update to the M rows I offered above: there are now 118 for Ocarina of Time v131072, the 88 from three routes through Kokiri Forest plus 30 recorded in two play sessions through Hyrule Field and Castle Town (M5 Max, macOS 26.6.2, our build of main with this PR ported).

In the second session, with all of them in the user list, the start prewarmed 111 mesh pipelines (prepare 3.64 s, warm cache), and Hyrule Field, which had stalled in the first session, no longer did. The one stall left was at the castle doorway, 4.6 s at a single mesh pipeline drawn for the first time that no route had recorded yet: the first-use cost ycarogabriel-lgtm measured, for a pipeline outside the list.

I haven't checked whether any of the 111 were dropped as Amost- describes; our log counts them as prewarmed, not as installed at their first draw.

The rows only take effect with this PR's M support. Happy to open them as a follow-up to the profile's pso_manifest.tsv once it lands.

The 118 rows are attached as a text file (one M row per line, as pso_user.tsv takes them): oot-v131072-mesh-m-rows.txt

@github-actions github-actions Bot added host The Horizon host: SVCs, services, threads, memory (dm/host) ios The iPhone and iPad app (dm/ios) labels Nov 8, 2026
@Gesalbter

Copy link
Copy Markdown

Some numbers from a slower machine that support this PR: MacBook Air M4 (Mac16,13, 10-core GPU, 24 GB, macOS 27.2), Ocarina of Time v131072, Mac app built from 7f502220, handheld, fullscreen.

On this machine the first-use native mesh pipelines cause the longest stalls in normal play, often when entering an area. Each of the slow compiles takes 0.6–1.65 s here:

Session Play time Native mesh pipelines ≥ 100 ms Total time in them
c78d7375, 1st session on that build ~3 h 15 min 80 49.7 s
7f502220, 1st session on that build ~45 min 37 30.7 s
7f502220, 2nd session ~45 min 12 6.3 s

Example, entering the Lost Woods from Goron City (7f502220, first session):

task / mesh pipeline: task 99280b7b8aae78df_b mesh eb021183bf9b0092_b fragment 087ef21b3a72ad3a_b, 763.6 ms
task / mesh pipeline: task a5288d0a6c4971fd_b mesh 04452448ac915259_b fragment 6792a3527dc82545_b, 1386.6 ms
task / mesh pipeline: task - mesh eed1add7c89f5d34_b fragment bc214ce5c4f88db8_b, 1348.4 ms
... (11 compiles of 600-1390 ms in total)
[longframe] frame 30341 (927.76 s) whole frame 5816.9 ms, submit+present 5693.3 ms, GPU 26.0 ms; new pipelines 9 + shader libraries 0, 32.8 ms in all; ...
[longframe] frame 30343 (933.97 s) whole frame 6178.8 ms, submit+present 6125.7 ms, GPU 25.2 ms; new pipelines 0 + shader libraries 0, 0.0 ms in all; ...

Example, Dodongo's Cavern (7f502220, second session): a single compile becomes a 1.7 s stall.

task / mesh pipeline: task a5288d0a6c4971fd_b mesh 3f288c4af8b8b112_b fragment 16d22c8b916037d5_b, 1647.0 ms
[longframe] frame 50053 (1435.97 s) whole frame 1746.2 ms, submit+present 1661.5 ms, GPU 16.3 ms; new pipelines 0 + shader libraries 0, 0.0 ms in all; ...

A side note that might help others reading their logs: the [longframe] line doesn't seem to count these compiles. It reports new pipelines 0 ... 0.0 ms for a frame that spent 1.65 s compiling a task/mesh pipeline, so the stall shows up only as submit+present time. At first I took these stalls for something else (memory pressure) until I matched them against the task / mesh pipeline lines. Counting native mesh pipelines in that line would make this much easier to spot.

Looking forward to this landing; on this machine it would remove the longest stalls in normal play.

@ycarogabriel-lgtm

Copy link
Copy Markdown

@padsiwono thanks for the 118 rows. Two measurements from our side that may help whoever merges them (MacBook Pro M1, 16 GB, macOS 26.6.2, OoT v131072, one machine only):

  • Without any prewarm or archive for mesh, a 300 s walk built 47 task/mesh pipelines on the submit thread, 103.5 s in all, the longest 12.76 s (cold Metal cache). With the 58 M rows plus an archive, 54-56 mesh pipelines came from the archive and 0-2 were compiled in game. In a later 55-minute session the mesh pipelines were also logged as served from the archive, though one of them (3439aaa071d868ae) still took 1.35 s even from there.
  • The prepare step on a larger list took a while on its first run: a 40,282-row manifest took 2,177 s of prepare with 8 workers (about 41 minutes for the whole session) and compiled 7,165 bases; it also hit MDB_MAP_FULL in the Metal archiver and fell back to compaction, so only part of what it compiled was saved.

Not measured here: any chip other than the M1, and whether any of your 111 prewarmed rows were dropped in the way @Amost- describes.

@ycarogabriel-lgtm

Copy link
Copy Markdown

@Gesalbter thanks for the M4 Air numbers. For comparison, on a MacBook Pro M1 (16 GB, macOS 26.6.2, cold Metal cache, no mesh archive) a 300 s walk built 47 task/mesh pipelines on the submit thread for 103.5 s in total, which is about 2.2 s per pipeline on average, the longest 12.76 s (12.1 s for 3439aaa071d868ae); the frames from the area change took 4-12 s each. Your 0.6-1.65 s per compile is therefore roughly 1.5 to 3.5 times shorter than our average. We cannot say how much of that is the chip and how much the cache state, since we only have the M1.

seedds added a commit that referenced this pull request Nov 9, 2026
…pipeline archive: Ocarina of Time's foliage, shadows and enemies no longer stall at their first draw (59.5 s of builds at the Kokiri Forest entry on an M5 Max with a cold Metal cache, one frame of 42.9 s), and its profile seeds 123 of them (PR #1368)

A native mesh pipeline was built only at its first draw, through the MTLDevice, and nothing kept it: no prewarm row named one and no pipeline serializer saw it. Since ea0c98d the draw waits for its pipeline, so each one met in play was a stall on the submit thread.

- mtk_pso records each pipeline it builds as an M row (the task shader's key or -, then the pso_skey hex with the mesh shader in the vertex place); pso_mesh_skey_parse refuses rows with a vertex layout, a specialized fragment or the wrong stages, and the prewarm builds the rows on its workers (kind 3), so the prepare step has them before the first frame.
- warm_claim takes a prewarmed pipeline under the live shader objects when they come from the same library and entry (warm_mesh_shader_equal): the program prefetch loads objects of its own for the same keys, which claiming by identity dropped; a mesh shader loaded with its other depth entry is not taken.
- mtk_build keeps the pipelines in the pipeline archive as cps_new keeps compute pipelines: looked up first (a p file when a flush holds only them), built through the MTLDevice on a miss (the system's shader cache serves it when it can) and captured into the flush period in the background (cps_harvest generalized to arch_harvest). Compaction captures one taken from an old plain file again; the flush line counts them on their own.
- Ocarina of Time's pso_manifest.tsv carries 123 M rows (Kokiri Forest, Hyrule Field, Castle Town, Hyrule Castle).
- prog_get returns nil for no program (draw_'s passdump line tests S.prog, the lines after it did not; clang-tidy found it once mtk_pso grew).

Measured on an M6 (16 GB), Ocarina of Time's village walk with the prepare step, hosts pinned to the libraries on disk (f9f01e4f9994): main built 56 mesh pipelines at their draws; this change's first start prewarmed 121 (two seed rows need a newer chain's paired mesh outputs, 5e344a8) and built none at a draw, prepare 19.0 s against 2.7 s as 65 were not in the system's cache; the second start took all 121 from the archive, prepare 2.8 s. Contributors' runs on the rows and prewarm: M1 cold 103.5 s of builds in a 300 s walk; iPad Pro (M5) 1.9-5 s per pipeline cold.

Checks: t_pso_skey, t_pso_warm (both compiler paths), t_pso_archive (mesh_pipelines), t_mesh_first_use, t_pso_bundle, t_pso_stall.

From PR #1368 (nanotchec): the M row format, its parser, the prewarm task and the claim fix; its source-expansion tool is left out. Seed rows also from padsiwono (50 new) and PR #1488; Amost- diagnosed the dropped claims.

Co-authored-by: nanotchec <145017345+nanotchec@users.noreply.github.com>
@seedds

seedds commented Nov 9, 2026

Copy link
Copy Markdown
Owner

Landed as bf420fd, with you as co-author. Thank you: the M row format, its parser, the prewarm task and the claim fix are yours and kept as they were.

What changed on the way in:

  • The source-expansion tool is left out (scripts/prewarm_native_mesh.py, pso_mesh_seed.m and its test). It predicts 7,716 pipelines from program × render-state products, and the evidence for it was one castle session on a combined host, where it covered one pipeline. Its cost is a 151 s first preparation and some 7,700 pipelines held in memory. Recorded rows pooled from players cover the same need at no cost. If predicting pipelines is wanted later, the host's pso_prebuild is the place: it already predicts render pairs and skips mesh pairs.
  • The CI-fix commits are left out. Main landed its own version of those fixes in d8b2b64.
  • Mesh pipelines now go into the pipeline archive, the way compute pipelines do. A pipeline is looked up there first. On a miss, the MTLDevice builds it, which the system's shader cache serves when it holds it, and the Metal 4 compiler captures it into the flush period in the background (arch_harvest). So the next start takes it from the archive even after the system cache drops it.
  • Seed rows: your 73, plus 50 new from padsiwono's 118. Ocarina of Time: a prewarm seed list (9,784 rows), so a first start prepares its pipelines instead of freezing for them in play #1488's 58 were already covered. 123 in all, each parsed by pso_mesh_skey_parse and checked against the shader map.

On an M6, Ocarina of Time's village walk with the prepare step:

  • Main: built 56 mesh pipelines at their draws.
  • This change, first start: prewarmed 121 and built none at a draw. Prepare took 19.0 s, as 65 pipelines weren't in the system's cache.
  • This change, second start: took all 121 from the archive, and prepare took 2.8 s.

Two seed rows fail on the older libraries I tested with, because their pixel shader reads an output the mesh shader doesn't write. 5e344a8 fixed that in the chain after those libraries were built, so I kept the rows.

Thanks also to @padsiwono for the rows and the cold-cache numbers, to @Amost- for the dropped-claim diagnosis, and to @aksmith-git and @ycarogabriel-lgtm for the measurements.

@seedds seedds 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
…pipeline archive: Ocarina of Time's foliage, shadows and enemies no longer stall at their first draw (59.5 s of builds at the Kokiri Forest entry on an M5 Max with a cold Metal cache, one frame of 42.9 s), and its profile seeds 123 of them (PR seedds#1368)

A native mesh pipeline was built only at its first draw, through the MTLDevice, and nothing kept it: no prewarm row named one and no pipeline serializer saw it. Since 5e5e7e9 the draw waits for its pipeline, so each one met in play was a stall on the submit thread.

- mtk_pso records each pipeline it builds as an M row (the task shader's key or -, then the pso_skey hex with the mesh shader in the vertex place); pso_mesh_skey_parse refuses rows with a vertex layout, a specialized fragment or the wrong stages, and the prewarm builds the rows on its workers (kind 3), so the prepare step has them before the first frame.
- warm_claim takes a prewarmed pipeline under the live shader objects when they come from the same library and entry (warm_mesh_shader_equal): the program prefetch loads objects of its own for the same keys, which claiming by identity dropped; a mesh shader loaded with its other depth entry is not taken.
- mtk_build keeps the pipelines in the pipeline archive as cps_new keeps compute pipelines: looked up first (a p file when a flush holds only them), built through the MTLDevice on a miss (the system's shader cache serves it when it can) and captured into the flush period in the background (cps_harvest generalized to arch_harvest). Compaction captures one taken from an old plain file again; the flush line counts them on their own.
- Ocarina of Time's pso_manifest.tsv carries 123 M rows (Kokiri Forest, Hyrule Field, Castle Town, Hyrule Castle).
- prog_get returns nil for no program (draw_'s passdump line tests S.prog, the lines after it did not; clang-tidy found it once mtk_pso grew).

Measured on an M6 (16 GB), Ocarina of Time's village walk with the prepare step, hosts pinned to the libraries on disk (f9f01e4f9994): main built 56 mesh pipelines at their draws; this change's first start prewarmed 121 (two seed rows need a newer chain's paired mesh outputs, 7db6904) and built none at a draw, prepare 19.0 s against 2.7 s as 65 were not in the system's cache; the second start took all 121 from the archive, prepare 2.8 s. Contributors' runs on the rows and prewarm: M1 cold 103.5 s of builds in a 300 s walk; iPad Pro (M5) 1.9-5 s per pipeline cold.

Checks: t_pso_skey, t_pso_warm (both compiler paths), t_pso_archive (mesh_pipelines), t_mesh_first_use, t_pso_bundle, t_pso_stall.

From PR seedds#1368 (nanotchec): the M row format, its parser, the prewarm task and the claim fix; its source-expansion tool is left out. Seed rows also from padsiwono (50 new) and PR seedds#1488; Amost- diagnosed the dropped claims.

Co-authored-by: nanotchec <145017345+nanotchec@users.noreply.github.com>
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…eedds#1825 (process-local miss cache for base-pipeline lookups), seedds#1368 (native mesh pipelines in the prewarm manifest, 73 M rows), seedds#1842 (DM_NVN_ISOLATE_TASK)

Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR):
- seedds#1368 by nanotchec (closed): Ocarina of Time: prewarm native mesh pipelines before attacks and enemies appear
- seedds#1825 by socram03 (open): Avoid repeated archive scans for missing pipeline bases
- seedds#1842 by ycarogabriel-lgtm (open): Ocarina of Time: opt-in task isolation (DM_NVN_ISOLATE_TASK), the Hi-Z task a5288 in command buffers of its own, a workaround for its GPU hangs on an M1
- seedds#1844 by nawangwe (closed): Shader libraries compile with -fwrapv: Ocarina of Time's mesh-shader models no longer take their ambient light from the wrong probe texels
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400486024DEC000 A game host The Horizon host: SVCs, services, threads, memory (dm/host) ios The iPhone and iPad app (dm/ios) nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants