Skip to content

The Legend of Zelda: Ocarina of Time (0400486024DEC000): renderer, shader and pipeline archive fixes through the Forest Temple - #1590

Closed
ksolenk38ze wants to merge 2 commits into
seedds:mainfrom
ksolenk38ze:oot-playthrough-fixes
Closed

ksolenk38ze wants to merge 2 commits into
seedds:mainfrom
ksolenk38ze:oot-playthrough-fixes

Conversation

@ksolenk38ze

Copy link
Copy Markdown
Contributor

Follow-up to #1444, rebased cleanly onto main (b2d46b86).

Most of the early bring-up from #1444 (EventFd, IAudioInManager, nn::ndve movie networks, BNSH task/mesh shaders, float atomics, ATOMG.CAS, textureGatherOffsets, RGBA8 image atomics, mem_map_limit on, tex_flush_upload / tex_dirty_upload, and the shader_map.tsv / shader_seen.tsv profiles) is now on main through recent commits. This PR carries the remaining renderer fixes from #1444 together with the fixes found while playing The Legend of Zelda: Ocarina of Time (0400486024DEC000, Switch 2 v131072) through all three child dungeons (Great Deku Tree, Dodongo's Cavern, Inside Jabu-Jabu's Belly), Hyrule Field, Market, Hyrule Castle, Kakariko Village, Death Mountain Trail, Zora's River / Domain / Fountain, the Temple of Time (adult timeline transition), the Graveyard, and completion of the first adult dungeon (Forest Temple, including the Poe Sisters and Phantom Ganon).

Picture fixes

  • Mesh stage attribute layout (dm/gfx/tools/dmsh_meshstage.py, dm/gfx/nvn/dmsh_chain.h): Ampere mesh shaders store all vertex attribute blocks (v_blocks = ceil(max_v / 32) blocks of 32 vertices) first in stage memory, followed by all primitive attribute blocks (p_blocks = ceil(max_p / 32) blocks of 32 primitives). dmsh_meshstage.py previously interleaved one 32-vertex block and one 32-primitive block per block = idx >> 5, so whenever max_v > 32 (v_blocks > 1), primitive attributes (attr >= p_first) read and wrote vertex block 1 (v[32..63]). In the Forest Temple, the Poe Sisters and Phantom Ganon painting shaders (ee4ef51a4fddc28e and 83c0dbc51c5481e3, max_v = 128, max_p = 64) carry per-primitive brush-stroke colors in p0..p2; reading vertex block 1's screen-space positions as p0 produced solid green/magenta paintings. dmsh_meshstage.py now places all v_blocks before p_blocks, accounts for sph[0x35] bit 3 (PointSize) in v_slots, and handles ctl[0x5f] point (1) and line (2) topologies. Fixed and verified with unit tests (test_dmsh_meshstage.py, 28 tests) right after completing the Forest Temple; to be visually re-checked in the painting rooms on a revisit.
  • Depth-read render pass split (dm/gfx/nvn/nvn_mtl.m): In fb_prepare, when a draw samples the currently attached depth target (R.rt.depth) in a fragment shader (prog_uses_tid) and an earlier draw in the same Metal render pass wrote depth (g_fb_dwrite), end the current pass before binding the draw. Without a pass boundary on Apple Silicon TBDR, translucent surfaces that sample scene depth for soft-depth fades (the Zora's Sapphire pedestal in Jabu-Jabu's Belly, translucent interior walls) read un-flushed tile depth and flickered with black blocks.
  • Format-view image upgrades and NvImageHeap alias rebinding (dm/gfx/nvn/nvn_mtl.m): image_prepare now persists NVN_TEXTURE_FLAGS_IMAGE on g_treg[tid].t.d.flags when an image binding upgrades a texture and calls tex_upgrade_pfv before the render pass starts when an image view binds a format view (R32Uint over RGBA8Unorm_sRGB in agl_ndsr_vr). tex_upgrade also preserves NvImageHeap ownership on placement textures and rebinds matching g_by_serial, g_ghost, and g_alias views (tex_upgrade_alias_rebind) so dynamic-resolution scaling and volcanic rock / lava shaders do not sample stale textures or leave blocky mosaic tiles.
  • BC1 / BC4 render-target redirection (dm/gfx/nvn/nvn_mtl.m): agl bakes sky and water reflection cubemap faces by attaching RGB_DXT1 (0x42) and RGTC1_UNORM (0x49) textures (flags 0x8 COMPRESSIBLE) as color targets. Since Metal cannot render directly into BC-compressed pixel formats, bcrt_attach redirects those attachments to an RGBA8Unorm / R8Unorm shadow texture and encodes the rendered blocks back into the guest pool / BC texture on rp_end (bc_encode_bc1, bc_encode_bc4).
  • Mesh-to-fragment varying padding (dm/gfx/nvn/nvn_geometry.inc, dm/gfx/tools/shader_split.py): When a fragment shader reads a locnN input that its paired mesh shader (3a6340a04bf09797_b, a203347bb7bdbd03_b) omits, mtk_pad_mesh_fn and var_pairs pad the missing varying with float4(0, 0, 0, 1) so MTL4Compiler does not abort pipeline compilation.
  • Bordered terrain tile detection (dm/gfx/nvn/nvn_mtl.m): tera allocates 260x260 (256 + 2*2 border) and 68x68 (64 + 2*2 border) R16Unorm, RGBA8Unorm, and R8Unorm terrain heightmap, normal, and blend-weight tiles in the same 2.43 GB pool as GPU render targets. Guarding attach()'s g_pool_gpu mark with g_in_rt_attach and recognizing 260x260 / 68x68 bordered tiles in tex_cpu_content preserves CPU terrain uploads across Kakariko Village and Death Mountain.

Frame rate, pipeline archives, and memory

  • Separate ArchEpoch flush states for pulling vs vertex-layout bases (dm/gfx/nvn/nvn_mtl.m): pso_save_base shared a single g_base_epoch between pulling bases (vd == 0, intended for p%05d.mtl4a) and vertex-layout bases (vd != 0, %05d.mtl4a). Whichever type hit the 32-pipeline batch threshold or 2-second timer flushed both into one file, so pulling bases landed in %05d.mtl4a and were discarded whenever pso_compact_dir compacted %05d.mtl4a with include_pull = NO. Splitting g_base_epoch and g_pull_epoch (ep->kind) keeps every pulling base in p%05d.mtl4a. pso_prebuild also probes up to 8 entries from prebuilt.tsv against g_pull_arch4 (PRE_PROBE) so an older pso_archive whose pulling bases were compacted away automatically rebuilds them on launch.
  • Compacted archive ordering and validation (dm/gfx/nvn/nvn_mtl.m): pso4_init places compacted shards (>= 256 MB) at the head of g_arch4 and keeps them out of g_compact_files so they are checked first and not re-copied on every compaction pass, and arch_entries_ok checks _MTL4Archive internal entry offsets before loading an archive.
  • Sub-command-buffer (OP_CALL) lookahead and pulling-base caching (dm/gfx/nvn/nvn_mtl.m): shader_prefetch_rest and pso_predict_rest_ now copy the OP_CALL return stack (cur.ret / cur.rdepth) when starting lookahead from inside a called sub-list so prefetch and PSO prediction continue across sub-list returns into the rest of the frame. hybrid_get_ex also caches the reusable vd == 0 pulling base when compiling a specialized vd != 0 pipeline on an uncached miss, and mtk_pso builds task/mesh pipelines on QOS_CLASS_USER_INITIATED.
  • Retired texture descriptors and bindless pool stability (dm/gfx/nvn/nvn_mtl.m, dm/gfx/nvn/nvn_dirty.h): treg_tex and tex_mtl_ skip materializing or ghosting an unread retired GPU texture when a live alias already owns its (pool, off) range (alias_overlaps), and alias_take_over indexes g_by_serial by (pool, page) and marks retired g_treg slots over taken storage gone. This avoids a cascade of pool_gpu bindless table rebuilds and res_commit spikes in Hyrule Field, Market, and Kakariko Village.
  • Batched sparse-texture uploads and 16 MB upload buffer cache (dm/gfx/nvn/nvn_mtl.m): pkg_upload_ gathers sparse-pool textures (< 64 MB) into a batch-retained MTLBuffer for the batched GPU deswizzle path instead of forcing synchronous up_flush() mid-cutscene, and up_big_src caches up to four 16 MB (BIG_S) upload buffers in g_up_big_src.
  • Static pool_gpu patch arrays (dm/gfx/nvn/nvn_mtl.m): pool_gpu's patch scratch arrays (ids, ser_list, vals, 1.96 MB total) are now static under g_rmu instead of living on the 1 MB dm_bigstack.
  • Game profile and manifest (dm/games/0400486024DEC000/): Increased mem_alias to 0x400000000 (16 GB) for multi-dungeon virtual pool churn, added the v0 base build entry in game.tsv, bridged nn::hid::IsOunceVibrationDeviceMounted in hle_hid.c (called when playing Ocarina notes), and updated pso_manifest.tsv to 40,154 observed (vs, fs, rt) triplets through the Forest Temple and Phantom Ganon.

Upgrade / cache note for existing users

  • DMSH_CHAIN is bumped to 8b11d946d5b4 for the mesh stage layout fix, so existing shader caches rebuild the 8 affected mesh shaders automatically.
  • Existing pso_archive directories do not need to be deleted manually: pso_prebuild now probes prebuilt.tsv against g_pull_arch4 (PRE_PROBE) and automatically regenerates p*.mtl4a if an older compaction run had dropped the pulling bases.

Open observations for follow-up

  • At night in Hyrule Field and Kakariko Village, scene lighting looks brighter than expected (making night and day harder to tell apart), and a thin dark outline can appear on certain silhouettes at night (such as Link's shield) alongside softer texture filtering. We will investigate the night-time environment / filter pass next.

Verification

  • In-game playthrough on macOS (Apple M5 Pro, v131072) through Kokiri Forest, Great Deku Tree, Hyrule Field, Market, Hyrule Castle, Kakariko Village, Death Mountain Trail, Dodongo's Cavern, Zora's River / Domain / Fountain, Inside Jabu-Jabu's Belly, Temple of Time, Graveyard, and the Forest Temple (Poe Sisters & Phantom Ganon).
  • Python lint and unit tests: lint_python.py --against origin/main (0 findings), all 30 test suites in dm/gfx/tests/test_*.py (including test_dmsh_meshstage.py 28/28), dm/tools/tests/test_*.py (test_docs.py, test_game_profile.py, test_env_switches.py, test_image_atomic_bounds.py, test_txq_type.py).
  • C / Metal and host tests: make -C dm/host/tests GAME=010015100B514000 run, GAME=010015100B514000 python3 dm/ios/tools/compile_check.py (261 files, 0 failed), and dm/gfx/tests (t_nvn 243, t_gfx_suspend, t_cpu_tex --metal 68, t_upgrade_alias 14, t_feedback 194, t_lookahead_bindless 22, t_pool_notes 81, t_pso_pending 52, t_pso_skey, t_refl 43).

@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 tools Tools and scripts (dm/tools, scripts) docs Documentation shader chain The shader translation chain (DMSH_CHAIN): a change rebuilds every game's shaders game: 0400486024DEC000 A game labels Nov 6, 2026
@bettterlabs

Copy link
Copy Markdown
Contributor

Built this on a Mac mini (M4, 16 GB, macOS 27.2) from a v131072 NUP: b2d46b86 + #1590 with #1544, #1390, #1543, #1557, #1556, #1575 and #1522. Two things that come from #1590 itself:

1. The NSP script stops at the shader table check

[RED] the split shader tables differ from game profile 0400486024DEC000's:
shader_pairs.tsv: 0 row(s) only in the profile, 1,284 only in the split
  • Cause: var_pairs in shader_split.py now also pairs mesh keys with pixel keys, but dm/games/0400486024DEC000/shader_pairs.tsv was not split again. The Legend of Zelda: Ocarina of Time (0400486024DEC000): renderer, shader and pipeline archive fixes through the Forest Temple #1590's commit is the only change to shader_split.py in that tree.
  • All 1,284 extra rows start with a mesh-stage key, and no row is removed.
  • shader_map.tsv, shader_facts.tsv and pso_pairs.tsv split identical to the profile's, so these are the files the profile registers.
  • With the split's shader_pairs.tsv committed to the profile, the build gets past the check (✓ shader split (2:21): 42,848 shaders).

2. t_pso_bundle: 2 of 58 checks fail on the PR head (4e6d5419) alone

make -C dm/gfx/tests GAME=0400486024DEC000 t_pso_bundle:

other pairs: two bases from the bundle: expected 2, got 3
other pairs: one compiled: expected 1, got 0

It's not in the PR's test list. On b2d46b86 + #1544, #1390 and #1543 the same test passes 58/58.

If #1390 lands too

tera_tile in tex_cpu_content makes two of #1390's t_cpu_tex checks fail: "the one over it is not" and "a write elsewhere in the pool leaves it taken". It takes a 260×260 texture as CPU content even over storage the GPU wrote. Leaving the exception out and keeping #1390's per-range rule passes t_cpu_tex 77/77.

Tests on the whole stack: t_nvn 243/0, t_cpu_tex --metal 77/0, t_pso_archive 102/0, t_pso_warm 99/0, t_upgrade_alias 20/0, t_image_alias 332/0, t_copy_alias 126/0. Python: test_dmsh_meshstage.py (with #1575's memory rule combined), test_pso_offline.py, test_docs.py and test_game_profile.py OK.

Not checked: the cause of the t_pso_bundle failures; t_pso_bundle on bare b2d46b86; the game itself on this build (the shaders are still building).

@seedds

seedds commented Nov 7, 2026

Copy link
Copy Markdown
Owner

A note for this PR: e2b76d4 (from #1604) takes uncompressed textures from their storage unless the GPU wrote that storage, decided by address range rather than by pool. That covers Ocarina of Time's 260x260 and 68x68 terrain textures for any size and any game, so this PR's size rule for them is no longer needed. Please drop it when you rebase.

@ksolenk38ze
ksolenk38ze force-pushed the oot-playthrough-fixes branch from 4e6d541 to d353679 Compare November 7, 2026 02:13
@ksolenk38ze

ksolenk38ze commented Nov 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Thank you for the detailed code review and for landing #1604!

All three review items have been addressed, and the branch has been rebased onto main (e2b76d4e) with tera_tile dropped in favor of the per-range mtl_tex_gpu_written tracking from #1604 (85599141 + 708f16ef):

  1. pso_offline.py (dm/gfx/tools/pso_offline.py)

    • Simplified the two-stage library cache (by_msl) so the pulling and plain variants live side by side in the same dictionary (by_msl[(vs, fs, pulling)]), compiled on demand by get_pair(vs, fs, pulling).
    • Kept the base archive copy only between the two rounds of a row that mixes pulling (vc > 0) and vertexless (vc == 0) variants (need_base_copy = bool(pull_rows and plain_rows)), removing the redundant copy and second serialize_archive pass when all rows of a shader pair take the same path.
    • Added unit coverage in dm/gfx/tests/t_pso_bundle.m (mixed vc > 0 + vc == 0 rows for the same (vs, fs) pair sharing a vertex-pulling vs in pso_offline.py --append).
  2. nvn_refl.m (dm/gfx/nvn/nvn_refl.m)

    • Replaced the two separate const char *has_pull / const char *has_root scans with a single line-by-line scan over the MSL source (refl_msl_flags) that matches actual identifier tokens (_pull followed by ( or [, dm_root followed by () while skipping // comments.
  3. hle_nvn.c (dm/gfx/nvn/hle_nvn.c)

    • Fixed the indentation in nvnMemoryPoolFinalize (dm/gfx/nvn/hle_nvn.c:266).

… attribute layout, pulling-base archive retention, image-view upgrades and retired alias safety through the Forest Temple

Picture:
- dmsh_meshstage.py lays out all v_blocks of 32 vertices before p_blocks of 32 per-primitive attributes in mesh stage memory (and honors sph[0x35] bit 3 PointSize and ctl[0x5f] point/line topologies), so the Forest Temple paintings (Poe Sisters and Phantom Ganon, rendered by ee4ef51a4fddc28e and 83c0dbc51c5481e3 with max_v = 128, max_p = 64 and per-primitive brush-stroke colors) read p0 from the primitive block instead of vertex block 1's screen-space positions.
- fb_prepare ends the render pass when a draw samples R.rt.depth after an earlier draw in the same pass wrote depth (g_fb_dwrite): translucent geometry that reads the attached depth buffer for soft-depth fade (the Zora's Sapphire pedestal, Jabu-Jabu's interior walls) flickered black blocks on TBDR without a pass boundary.
- image_prepare keeps NVN_TEXTURE_FLAGS_IMAGE on g_treg[tid].t.d.flags and upgrades R32Uint format views over RGBA8Unorm_sRGB before the pass (tex_upgrade_pfv), and tex_upgrade keeps an NvImageHeap owner and rebinds g_by_serial, g_ghost and g_alias views (tex_upgrade_alias_rebind) so agl_ndsr_vr does not leave blocky tiles on volcanic rocks or dynamic-resolution cutscenes.
- bcrt_attach redirects renders into RGB_DXT1 (0x42) and RGTC1_UNORM (0x49) targets (flags 0x8) to an RGBA8Unorm / R8Unorm shadow texture and encodes them back on rp_end (bc_encode_bc1, bc_encode_bc4) for sky and water reflection baking.
- mtk_pad_mesh_fn (nvn_geometry.inc) and var_pairs (shader_split.py) pad missing locnN outputs in mesh shaders (3a6340a04bf09797_b, a203347bb7bdbd03_b) with float4(0, 0, 0, 1) when paired with a fragment shader that reads them.

Frame rate, pipeline archives and memory:
- ArchEpoch separates vd == 0 (pulling base) and vd != 0 (vertex-layout base) flush periods (ep->kind) so pulling bases always serialize into p%05d.mtl4a instead of %05d.mtl4a and survive pso_compact_dir compaction (pso_prebuild also probes PRE_PROBE pairs against g_pull_arch4 before trusting prebuilt.tsv); pso4_init places compacted shards >= 256 MB at the head of g_arch4 outside g_compact_files and validates _MTL4Archive entry sizes (arch_entries_ok).
- shader_prefetch_rest and pso_predict_rest_ copy the OP_CALL return stack (cur.ret) so lookahead continues across sub-command-buffer returns; hybrid_get_ex caches the reusable pulling base (vd == 0) on an uncached miss, and mtk_pso builds task/mesh pipelines on QOS_CLASS_USER_INITIATED.
- treg_tex and tex_mtl_ skip re-creating or ghosting an unread retired GPU texture when a live alias already holds its (pool, off) range (alias_overlaps), and alias_take_over indexes g_by_serial by (pool, page) and marks retired g_treg slots over taken storage gone.
- pool_gpu's patch scratch arrays (ids, ser_list, vals) are static under g_rmu instead of taking 1.96 MB on the 1 MB dm_bigstack.
- Profile 0400486024DEC000: 16 GB mem_alias (0x400000000) for multi-dungeon virtual pool churn, v0 build entry, and updated README note.

Checked: test_dmsh_meshstage 28, t_cpu_tex --metal 68, t_upgrade_alias 14, t_feedback 194, t_lookahead_bindless 22, t_pool_notes 81, dm/host/tests, test_docs, test_env_switches, test_game_profile, test_dmsh_pool_arrays, all 0 failed.
@ksolenk38ze
ksolenk38ze force-pushed the oot-playthrough-fixes branch from d353679 to 708f16e Compare November 7, 2026 03:19
cfryerdev pushed a commit to cfryerdev/nxnative that referenced this pull request Nov 7, 2026
…il cached view, on the test stack

seedds#1604 uncompressed textures are taken from their storage unless the GPU
wrote that storage, by address range, not their pool: the terrain no
longer opens holes after walking a distance (streamed tiles came out
zero once any texture in their pool had been a render target; the
260x260 tera_tile special case goes with it). Hand-merged with the
stack's own ownership code: serial GPU writes (seedds#1558) and the BC
render-target shadows (seedds#1590) stay; the image bind records the range
through mtl_tex_gpu_written beside them; mtl_pool_gpu_written is gone
with its last caller.

seedds#1607 a texture ID with no texture drops its cached view: nil == nil
kept a released texture valid on the pool tables' fast path, and the
fast paths gave the shaders its resource ID after the host had released
it (GPU page faults). The nil guard keeps the alias-view parent match.

Tested in game on an M3 Max: a walk between areas holds the ground
whole, no page faults, no regression of the earlier fixes (upscaler
lines, load-time holes, stutters).

Front-ends: host change only, chain unchanged (2027aa29b1b7).
seedds added a commit that referenced this pull request Nov 7, 2026
…ck before the primitive blocks, with the point size ahead of the position and the topology from the control section: Ocarina of Time's Poe Sisters and Phantom Ganon paintings take their per-primitive colours again (chain 8900605baef9, from PR #1590)

Symptom: a mesh shader's per-primitive outputs read as other values once it has more than 32 vertices. Ocarina of Time's Poe Sisters and Phantom Ganon paintings (ee4ef51a4fddc28e, 83c0dbc51c5481e3: 128 vertices, 64 primitives, a per-primitive colour) came out solid green or magenta (pull request #1590). A point mesh took its point size as its position's x.

Cause: the wrapper read the attribute area as blocks of 32 vertices and 32 primitives side by side, each holding the vertex and then the primitive components. The guest writes all its vertex blocks first, then all its primitive blocks, each block only its own SPH's components: ee4ef51a4fddc28e (6 vertex components, 4 primitive) writes vertex v at (v / 32) * 768 + (v % 32) * 4 and primitive p at 3072 + (p / 32) * 512 + (p % 32) * 4. The SPH's point size (byte 0x35 bit 3) is a vertex block's first component: the point mesh 3a6340a04bf09797 writes the size at component 0 and the position at 1-4. Control +0x5f is a primitive's vertices (1 point, 2 line, 3 triangle); Ocarina's 5 mesh shaders with 1 are the 5 that write a point size, the other 495 have 3.

Fix: dmsh_meshstage.memory and to_stage take ceil(max_v / 32) vertex blocks, then ceil(max_p / 32) primitive blocks; the point size goes to [[point_size]]; the topology and a primitive's index bytes follow +0x5f (another value stops the shader's build). The primitive index bytes stay reserved for every primitive block (PR #1575), +0x5f bytes a primitive instead of 3. The pull request's index area, +0x5f * max_p, would have undone #1575 and was not taken; nor were its other changes (reviewed on the pull request).

Checks: test_dmsh_meshstage.py (30: the pull request's layout and point mesh tests, a point mesh memory test, the Metal compile of each case), test_docs.py, test_nsp_chain.py. The wrapper's reads checked against the guest's address arithmetic in the translated ee4ef51a4fddc28e and 3a6340a04bf09797. Regression: Ocarina of Time, its 500 mesh shaders translated and compiled on this chain (no other shader changes): 500 of 500 build, none refused, the largest stage memory 17,172 bytes (21,652 before). Mario Kart World, Splatoon Raiders and Yoshi not checked (no build here); the paintings not seen in the game; line topology not met. Ocarina's foliage point meshes (3a6340a04bf09797, a203347bb7bdbd03) still fail their pipelines on user(locn5), which their pixel shader reads as a texture coordinate (point sprite coordinate replacement suspected; open).

Co-authored-by: ksolenk38ze <352968655+ksolenk38ze@users.noreply.github.com>
@seedds

seedds commented Nov 7, 2026

Copy link
Copy Markdown
Owner

The mesh stage attribute layout from this PR landed as 3ccb7da (chain 8900605baef9), with you as co-author. Thank you.

What I checked. The guest's own address arithmetic confirms it. In ee4ef51a4fddc28e, vertex v is written at (v/32)*768 + (v%32)*4 and primitive p at 3072 + (p/32)*512 + (p%32)*4: all the vertex blocks first, then the primitive blocks. The point mesh 3a6340a04bf09797 writes its point size at component 0 and its position at components 1 to 4. Ocarina's 5 mesh shaders with control +0x5f = 1 are exactly the 5 that write a point size. All 500 of Ocarina's mesh shaders build on the new chain. I haven't seen the paintings in the game myself.

One difference. The index area keeps #1575's rule, reserved for every primitive block: 4 + verts * ceil(max_p/32) * 32. This PR's verts * max_p would have brought back the overflow #1575 fixed.

The rest of this PR. It can't land as one change. The description and the code differ: the BC1/BC4 encoding, the alias rebinding in tex_upgrade and the 16 MB upload cache are described but not in the code. Meanwhile the code has changes the description doesn't mention (a GPU watchdog that is on by default, an indirect dispatch clamp for PRAGMATA, DM_SHADER_CHAIN, pkg_copy_absorb, null guards in the nvnBuffer* getters). Please send the remaining fixes as separate PRs on current main, each with its symptom and a measurement:

seedds added a commit that referenced this pull request Nov 7, 2026
… do not write, as (0,0,0,1) as a vertex shader does: Ocarina of Time's foliage point meshes build their pipelines again (chain 5616dbb62ec3, from PR #1590)

Symptom: [RED] task / mesh pipeline: ... is not found in the output from the previous stage, and those mesh draws skipped. Ocarina of Time's foliage point meshes 3a6340a04bf09797 and a203347bb7bdbd03 write only a point size and a position; their pixel shaders 2a7c48d595feaaeb and cd609edc78b467d8 read generic attribute 5 (pull request #1590).

Cause: a pixel shader input no stage wrote reads a default on the console and Metal refuses the pipeline; vs_pad covers vertex shaders, but mesh shaders had no pairs (shader_split.py var_pairs paired only vertex keys) and to_stage made only the outputs their SPHs enable. Not point sprite coordinate replacement: the pixel shaders' SPH input maps read generic attribute 5 (not a fixed-function texture coordinate, nor the point coordinate at 0x2e0), NVN has no point sprite function (only nvnCommandBufferSetPointSize), and the same file's triangle meshes leave locations 2, 8 and 9 unwritten that their pixel shaders read.

Fix: var_pairs pairs a mesh key with its variation's pixel key; dmsh_build.py passes to_stage what the partners read (partner_inputs, shared with vs_pad), and the locations neither SPH enables become (0,0,0,1) vertex outputs. pso_offline.py plan and the host's pre_desc leave mesh pairs out (mesh pipelines are built at the draw). Ocarina's shader_pairs.tsv split again: 1,284 rows added, all with one of 491 mesh keys, none removed; its other tables split unchanged. test_dmsh_pairs.py gets an output() stand-in (two of its tests had failed since vs_pad read through the compile cache). The pull request's run-time padding on Metal's error text was not taken.

Checks: test_dmsh_meshstage.py (31), test_dmsh_pairs.py (6, DM_GAME set), test_pso_offline.py, test_docs.py, test_nsp_chain.py, test_game_profile.py, the iOS compile check (261 files), lint_python (0 new). Ocarina's 500 mesh shaders and their 1,000 pixel partners built on the chain: 1,500 of 1,500, 19 mesh shaders padded. A Metal mesh pipeline built for each of the 1,284 mesh pairs: 1,283 build; the 31 that needed padding failed before. Open: 626fbf48041cb82b + c5eb985e769e317f (foliage, used by the game), 160 vertices of 14 outputs = 35,840 bytes past Metal's 32,768 mesh size, refused before for linkage and now for size. Regression: Ocarina only (no other mesh-shader game built here); the foliage not seen in the game; Yoshi, Mario Kart World and Splatoon Raiders have no mesh shaders in variations by their maps, not re-split.

Co-authored-by: ksolenk38ze <352968655+ksolenk38ze@users.noreply.github.com>
@seedds

seedds commented Nov 7, 2026

Copy link
Copy Markdown
Owner

A correction to my last comment: padding the missing mesh varyings is the right fix, and it landed as 5e344a8 (chain 5616dbb62ec3), with you as co-author. I was wrong to suggest point-sprite coordinate replacement:

  • The pixel shaders' input maps read generic attribute 5, not a fixed-function texture coordinate (the only kind point-sprite replacement covers) and not the point coordinate (0x2e0).
  • NVN has no point-sprite function, only nvnCommandBufferSetPointSize.
  • The same foliage file's triangle mesh shaders also leave locations 2, 8 and 9 unwritten that their pixel shaders read.

So it's the same case as vs_pad: the console gives an unwritten input a default (0,0,0,1), and Metal needs an explicit output.

How it landed. Your var_pairs change is in, with Ocarina's shader_pairs.tsv split again (the same 1,284 rows). The padding happens offline in dmsh_meshstage.to_stage, from the pixel partners' inputs, instead of at run time on Metal's error text. Both pipeline prebuilds (pso_offline.plan and the host's pre_desc) skip mesh pairs, which are built at the draw.

Results. Of the 1,284 mesh pairs, 31 needed padding, and Metal now builds 1,283 of the pipelines. The one left is 626fbf48041cb82b + c5eb985e769e317f: 160 vertices of 14 outputs is 35,840 bytes, over Metal's 32 KB mesh limit. I haven't checked the foliage in the game.

@seedds

seedds commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Follow-up: the last refused foliage pair (626fbf48041cb82b + c5eb985e769e317f, over Metal's 32 KB mesh size) builds now, as of d777f0a (chain 3ee493b2c3a4).

Cause. Mesh outputs were declared as a full float4 for every attribute, but this mesh shader writes only 26 components per vertex.

Fix. Each output is now declared only as wide as its paired pixel shaders can see: the components the mesh writes, plus a w they read but it doesn't write. On the GPU, Metal fills a narrower output's missing components with 0, which is the console's default for x, y and z, so the pixel shaders read the same values. That mesh comes to 28,160 bytes, and all 1,284 of Ocarina's mesh pairs build their pipelines.

thomasruegsegger pushed a commit to thomasruegsegger/nxnative that referenced this pull request Nov 7, 2026
…s first write stays resident while any of them holds it: releasing one took it out of the residency set (GPU page faults, issue seedds#1326)

The serials of one storage share one packaged texture (g_pkg_share). At the GPU's first write over that storage, cpu_unc_drop gives each of them the package's texture itself (tex_put), so they all hold one texture that was in no shared set. The first holder released (tex_put's ghost-ring eviction, serial_put or serial_del, through obj_drop or defer_obj_release), or the package's own release (NvPkg dealloc's res_remove), took it out of the residency set while the other serials still drew with it, and a command buffer reading it faulted.

pkg_hand_over enters the handed-over texture in g_alias_set and in g_handed, a list by the package's sharing key, so releasing one holder only drops a reference (alias_has). The package's release and its re-upload's spare leave a handed-over texture resident, mtl_alias_drop_pool retires it with its pool, and tex_upgrade moves its entry. It does not enter g_alias, so a new texture object over the same storage still gets a texture of its own. DM_UNC_SHARE=0 gives back the old hand-over.

Ocarina of Time v131072, Mac mini M4 16 GB, macOS 27.2, on main b2d46b8 with seedds#1590, seedds#1557, seedds#1556, seedds#1575, seedds#1522, seedds#1543 and seedds#1390: page faults at 44, 29, 24 and 15.5 minutes, with the missing textures named by DM_RES_CHECK and a diagnostic of where each left the residency set, before two of them, as 260x260 terrain tiles handed over this way and released by the ghost-ring eviction. One binary: with the change, 107 minutes and no fault (86 hand-overs); with DM_UNC_SHARE=0, a fault at 37 minutes with the same kind of tiles named. t_cpu_tex --metal handover_shared: 97 checks, 0 failed; with DM_UNC_SHARE=0, 2 failed. The other GPU suites are unchanged.

(cherry picked from commit 8cbc330)
m-gnr pushed a commit to m-gnr/kid-with-sword-remake that referenced this pull request Nov 7, 2026
- Reconcile upstream PRs (seedds#1455, seedds#1447, seedds#1394, seedds#1439, seedds#1489, seedds#1590, seedds#1663) and commits (ffebfd5, e5f1ade) against local fork
- Confirm PRs seedds#1455, seedds#1447, seedds#1394, and seedds#1439 are already implemented in ad99858
- Establish that host-only renderer optimizations require zero shader rebuilds and reuse the 42,854-entry b-dc815e0f38ca.img library
- Establish that native subword stores (ffebfd5) affect only 515 shaders and leverage cas.db compilation caching
- Rank candidates for base M3 Normal + Handheld (>=40 FPS target, ideal 60 FPS) and identify the top three candidates
- Recommend the smallest independently verifiable first optimization (host-only PSO archive epoch separation from PR seedds#1590)
- Update documentation index, plan, changelog, and known issues
cfryerdev pushed a commit to cfryerdev/nxnative that referenced this pull request Nov 7, 2026
…a, gmem commit repoint on the test stack

seedds#1589 (carrying seedds#1391 + seedds#1482) the GPU memory guard reads the residency
set's size and stops before macOS runs out of RAM; hand-merged with
seedds#1590's ghost-active alias tables. seedds#1599 soft textures (gated, default
off). seedds#1662's tip commit: the ocarina plays into the Mac's microphone
(DM_MIC=1). seedds#1427 raw pool table words follow the pools at commit, as a
candidate for seedds#1326's page faults (its window is skipped, not faulted:
kept for the record). Full-map shader library (42,848), clearing the
no-stamp and c1_full red lines.

In-game: the GPU page fault at scene changes (seedds#1326) remains on this
stack - one session lost the picture at frame 55,940 with the device
revoked; the fault predates seedds#1585, which was excluded after the M2 Pro
report but is not its cause.

Front-ends: host change only, chain 2027aa29b1b7.
@ksolenk38ze

Copy link
Copy Markdown
Contributor Author

Thank you @seedds for landing the mesh stage attribute layout (3ccb7da0), the mesh output padding (5e344a87), and the narrowed mesh output layout (d777f0a3)!

As requested, we have rebased onto current main (bd979ced), dropped the items covered by main or excluded in your review, and split the remaining changes into focused PRs:

Closing #1590 in favor of the split PRs above.

@ksolenk38ze ksolenk38ze closed this Nov 7, 2026
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
…ck before the primitive blocks, with the point size ahead of the position and the topology from the control section: Ocarina of Time's Poe Sisters and Phantom Ganon paintings take their per-primitive colours again (chain 8900605baef9, from PR seedds#1590)

Symptom: a mesh shader's per-primitive outputs read as other values once it has more than 32 vertices. Ocarina of Time's Poe Sisters and Phantom Ganon paintings (ee4ef51a4fddc28e, 83c0dbc51c5481e3: 128 vertices, 64 primitives, a per-primitive colour) came out solid green or magenta (pull request seedds#1590). A point mesh took its point size as its position's x.

Cause: the wrapper read the attribute area as blocks of 32 vertices and 32 primitives side by side, each holding the vertex and then the primitive components. The guest writes all its vertex blocks first, then all its primitive blocks, each block only its own SPH's components: ee4ef51a4fddc28e (6 vertex components, 4 primitive) writes vertex v at (v / 32) * 768 + (v % 32) * 4 and primitive p at 3072 + (p / 32) * 512 + (p % 32) * 4. The SPH's point size (byte 0x35 bit 3) is a vertex block's first component: the point mesh 3a6340a04bf09797 writes the size at component 0 and the position at 1-4. Control +0x5f is a primitive's vertices (1 point, 2 line, 3 triangle); Ocarina's 5 mesh shaders with 1 are the 5 that write a point size, the other 495 have 3.

Fix: dmsh_meshstage.memory and to_stage take ceil(max_v / 32) vertex blocks, then ceil(max_p / 32) primitive blocks; the point size goes to [[point_size]]; the topology and a primitive's index bytes follow +0x5f (another value stops the shader's build). The primitive index bytes stay reserved for every primitive block (PR seedds#1575), +0x5f bytes a primitive instead of 3. The pull request's index area, +0x5f * max_p, would have undone seedds#1575 and was not taken; nor were its other changes (reviewed on the pull request).

Checks: test_dmsh_meshstage.py (30: the pull request's layout and point mesh tests, a point mesh memory test, the Metal compile of each case), test_docs.py, test_nsp_chain.py. The wrapper's reads checked against the guest's address arithmetic in the translated ee4ef51a4fddc28e and 3a6340a04bf09797. Regression: Ocarina of Time, its 500 mesh shaders translated and compiled on this chain (no other shader changes): 500 of 500 build, none refused, the largest stage memory 17,172 bytes (21,652 before). Mario Kart World, Splatoon Raiders and Yoshi not checked (no build here); the paintings not seen in the game; line topology not met. Ocarina's foliage point meshes (3a6340a04bf09797, a203347bb7bdbd03) still fail their pipelines on user(locn5), which their pixel shader reads as a texture coordinate (point sprite coordinate replacement suspected; open).

Co-authored-by: ksolenk38ze <352968655+ksolenk38ze@users.noreply.github.com>
Know-It-All-Brother pushed a commit to Know-It-All-Brother/nxnative that referenced this pull request Nov 9, 2026
… do not write, as (0,0,0,1) as a vertex shader does: Ocarina of Time's foliage point meshes build their pipelines again (chain 5616dbb62ec3, from PR seedds#1590)

Symptom: [RED] task / mesh pipeline: ... is not found in the output from the previous stage, and those mesh draws skipped. Ocarina of Time's foliage point meshes 3a6340a04bf09797 and a203347bb7bdbd03 write only a point size and a position; their pixel shaders 2a7c48d595feaaeb and cd609edc78b467d8 read generic attribute 5 (pull request seedds#1590).

Cause: a pixel shader input no stage wrote reads a default on the console and Metal refuses the pipeline; vs_pad covers vertex shaders, but mesh shaders had no pairs (shader_split.py var_pairs paired only vertex keys) and to_stage made only the outputs their SPHs enable. Not point sprite coordinate replacement: the pixel shaders' SPH input maps read generic attribute 5 (not a fixed-function texture coordinate, nor the point coordinate at 0x2e0), NVN has no point sprite function (only nvnCommandBufferSetPointSize), and the same file's triangle meshes leave locations 2, 8 and 9 unwritten that their pixel shaders read.

Fix: var_pairs pairs a mesh key with its variation's pixel key; dmsh_build.py passes to_stage what the partners read (partner_inputs, shared with vs_pad), and the locations neither SPH enables become (0,0,0,1) vertex outputs. pso_offline.py plan and the host's pre_desc leave mesh pairs out (mesh pipelines are built at the draw). Ocarina's shader_pairs.tsv split again: 1,284 rows added, all with one of 491 mesh keys, none removed; its other tables split unchanged. test_dmsh_pairs.py gets an output() stand-in (two of its tests had failed since vs_pad read through the compile cache). The pull request's run-time padding on Metal's error text was not taken.

Checks: test_dmsh_meshstage.py (31), test_dmsh_pairs.py (6, DM_GAME set), test_pso_offline.py, test_docs.py, test_nsp_chain.py, test_game_profile.py, the iOS compile check (261 files), lint_python (0 new). Ocarina's 500 mesh shaders and their 1,000 pixel partners built on the chain: 1,500 of 1,500, 19 mesh shaders padded. A Metal mesh pipeline built for each of the 1,284 mesh pairs: 1,283 build; the 31 that needed padding failed before. Open: 626fbf48041cb82b + c5eb985e769e317f (foliage, used by the game), 160 vertices of 14 outputs = 35,840 bytes past Metal's 32,768 mesh size, refused before for linkage and now for size. Regression: Ocarina only (no other mesh-shader game built here); the foliage not seen in the game; Yoshi, Mario Kart World and Splatoon Raiders have no mesh shaders in variations by their maps, not re-split.

Co-authored-by: ksolenk38ze <352968655+ksolenk38ze@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400486024DEC000 A game nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) 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.

3 participants