Skip to content

A draw that samples the depth it writes draws into a copy of it: Ocarina of Time's snow no longer flickers with black 16×16 blocks - #1995

Closed
scoofz wants to merge 2 commits into
seedds:mainfrom
scoofz:depth-read-while-written
Closed

scoofz wants to merge 2 commits into
seedds:mainfrom
scoofz:depth-read-while-written

Conversation

@scoofz

@scoofz scoofz commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

What and why

Symptom. In The Legend of Zelda: Ocarina of Time (0400486024DEC000, v131072), the snowy areas showed black or dark 16×16 blocks, aligned to the screen grid, where the snow meets rock and walls: in front of the frozen walls, at the foot of pillars, on snowy stairs. The blocks moved every frame. Inside them, what lies behind the snow showed through, as if the snow had failed its depth test there. Nothing changed them: present upscaling off, render scale, docked or handheld, DM_NVN_SKIP_MESH=1, the fixed-resolution cut or not, DM_NVN_FB_SNAP=all, DM_NVN_UNWRITTEN_DC=0.

Cause. The snow draws (55845639d605e028, tessellated, and ee567dea65288f58, among others) test depth LEQUAL with depth writes on, and their fragment shader samples that same depth texture through a bound texture slot (a depth fade against what lies under the snow). Sampling an attachment the draw itself writes is undefined in Metal: on Apple's GPUs whole tiles of another state come back, the same failure as Same-pass feedback read through a snapshot for colour attachments (#377). NVN gives the draw the depth from before it. fb_prepare already ended the pass before such a draw when an earlier draw of the pass had read the depth, but the draw's own read of what it writes stayed undefined.

Change. General, in nvn_mtl.m:

  • fb_prepare: a draw that writes depth while a fragment texture slot (fb_slot_reads, new) or a bindless handle (fb_texu_reads) names the depth attachment's storage asks for a depth copy, and the open pass ends.
  • rp_begin_ex_: the pass that draw opens copies the depth attachment into a texture of the same format and size (g_dswap, kept between uses) and attaches the copy instead. The draw writes the copy and samples the original, which nothing changes until the pass ends.
  • rp_end_at: the copy goes back into the depth attachment. A full barrier orders both copies with the passes around them (g_game_barrier).
  • Only whole single-level 2D depth attachments that the pass does not clear. A request no pass with a depth attachment takes is dropped, not left for a later pass.
  • Reads through the texture pool or a dynamic index are left out on purpose: they may name any texture, and Ocarina of Time keeps its depth textures in the pool every material shader reads (pool_ds_read); taking them would give many draws a pass and two copies each.
  • DM_NVN_DEPTH_SWAP=0 turns it off.

Reference entry: A draw that samples the depth it writes. The game note has a line for it, and the symptom index is regenerated.

Checked

  • Tests:
    • New dm/gfx/tests/t_depth_swap.m (make -C dm/gfx/tests GAME=<id> check_t_depth_swap), through the production fb_prepare, rp_begin and rp_end, with MTL_DEBUG_LAYER=1, once at the default and once with DM_NVN_DEPTH_SWAP=0. A draw writing depth whose texture slot names the depth takes the copy, and the depth it wrote reaches the attachment. A draw writing depth it does not read, and one reading depth it does not write, keep the attachment. A request no depth pass takes is not left for a later pass. 19 checks each, 0 failed.
    • check_t_feedback: 203 checks, 0 failed. One check, "two reports in one command buffer read two times", failed once on main without this change and passed in the run with it; it looks flaky and unrelated.
    • dm/tools/tests/test_docs.py and test_env_switches.py pass. The host builds (make -C dm/host GAME=0400486024DEC000 dmhost).
    • Not run: dm/tools/lint_c.py tidy (no clang-tidy 23 on this Mac).
  • Games run: The Legend of Zelda: Ocarina of Time (0400486024DEC000, v131072), Mac, docked, around the snowy areas (the frozen walls and pillars, the snowy stairs).
  • Devices and OS: MacBook Pro M3 Max, macOS 26.7.2.
  • Regression: not run. regress.py check needs games this Mac does not have beyond Ocarina of Time. The change only acts on a draw that writes depth while a texture slot or bindless handle of its fragment shader names that depth, which was undefined on Metal before; every other draw takes the same path as before.
  • Cost: a pass of its own and two copies of the depth for each such draw (about 6.6 MB each for a 1536×864 depth-stencil). In Ocarina of Time's snowy areas that is about ten draws a frame.

Docs

  • The finding is in docs/reference/rendering.md and the game note, and python3 dm/tools/docs_symptoms.py was run (the symptom index)
  • No game files, keys or data extracted from a game in the change or the attachments

🤖 Generated with Claude Code

…ina of Time's snow no longer flickers with black 16x16 blocks

The snow draws (55845639d605e028, ee567dea65288f58 and others) test depth LEQUAL with writes while their fragment shader samples that depth through a texture slot; Metal leaves the read undefined and Apple's GPUs returned whole 16x16 tiles of another state. fb_prepare now asks such a draw (a fragment texture slot or bindless handle naming the depth it writes) for a pass whose depth attachment is a copy (g_dswap): rp_begin_ex_ copies the depth in, the draw writes the copy and samples the original, and rp_end_at copies it back, with full barriers. Pool and dynamic reads are left out (they may name any texture). DM_NVN_DEPTH_SWAP=0 turns it off. New GPU test t_depth_swap (19 checks, at the default and off); reference entry "A draw that samples the depth it writes".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation game: 0400486024DEC000 A game labels Nov 9, 2026
# Conflicts:
#	dm/games/0400486024DEC000/README.md
@scoofz

scoofz commented Nov 9, 2026

Copy link
Copy Markdown
Contributor Author

Closing: #1917 (merged in 8190693) handles the same case: a draw that writes depth and samples it through a texture slot now reads a copy. That covers the snow draws here (55845639d605e028, ee567dea65288f58). The snow area could not be checked again in game. If the black 16×16 blocks come back, I'll reopen with a capture.

@scoofz scoofz closed this Nov 9, 2026
seedds added a commit that referenced this pull request Nov 10, 2026
…s pass before it copies: the first draw after a clear that samples and writes its target read the content from before the clear, in the colour snapshots and the depth copies alike (issue #1932's patch)

A clear with no draw after it waits to be the load action of the next pass on its targets (R.clear_c / clear_d). fb_prepare's colour snapshot and fb_depth_snap only ended an open pass before copying, so when the draw taking the copy was the first of its pass, the copy held what the attachment had before the game's clear, while the draw wrote into the cleared attachment. Both copy paths now call rp_flush, which ends the pass or lands that clear first. Found by reading dean0396's patch in issue #1932 against what landed in 8190693; not seen in a game.

Checked: check_t_feedback, 281 checks under Metal validation, with new cases for the clear still waiting (real Depth32Float and Depth32Float_Stencil8 depth copies in a colour and a depth-only pass, and a colour snapshot); 6 of them failed before. The graphics suite, test_docs.py, test_env_switches.py; lint_c.py tidy at the baseline. regress.py check on NieR:Automata (its FXAA takes a colour snapshot every frame) against 04811a6: no new red line, fps 59.5 -> 59.7, frame p99 17.74 -> 17.91 ms, snapshot #1000 reached on both sides; the screenshot difference it flagged is the opening flight a moment further along (first frame 0.5 s later), both clean. Not run on Ocarina of Time (no libraries for this chain on disk, no save at the snow) or on iOS.

The reference entry also records Ocarina of Time's snow in adult Zora's Domain (issue #1932) and on frozen walls (PR #1995): its shaders sample the depth they write through texture slot 6, which the copy from 8190693 covers; not yet run on main.

Co-authored-by: dean0396 <4335213+dean0396@users.noreply.github.com>
@seedds

seedds commented Nov 10, 2026

Copy link
Copy Markdown
Owner

Thanks for tracing this down to the snow draws. Main now covers them another way, so I'm closing this PR without landing it:

The storage-overlap match (tex_storage_overlap) would matter if a game samples its depth through a different texture object over the same memory. Nothing shows that here, since #1932's pass dump has the same serial on the slot and the attachment, but tell me if you find such a case.

Could you retest the frozen walls, pillars and stairs on current main? If the blocks are still there, please open an issue with the spot and a pass dump.

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)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants