Skip to content

Ocarina of Time: a patch of scenery no longer draws dark for one frame when the game gives it a sharper texture (a pool texture the walk does not make holds a stand-in of its own colours until its first read) - #2075

Closed
sleepyeyedjoe wants to merge 3 commits into
seedds:mainfrom
sleepyeyedjoe:oot-pool-standin-1109

Conversation

@sleepyeyedjoe

@sleepyeyedjoe sleepyeyedjoe commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1969. Built on #2022: its two commits come first, and only the last commit is this change.

In short

  • What it fixes: a patch of distant scenery went dark for exactly one frame when the game switched it to a sharper texture. The traced case is the cliff beside the Castle Town wall in Hyrule Field.
  • How: until a shader reads such a texture for the first time, its pool table entry now holds a tiny copy of the texture's own content instead of the dummy. The first frame is a little soft instead of black.
  • Cost: about 6 µs per registration and 16 MB in all on an M4 Pro. Memory and frame times stay within the run-to-run range.
  • Off switch: DM_NVN_POOL_STANDIN=0.

What and why

Cause.

Pictures. Three presents in a row at the cliff's first read, from the replays: stand-ins off in the top row (the middle frame is dark), stand-ins on in the bottom row.

standin-before-after

The same three frames with stand-ins on, enlarged: in the middle one the cliff samples its stand-in and is a little soft.

standin-zoom

Fix. A texture the walk leaves out for good holds a stand-in in the tables until its first read (pool_stand).

  • What the stand-in holds:
    • the texture's own smallest levels, from the first of at most 1 KB on;
    • or, when it has no level that small, its last level cut down to at most 4 KB a layer: every n-th block of every n-th block row (nvn_deswizzle_pick). The blocks stay as they are, so compressed formats need no decoder.
  • How it is made: the host reads those bytes from the storage on the CPU into a small shared texture cut from a heap that is resident from the start. That takes no upload batch and no residency commit, and a table may name it at once.
  • Which textures get one: 2D textures and 2D arrays of up to 8 layers, with the type, layers, format and channel swizzle of the view tex_by_id makes for the ID. The swizzle rule moves to treg_swizzle, which both use.
  • Unchanged (the dummy): other types, textures in sparse pools, depth formats, and render targets no pass has written.
  • Only once the budget is full. Below it a later walk makes the texture. A table walked while the budget had room is walked once more when the budget has filled.
  • At most 512 a frame. An area load registers thousands at once (Hyrule Field: 5,152, which took 82 ms in one frame without the limit). The rest wait and are written into the tables in place, as a newly read ID's texture is.
  • DM_NVN_POOL_FIRST=1 logs each first read of a texture not made yet and what it found. The totals go to the log every 300 frames.

Evidence

Windowless replays of a player's recorded route past the cliff (DM_KEYS, DM_FRAME_DELTA=0, M4 Pro, macOS 27.0.1): 10 with DM_NVN_POOL_STANDIN=0 and 8 without. The replays are not deterministic (the cliff's set streams in about half of them), so the comparison is per event.

Stand-ins off (10 runs) Stand-ins on (8 runs)
First reads after the area load that found the dummy 31 of 31 0 of 49
The cliff's first read, connected dark cells on a 32x18 luminance grid 9, 10, 10, 12 (4 runs) 0–2 (5 runs)
First reads at the area load that found the dummy, per run 39 5–11
  • The grid's noise: over 17,994 presents without a first read, a connected group of 4 or more cells appears in 7.
  • By picture: with stand-ins off the cliff is dark for one frame; with them on it is a little soft for that frame.
  • Stale stand-ins: none of the 49 was made from storage the game wrote afterwards.
  • The frames left at the area load are the limit of 512 a frame at work: those reads come in the first frames, before every stand-in is made.

Cost

Medians over the runs, stand-ins off and on:

Off On Run-to-run range
Resident memory 10,830 MB 10,883 MB 10,725–11,536
GPU allocated 16,376 MB 15,965 MB 15,739–17,064
Residency set 6,465 objects 6,491 objects
Frame time p50 16.67 ms 16.66 ms
Frame time p95 16.85 ms 17.19 ms 16.8–19.4 in both
Submit+present p50 6.38 ms 6.51 ms 6.2–9.6
  • A run makes about 57,000 stand-ins in 355 ms (6 µs each) and holds about 6,000 at a time, in four 4 MB heaps.
  • The p95 medians differ by 0.34 ms, but runs of both kinds fall in two groups (near 16.8 ms and 17.5–19.4 ms; 3 of 10 and 4 of 8 in the upper one), so the replays show no difference.
  • No new red lines. The known texture type mismatch … texture ID 850 appears in 6 of the 10 runs and 2 of the 8.

Tests

  • New: t_pool_standin (51 checks; 31 with the switch off). It reads the stand-ins back from the GPU and covers:
    • levels as they are, a level cut down, and a compressed texture;
    • an sRGB view with a swizzle, a 2D array, and a layer of it;
    • the first read, another registration, and an emptied ID;
    • a frame's share, and the rest written in place.
  • New: t_tex P checks the picked blocks against the level decoded whole, in both GOB orders.
  • Changed: t_pool_notes and t_pool_chunks. Where they checked that an unread texture past the budget is the dummy, they now check that it is not made (its stand-in, or the dummy with the switch off).
  • Passing: those, t_pool_seen_replace, t_cpu_tex, t_bc_alias, t_defer_chain, t_image_usage, t_image_type, t_view_swizzle, t_stencil_view, t_pfv, t_int_float_view, t_readback; docs_symptoms.py --check, test_docs.py, test_env_switches.py.

Not done

  • Other games: scripts/regress.py was not run, as this Mac has no other game's build. The stand-ins act only once pool_budget() is spent.
  • Live play (one short session): on the installed build the player walked Hyrule Field toward the wall for about two minutes (M4 Pro, fullscreen) and saw no dark frame on the cliffs. The log's totals for that session: 46 first reads found a stand-in and 8 the dummy. All 8 came within 300 frames of the area load, while stand-ins were still waiting; after that, 39 of 39 found a stand-in. Frame time p95 was 17.35 ms and submit+present p50 7.35 ms (medians of the session's 300-frame windows), against 17.75 and 8.04 ms in the first 9,000 frames of the previous session on the old host, which was not the same route. No red lines.
  • The diagnostics behind the pictures and the luminance grid are not part of this PR.

🤖 Generated with Claude Code

sleepyeyedjoe and others added 3 commits November 9, 2026 17:37
… 4-5 frames apart, when the game streams new textures into them: a texture ID a shader has read keeps its read mark when it gets another texture (DM_NVN_POOL_SEEN_KEEP)

The models' shaders read their textures through the bindless pool table. When a walk writes an ID's entry it makes the ID's Metal texture if it fits the walk's room (pool_room, within pool_budget()), or, whatever the room, if a shader has read the ID (pool_seen, under POOL_REQ_BYTES); otherwise the entry is the dummy (black) and pool_seen_scan makes the texture after the first read. In Ocarina of Time the room is always 0: pool_budget() is a twentieth of Metal's working set (909 MB on a 24 GB M4 Pro) against 15-18 GB of currentAllocatedSize, so only read IDs are made ahead.

mtl_reg_tex cleared the read mark whenever the game registered another texture into an ID. The game's streaming replaces the 17 textures of the castle town wall's material set at once (3-7 MB each), finalizing the old texture first, and registers each step twice, 4-5 frames apart. With the marks cleared the replacements counted as unread, and each registration's first read found the dummy: the wall, the castle, rocks and trees drew black for one frame, twice, with the fog in front.

Now an ID that held a made texture (retired by the game, or still viewed; the finalize drops the view before the new registration arrives) keeps its mark, so the next walk makes the replacement under the read budget. An ID that held none, or is emptied, is cleared as before; DM_NVN_POOL_SEEN_KEEP=0 restores the old clearing. A texture streamed into an ID no shader has read yet still reads the dummy once, a single dark frame (issue seedds#1969).

Replays of a player's route past the wall (M4 Pro, macOS 27.0.1), every flagged frame checked by picture: 4-5 dark frames flagged per run before (2-4 at the wall's streaming steps), 0 in 5 runs with the change and 0 in 5 runs under a stress that makes streaming steps collide; with DM_NVN_POOL_SEEN_KEEP=0 the pairs return. Frame intervals and GPU time unchanged. Another user saw the same pairs on an M5 Max and none with a change of the same kind (issue seedds#1969).

t_pool_chunks: its check that an ID given another texture is unread now checks the new rule (kept for a made texture, cleared under DM_NVN_POOL_SEEN_KEEP=0), and that an emptied ID and an ID given its first texture are unread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exercise a historically sampled ID with no ordinary pool-walk room, both when replacing a live texture and after Finalize drops its view. Check the first table's replacement GPU pixels, old table entry stability, unread and empty IDs, unwritten targets, and the clearing control. Exhaust the existing budget counter rather than allocating a large working set.

The fixture passes 38/38 with the fix in PR2022 and 36/36 with DM_NVN_POOL_SEEN_KEEP=0. It uses synthetic 1 MiB storage and requires no game assets. These direct-backend configuration variants do not independently stress replay-worker concurrency.

From akratch's commit 6f251e7, offered on PR 2022. Rerun on an M4 Pro (macOS 27.0.1) on this branch: 38/38, 38/38 and 36/36. Against nvn_mtl.m from before the fix it fails 6 of 38, the six replacement checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e when the game gives it a sharper texture: a pool texture the walk does not make holds a stand-in of its own colours until its first read (DM_NVN_POOL_STANDIN, issue seedds#1969)

The surface's shader reads its textures through the bindless pool table. Once pool_budget() is spent a walk makes only the textures a shader has read (pool_seen), so an ID no shader has read held the dummy, which reads 0; the first read sets the ID's bit and pool_seen_scan makes the texture for the next frame. Ocarina of Time keeps a texture set of its own for each piece of distant scenery and resolution (one level each: a BC5 texture, and two three-layer arrays, BC6H and BC7, of half its size) and registers a new set 12 to 969 frames before the material names it, so the first frame of each such step read the dummy: the cliff beside the castle town wall dark for one frame. Which registrations a shader will read is not known ahead: in 58 s of play the game registered 14,455 textures that stayed unread, 9.8 GB, against 10 first reads, and making textures ahead by age and size cost 0.7-1.9 GB for one hit in three.

Now a texture the walk leaves out for good holds a stand-in in the tables until its first read (pool_stand): its own smallest levels, from the first of at most 1 KB on, or, when it has no level that small (the sets above have one level), its last level cut down to at most 4 KB a layer, every n-th block of every n-th block row (nvn_deswizzle_pick; the blocks stay as they are, so compressed formats need no decoder). The host reads them from the storage on the CPU into a small shared texture cut from a heap that is resident from the start, so making one takes no upload batch and no residency commit, and a table may name it at once. The first read then samples the texture's own colours, coarse, for the one frame before the texture is there. 2D textures and 2D arrays of up to 8 layers get one, with the type, layers, format and channel swizzle of the view tex_by_id makes for the ID (the swizzle rule moves to treg_swizzle, which both use); other types, textures in sparse pools, depth formats and render targets no pass has written hold the dummy as before. Only once the budget is full: below it a later walk makes the texture, and a table walked then is walked once more when the budget has filled. At most 512 are made a frame, as an area load registers thousands at once (Hyrule Field: 5,152, 82 ms in one frame without the limit); the rest wait and are written into the tables in place, as a newly read ID's texture is. DM_NVN_POOL_STANDIN=0 keeps the dummy. DM_NVN_POOL_FIRST=1 logs each first read of a texture not made yet and what it found; the totals go to the log every 300 frames.

Replays of a player's route past the cliff (M4 Pro, macOS 27.0.1), 10 with DM_NVN_POOL_STANDIN=0 and 8 without. After the area load, 31 first reads of a texture not made found the dummy in the 10 runs and none in the 8, where 49 found a stand-in (none made from storage the game wrote afterwards). On a 32x18 luminance grid of every present the cliff's first read is a connected group of 9-12 cells at 0.70-0.79 of the presents before and after in each of the 4 baseline runs in which its set streamed, and at most 2 cells, the grid's noise (a group of 4 or more in 7 of 17,994 presents elsewhere), in the 5 runs with stand-ins; by picture the cliff is dark for one frame in the one and a little soft in the other. At the area load 39 first reads a run found the dummy, and 5-11 with stand-ins (the frame's share). A run makes about 57,000 stand-ins in 355 ms (6 us each) and holds about 6,000 at a time, in 16 MB of heaps. Medians without and with: resident 10,830 and 10,883 MB (runs 10,725-11,536 and 10,726-11,398), GPU allocated 16,376 and 15,965 MB, residency set 6,465 and 6,491 objects, frame time p50 16.67 and 16.66 ms, p95 16.85 and 17.19 ms (runs of both kinds between 16.8 and 19.4; 3 of 10 and 4 of 8 above 17.5), submit+present p50 6.38 and 6.51 ms. No new red lines (the known "texture type mismatch ... texture ID 850" in 6 of the 10 runs and 2 of the 8). Not yet checked in live play, and scripts/regress.py was not run (no other game's build on this Mac).

Tests: t_pool_standin (which registrations get a stand-in and what it holds, read back from the GPU: levels as they are, a level cut down, a compressed texture, an sRGB view with a swizzle, a 2D array and a layer of it; the first read; another registration; an emptied ID; a frame's share and the rest written in place; with DM_NVN_POOL_STANDIN=0 the dummy), t_tex P (the picked blocks against the level decoded whole, in both GOB orders). t_pool_notes and t_pool_chunks: where they checked that an unread texture past the budget is the dummy, they now check that it is not made (its stand-in, or the dummy with the switch off). Passing: those, t_pool_seen_replace, t_cpu_tex, t_bc_alias, t_defer_chain, t_image_usage, t_image_type, t_view_swizzle, t_stencil_view, t_pfv, t_int_float_view, t_readback; docs_symptoms.py --check, test_docs.py, test_env_switches.py.

Co-Authored-By: Claude <noreply@anthropic.com>
@Itsjonaathan

Copy link
Copy Markdown

I tested this PR (with #2022, which it includes) on a MacBook Air M5 (10-core GPU, 16 GB, macOS 27.2). It was applied to 04811a63, with the host rebuilt and the shader chain unchanged (39eb2f781d07). The game renders at 1280x720.

  • Before: in one session of about 8 minutes, surfaces went black for a moment three times, twice in Kakariko Village and once in the Kakariko Graveyard.
  • After: no black flashes in about 4 minutes in the same areas. The frame rate is unchanged at 60 fps, GPU p50 12–14 ms, with 0–4 frames dropped per 300.
  • Stand-ins: the log shows them working: pool stand-ins up to frame 1448: 4492 made in 56.5 ms (1249 of them a level cut down), 1322 gave way to their texture, 3140 held now in 3 heaps of 4 MB (0 not made: the heaps were full).
  • Tests: t_pool_standin (51), t_pool_seen_replace (38), t_pool_chunks (148) and t_pool_notes (81) pass, 0 failed.

The check was by eye, and the session after was short; it was not a frame-by-frame comparison.

betogzo added a commit to betogzo/nxnative that referenced this pull request Nov 10, 2026
seedds#2030, seedds#2077, seedds#2022, seedds#2075, seedds#2085), last merged at 04811a6, and the Castle Town result on a MacBook Air M4

Castle Town on a 16 GB MacBook Air M4 at 30 fps and full resolution: GPU p50 26-27 ms a frame and 6.9 GB of GPU memory allocated with seedds#2085, against 16.2 GB and about 9 fps before it.

Tests: dm/tools/tests/test_docs.py passes.
FeReuss pushed a commit to FeReuss/nxnative-oot that referenced this pull request Nov 10, 2026
…tch of scenery no longer draws dark for one frame when the game gives it a sharper texture (a pool texture the walk does not make

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
seedds added a commit that referenced this pull request Nov 10, 2026
…s until its first read: Ocarina of Time's cliff beside the castle town wall, and other streamed scenery, no longer draws dark for one frame when the game gives it a sharper texture (PR #2075)

Once pool_budget() is spent a walk makes only the textures a shader has read (pool_seen), and an ID no shader has read held the dummy, which reads 0: the first read set its bit and pool_seen_scan made the texture for the next frame, so that frame was dark. Ocarina of Time keeps a texture set of its own for each piece of distant scenery and resolution (one level each: a BC5 texture and two three-layer BC6H / BC7 arrays) and registers a new set 12 to 969 frames before the material names it. Which registrations will be read cannot be known ahead (14,455 stayed unread in 58 s of play, 9.8 GB, against 10 first reads), and making them all is what lazy making exists to avoid, so the miss frame stays; this changes what it reads.

The entry holds a stand-in (pool_stand): the texture's own smallest levels, from the first of at most 1 KB on, or its last level cut down to at most 4 KB a layer, every n-th block of every n-th block row (nvn_deswizzle_pick; blocks as they are, so no decoder). It is read from the storage on the CPU into a small shared texture cut from a heap resident from the start, for 2D textures and 2D arrays of up to 8 layers with the view's type, layers, format and swizzle (the swizzle rule moves to treg_swizzle, which tex_by_id uses too). Only once the budget is full; at most 512 a frame, the rest written into the tables in place. DM_NVN_POOL_STANDIN=0: the dummy, as before; DM_NVN_POOL_FIRST=1 logs each first read of a texture not made and what it found, including a stand-in whose storage the game wrote since.

Rebased onto 6d9edb0's read-mark rule (the PR carried #2022's earlier form); the test's readback buffer is checked for nil (lint_c).

Checked: t_pool_standin 51/51 with each encoder setting (31/31 with DM_NVN_POOL_STANDIN=0), t_pool_chunks 151, t_pool_notes 81 (both settings), t_pool_seen_replace 44, t_view_swizzle 554, t_cpu_tex, t_stencil_view, t_pfv, t_int_float_view; t_tex 5,382 (P: picked blocks against the whole level, both GOB orders), also under ASan/UBSan; the host builds; lint_c at the baseline; docs_symptoms --check, test_docs, test_env_switches. The PR's replays of a player's route (M4 Pro): after the area load 31 of 31 first reads found the dummy with stand-ins off, 0 of 49 with them on (none stale); memory and frame times within run-to-run range. A MacBook Air M5 tester: three black flashes in Kakariko in 8 minutes before, none in 4 after. No game run here: only pool-table games reach it, and none has libraries on the current chain.

Fixes #1969

Co-authored-by: sleepyeyedjoe <353778822+sleepyeyedjoe@users.noreply.github.com>
@seedds

seedds commented Nov 10, 2026

Copy link
Copy Markdown
Owner

Landed on main as 6c67644, with you credited as co-author. Thank you: the per-event replay numbers and the before/after pictures made the case clear. Thanks also to Itsjonaathan for the MacBook Air M5 test.

What changed on the way in:

Checked here (M-series Mac, macOS 27.2):

  • t_pool_standin: 51/51 with each encoder setting, 31/31 with stand-ins off.
  • t_pool_chunks: 151, and t_pool_notes: 81, both with stand-ins on and off.
  • t_pool_seen_replace: 44, and t_view_swizzle: 554.
  • t_tex: 5,382, also under ASan/UBSan.
  • The host builds, the C lint stays at its baseline, and the docs and env-switch tests pass.

Not run here: no game. Only games that read through the pool table reach this, and none of the local builds has libraries on the current chain. A short session on main would confirm it.

This closes #1969.

@seedds seedds closed this Nov 10, 2026
FeReuss pushed a commit to FeReuss/nxnative-oot that referenced this pull request Nov 10, 2026
Upstream absorbed PR seedds#2022 (pool_seen read mark kept on replacement, now
unconditional, DM_NVN_POOL_SEEN_KEEP gone) and PR seedds#2075 in reworked commits;
conflicts take upstream's versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
seedds#1901 seedds#1459 seedds#1928 seedds#1942 seedds#1940 seedds#1952 seedds#1611 seedds#2065 seedds#2083 seedds#2008 seedds#2079 seedds#2022 seedds#2075 seedds#2077 seedds#2020 seedds#2021 onto main 04811a6

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7

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#1459 by PromethOps (open): DM_UPSCALER=metalfx takes agl's agl_ndsr_vr.pck networks too, and a variant with no curve and no power is drawn as one colour space: Ocarina of Time's upscaler is MetalFX's
- seedds#1611 by Esteban-V (open): An argument block whose only change is a uniform buffer is patched instead of encoded again
- seedds#1774 by uncraftablexd (closed): Compute threads that exit before a barrier keep arriving at it, and every shader loop has a cap (GPU hang defences), chain change
- seedds#1824 by cocoman93 (open): Clamp texel fetches past a sampled 2D texture's edge: no hard edge in Ocarina of Time's mist
- seedds#1901 by FifteenPen (open): The game's compute dispatches with no game barrier between them run side by side (profile key dispatch_overlap, on for Ocarina of Time): Kokiri Forest GPU p50 15.63 → 14.76 ms
- seedds#1928 by FeReuss (open): Ocarina of Time: enc_thread on, the replay encodes on a thread of its own
- seedds#1929 by FeReuss (open): Unused border corrections are compiled out of pipelines that bind no sampler with a twin (function constant DMSH_BORDER_FC)
- seedds#1930 by FeReuss (open): A float add to storage memory with no result read is one native atomic add instead of a compare-and-swap loop
- seedds#1940 by uncraftablexd (open): A prewarm list's lines after its #@after_first_frame line are built after the first frame, in the background, instead of in the prepare step (issue seedds#1747)
- seedds#1942 by uncraftablexd (open): Frame step: late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed
- seedds#1952 by AdyaGMD (open): Compute moves ahead of scene passes past occlusion queries and conditional rendering; clears wait across dispatches
- seedds#2008 by bquilty25 (open): nvn: reactive memory watchdog, 16-way view cache, and texture upload batching
- seedds#2020 by padsiwono (open): Ocarina of Time v131072: Output Native draws the full output size instead of the game's 80 % frame (`DM_OUTPUT`)
- seedds#2021 by padsiwono (open): Ocarina of Time v131072 renders at the launcher's Resolution (`DM_RENDER_SIZE`), docked
- seedds#2022 by sleepyeyedjoe (closed): Ocarina of Time: large models no longer go black for one frame twice, 4–5 frames apart, when the game streams new textures into them (a texture ID a shader has read keeps its read mark when it gets another texture)
- seedds#2030 by humanperzeus (open): A NaN colour at alpha 0 is returned as 0, as NVIDIA's blender takes a zero factor: Ocarina of Time's white clouds over the ferns are gone
- seedds#2035 by Know-It-All-Brother (open): The shader build runs as worker processes, and no shader waits for a batch (Ocarina of Time's cold Mac map: 3,002 s to 1,298 s)
- seedds#2065 by Raymon82777 (closed): A vertex library with layer entry points of its own gets bases for its main0 pairs: Ocarina of Time's 230 ms camera-turn hitch in Hyrule Field goes to 39 ms
- seedds#2075 by sleepyeyedjoe (closed): Ocarina of Time: a patch of scenery no longer draws dark for one frame when the game gives it a sharper texture (a pool texture the walk does not make holds a stand-in of its own colours until its first read)
- seedds#2077 by roandegraaf (closed): Ocarina of Time: fix the "plaid" terrain / missing texture tiles (GPU-drawn textures now reach the storage the game copies)
- seedds#2079 by uncraftablexd (open): Render pipelines that share a vertex function are taken from the archive or compiled one at a time: the AGX driver crashes in createVertexProgramVariant's completion block
- seedds#2083 by FifteenPen (closed): Ocarina of Time's prewarm list gets 41 more task / mesh pipelines from later play
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…a 23 was built from

Ours kept where main has not taken the change: the pool test of image_texture (virtual pools), need_make with seedds#2075's stand-in,
the copy absorb beside main's gsb_mark, DM_SHARPEN's copy_t2t_draw_x, the render scale (seedds#1903) and fixed resolution stubs beside
seedds#2020's native output and seedds#2021's render size. Main's own taken for the depth rule (fb_depth_rule), the family textures and the
movie network: the opt-in shortcut of PR seedds#1331 (DM_NVN_NDVE_MOVIE_DIRECT) is removed, main runs the network as host kernels.
Generated files (chain header, lock, translator binaries, symptoms, game-patches) are main's here and rebuilt in the next commit.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7

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#1331 by givethesourceplox (closed): Scale decoded NDVE movies on Metal and skip matched reconstruction
- seedds#1903 by FifteenPen (open): Ocarina of Time takes the launcher's Render scale: the 3D views at that fraction of the game's own size, the dynamic resolution held at its top level (Kokiri Forest docked at 0.75: GPU p50 15.77 → 14.31 ms, 60 fps held)
- seedds#2020 by padsiwono (open): Ocarina of Time v131072: Output Native draws the full output size instead of the game's 80 % frame (`DM_OUTPUT`)
- seedds#2021 by padsiwono (open): Ocarina of Time v131072 renders at the launcher's Resolution (`DM_RENDER_SIZE`), docked
- seedds#2075 by sleepyeyedjoe (closed): Ocarina of Time: a patch of scenery no longer draws dark for one frame when the game gives it a sharper texture (a pool texture the walk does not make holds a stand-in of its own colours until its first read)
iDams pushed a commit to iDams/nxnative that referenced this pull request Nov 10, 2026
…s until its first read: Ocarina of Time's cliff beside the castle town wall, and other streamed scenery, no longer draws dark for one frame when the game gives it a sharper texture (PR seedds#2075)

Once pool_budget() is spent a walk makes only the textures a shader has read (pool_seen), and an ID no shader has read held the dummy, which reads 0: the first read set its bit and pool_seen_scan made the texture for the next frame, so that frame was dark. Ocarina of Time keeps a texture set of its own for each piece of distant scenery and resolution (one level each: a BC5 texture and two three-layer BC6H / BC7 arrays) and registers a new set 12 to 969 frames before the material names it. Which registrations will be read cannot be known ahead (14,455 stayed unread in 58 s of play, 9.8 GB, against 10 first reads), and making them all is what lazy making exists to avoid, so the miss frame stays; this changes what it reads.

The entry holds a stand-in (pool_stand): the texture's own smallest levels, from the first of at most 1 KB on, or its last level cut down to at most 4 KB a layer, every n-th block of every n-th block row (nvn_deswizzle_pick; blocks as they are, so no decoder). It is read from the storage on the CPU into a small shared texture cut from a heap resident from the start, for 2D textures and 2D arrays of up to 8 layers with the view's type, layers, format and swizzle (the swizzle rule moves to treg_swizzle, which tex_by_id uses too). Only once the budget is full; at most 512 a frame, the rest written into the tables in place. DM_NVN_POOL_STANDIN=0: the dummy, as before; DM_NVN_POOL_FIRST=1 logs each first read of a texture not made and what it found, including a stand-in whose storage the game wrote since.

Rebased onto 6d9edb0's read-mark rule (the PR carried seedds#2022's earlier form); the test's readback buffer is checked for nil (lint_c).

Checked: t_pool_standin 51/51 with each encoder setting (31/31 with DM_NVN_POOL_STANDIN=0), t_pool_chunks 151, t_pool_notes 81 (both settings), t_pool_seen_replace 44, t_view_swizzle 554, t_cpu_tex, t_stencil_view, t_pfv, t_int_float_view; t_tex 5,382 (P: picked blocks against the whole level, both GOB orders), also under ASan/UBSan; the host builds; lint_c at the baseline; docs_symptoms --check, test_docs, test_env_switches. The PR's replays of a player's route (M4 Pro): after the area load 31 of 31 first reads found the dummy with stand-ins off, 0 of 49 with them on (none stale); memory and frame times within run-to-run range. A MacBook Air M5 tester: three black flashes in Kakariko in 8 minutes before, none in 4 after. No game run here: only pool-table games reach it, and none has libraries on the current chain.

Fixes seedds#1969

Co-authored-by: sleepyeyedjoe <353778822+sleepyeyedjoe@users.noreply.github.com>
betogzo added a commit to betogzo/nxnative that referenced this pull request Nov 10, 2026
seedds#2085: upstream merged them (6c3aeb3, 6d9edb0, 6c67644, 6f50de0), and the merge that follows takes upstream's versions

Reverts 0218338, faa7afe, cfce7d8, f92d3f3 and a74098c.
betogzo added a commit to betogzo/nxnative that referenced this pull request Nov 10, 2026
 and seedds#2085 in upstream's versions, the host at -O2, Ocarina of Time's camera-turn hitch in Hyrule Field (230 → 39 ms), 41 more task / mesh pipelines and 4,959 render / compute pipelines in its prewarm list, GPU writes into uploaded textures kept on the GPU, compact NVN recordings; chain 8f2d8a09c3f9

PR seedds#2030 (blend_zero) stays the fork's own: upstream moved the shader build's MSL rewrites into dmsh_rewrite.py (80fc2ec), so blend_zero and BLEND_ZERO move there, dmsh_build.py finish calls dmsh_rewrite.blend_zero, and test_dmsh_blend_zero.py imports it. dmsh_chain.h regenerated (8f2d8a09c3f9).

Tests: test_dmsh_*.py 26/26 OK (test_dmsh_pairs with DM_GAME=0400486024DEC000); t_pool_chunks 151/0, t_cpu_storage 135/0, t_readback 218/0, t_pool_seen_replace 44/0, t_pool_standin 31/0, t_pool_notes 81/0, t_mesh_first_use 53/0.
computerguy526 added a commit to computerguy526/nxnative that referenced this pull request Nov 13, 2026
…e page table, so PRAGMATA's cut to the tablet no longer shows a dark frame

The stand-ins of seedds#2075 left out every texture in a sparse pool, whose address
range must be read through its page table (an unmapped page stops the host
with SIGBUS). PRAGMATA puts all its streamed textures in sparse pools and is
past the walk's budget from its first scene, so the materials first read at a
scene cut still found the dummy: the whole-picture luma at the cut from the
tower to the tablet fell from 65 to 22 for a frame.

stand_fill gathers each layer's bytes from the stand-in's first level to the
layer's end through the page table (nvn_sparse_copy, at most 256 KB a layer)
and decodes them with nvn_deswizzle_from / nvn_deswizzle_pick. A texture with
the SPARSE flag gets one only from its mip tail on. A page behind those levels
not mapped yet: the ID keeps the dummy and stays open (stand_no 0), so it is
looked at again when a mapping over its pages notes it (treg_sparse_notes) or
at its next walk, where it was refused for the registration's life before.
Such a refusal counts toward the 512 stand-ins a frame, so a game that maps
every frame retries at most that many.

Test: t_pool_standin_sparse (new, Metal validation), 23 checks, including a
mapping in the same frame with no walk; with a refused ID kept closed, as on
main, that check fails. t_pool_standin and t_pool_notes pass. Reference entry
in rendering.md, and a line in PRAGMATA's note.

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

Labels

docs Documentation nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) textures Texture layouts and formats (dm/gfx/tex)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ocarina of Time (0400486024DEC000 v131072, Mac): parts of the scenery flash dark for a split second when the game loads a sharper version of them

4 participants