Repository navigation
A draw that writes depth and samples its own depth attachment reads a copy of it: Ocarina of Time's light-blue dashes over the rock at Death Mountain Trail's cave entrance are gone - #1917
Conversation
… copy of it: Ocarina of Time's light-blue dashes over the rock at Death Mountain Trail's cave entrance are gone A full-screen pass (25a8adc977ff813d) and five decals after it sample the scene depth through a texture slot while writing it with LEQUAL. On Apple's GPUs a depth attachment sampled by the pass that renders into it reads compressed blocks of undefined content, so the comparison against it failed block by block. fb_depth_snap (called by fb_prepare, every game) ends the pass, copies the attachment's level and layer into a snapshot (fb_snap) and binds the copy, reused until the game's next barrier as the colour snapshots are. DM_NVN_FB_DSNAP=0 reads the attachment directly. Check: check_t_feedback (depth_snap_case).
|
Independent test coverage from an M4 Max, macOS 27.2 / Xcode 27.2. The submitted RGBA stand-in is useful for substitution and reuse, but cannot exercise an actual depth format or the depth attachment's store/copy path. I added a second case that clears a private The test-only diff below is against this PR's Additional real-depth regressiondiff --git a/dm/gfx/tests/t_feedback.m b/dm/gfx/tests/t_feedback.m
index 5b151b8a..215ec54f 100644
--- a/dm/gfx/tests/t_feedback.m
+++ b/dm/gfx/tests/t_feedback.m
@@ -597,6 +597,43 @@ static void depth_snap_case(NvProg *pr) {
mtl_tex_release(&output); mtl_tex_release(&depth);
printf("depth copy: %s\n", before == failed ? "PASS" : "FAIL");
}
+// Exercise the depth snapshot with an actual private Depth32Float attachment.
+// The RGBA fixture above checks substitution and reuse; it cannot catch a depth
+// format, render-pass store or depth-to-buffer copy error.
+static void real_depth_snap_case(NvProg *pr) {
+ tex_t colour, depth;
+ target(&colour, 99930);
+ depth = (tex_t){.magic=MAGIC('T','E','X',0), .serial=99931};
+ depth.d = (nvn_texdesc){.target=NVN_TEXTURE_TARGET_2D, .format=NVN_FORMAT_DEPTH32F,
+ .width=4, .height=4, .depth=1, .levels=1, .samples=1};
+ id<MTLTexture> dt = [g_dev newTextureWithDescriptor:tex_desc(&depth, 0)];
+ CHECK(dt != nil, "real depth attachment allocates");
+ if (!dt) { mtl_tex_release(&colour); return; }
+ res_add(dt); mtl_tex_init(&depth); serial_put(depth.serial, dt);
+ mtl_reg_tex(653, &depth, NULL);
+ id<MTLBuffer> result = [g_dev newBufferWithLength:1024 options:MTLResourceStorageModeShared];
+ CHECK(result != nil, "real depth readback allocates");
+ if (!result) { mtl_tex_release(&colour); mtl_tex_release(&depth); return; }
+ res_add(result);
+ state_reset(); memset(&R, 0, sizeof R); mtl_sub_begin();
+ R.has_rt = 1; R.rt.n = 1; R.rt.color[0] = &colour; R.rt.depth = &depth;
+ S.ds.dt = S.ds.dw = 1; S.tex[1][0] = 653; pr->sh[1]->rf.b[0].reg = 0;
+ R.clear_d = 1; R.cd = 0.375;
+ clear(MTLClearColorMake(0, 0, 0, 1));
+ fb_prepare(pr);
+ id<MTLTexture> snap = g_fb_n == 1 ? g_fb_use[0] : nil;
+ CHECK(snap && snap != dt && snap.pixelFormat == MTLPixelFormatDepth32Float,
+ "real depth feedback substitutes a Depth32Float snapshot");
+ if (snap) { blit_barrier(); readback(snap, result, 0); }
+ R.rt.depth = NULL; S.ds.dt = S.ds.dw = 0; S.tex[1][0] = 0; rp_end();
+ CHECK(mtl_wait(mtl_sub_commit(), 5000000000ull) && !mtl_last_commit_error(),
+ "real depth copy and readback complete under Metal validation");
+ int valid = snap != nil;
+ for (int y = 0; y < 4; y++) for (int x = 0; x < 4; x++)
+ valid &= *(const float *)((const uint8_t *)result.contents + y * 256 + x * 4) == 0.375f;
+ CHECK(valid, "every real depth sample retains the completed attachment clear");
+ mtl_tex_release(&colour); mtl_tex_release(&depth);
+}
int main(int argc, char **argv) { @autoreleasepool {
setenv("DM_NVN_TBDR", "0", 1); setenv("DM_NVN_GPU_TS", "segment", 1);
mtl_init(); NSError *error = nil;
@@ -623,6 +660,7 @@ int main(int argc, char **argv) { @autoreleasepool {
for (int i = 0; i < 3; i++) slot_case(pr, pipeline, slots[i], i);
reuse_case(pr);
depth_snap_case(pr);
+ real_depth_snap_case(pr);
shape_case();
mip_chain_case(pr);
fs->fn = [lib newFunctionWithName:@"mask"]; |
… copy of it: Ocarina of Time's light-blue dashes over the rock at Death Mountain Trail's cave entrance are gone Ported from seedds#1917 (open upstream) onto this branch: its own changes from its merge base with main, conflicts resolved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U35V38c6UYLbzyyPx6Up7P
… a draw that writes depth and samples it reads a copy, one after a depth write of its pass ends the pass first, in depth-only passes too. Ocarina of Time's light-blue dashes over the rock at Death Mountain Trail's cave entrance and Red Dead Redemption's 8x8 sky blocks over the trees are gone (PR #1917, PR #1866) On Apple's GPUs a depth attachment sampled in the pass that writes it reads whole compressed blocks of undefined content, and Metal has no tile read for depth. fb_prepare only ended the pass before a depth write that followed a read, and only in passes with a colour attachment. fb_depth_rule, run before the colour snapshots and tile reads: - A draw that writes depth or stencil and samples the attachment through a texture slot reads a copy (fb_depth_snap, the fb_snap cache), reused until the game's next barrier. Ocarina's full-screen 25a8adc977ff813d and its five decals sample the scene depth while writing it with LEQUAL. - A draw that samples it after an earlier draw of its pass wrote depth or stencil (g_fb_dwrote), and writes none itself, ends the pass first, whether the depth is bound in a slot or named by a uniform-buffer handle. Red Dead Redemption's cloud fog c0ab3c051753e36d reads it through a handle after the billboards and terrain. - It runs in depth-only passes too, counting only reads it can name: pool shaders, dynamic tables and handle tables in c2 (TEXA) stay possible readers in colour passes only, as before, so shadow passes are not cut at every draw. DM_NVN_FB_DEPTH=0 reads the attachment in the pass, as before. A draw that writes depth and reads it through an image slot, a handle, a pool or a dynamic table still reads it in the pass (open, in the reference entry). Unlike the PRs: one rule and one switch; the pass ends before the colour bindings are judged instead of in the middle of the binding scan; depth-only passes are covered; the RED key moved to 163. Checked: check_t_feedback, 251 checks (the copy, its reuse and the barrier, the pass end after a write, and an actual Depth32Float and Depth32Float_Stencil8 attachment copied and read back in a colour and a depth-only pass under Metal validation); the graphics suite (t_nvn passes after the rebase onto 68869cc; t_ubp_running fails as it does on main); test_docs.py, test_env_switches.py; lint_c.py tidy at the baseline. regress.py check on Octopath Traveler II against 1d37720: PASS, both paths taken on its Depth32Float_Stencil8 depth (a copy reused by about 21 draws, about one pass end a frame), no new red line, fps 59.8, frame p99 18.19 -> 18.27 ms, the same screenshots. Fire Emblem Three Houses did not build (stale shader package). Ocarina and Red Dead Redemption were checked by the PR authors, by eye in play; not run on iOS. Co-authored-by: FifteenPen <354289956+FifteenPen@users.noreply.github.com> Co-authored-by: robandstuff <20628004+robandstuff@users.noreply.github.com> Co-authored-by: akratch <70729875+akratch@users.noreply.github.com>
|
Landed in 8190693, folded together with #1866 into one depth rule (fb_depth_rule): a draw that writes depth and samples it reads your copy (fb_depth_snap, unchanged apart from the RED key), and a draw that samples depth an earlier draw of its pass wrote ends the pass first (#1866). The rule now also runs in depth-only passes, so the limit you noted is gone, and one switch, DM_NVN_FB_DEPTH, replaces DM_NVN_FB_DSNAP. akratch's real-depth test is in, extended to Depth32Float_Stencil8 and a depth-only pass. regress on Octopath Traveler II, which takes the copy on its Depth32Float_Stencil8 depth: PASS, no new red line, the same frame times. You are credited as co-author. Thanks for the careful diagnosis and measurements. |
… a draw that writes depth and samples it reads a copy, one after a depth write of its pass ends the pass first, in depth-only passes too. Ocarina of Time's light-blue dashes over the rock at Death Mountain Trail's cave entrance and Red Dead Redemption's 8x8 sky blocks over the trees are gone (PR seedds#1917, PR seedds#1866) On Apple's GPUs a depth attachment sampled in the pass that writes it reads whole compressed blocks of undefined content, and Metal has no tile read for depth. fb_prepare only ended the pass before a depth write that followed a read, and only in passes with a colour attachment. fb_depth_rule, run before the colour snapshots and tile reads: - A draw that writes depth or stencil and samples the attachment through a texture slot reads a copy (fb_depth_snap, the fb_snap cache), reused until the game's next barrier. Ocarina's full-screen 25a8adc977ff813d and its five decals sample the scene depth while writing it with LEQUAL. - A draw that samples it after an earlier draw of its pass wrote depth or stencil (g_fb_dwrote), and writes none itself, ends the pass first, whether the depth is bound in a slot or named by a uniform-buffer handle. Red Dead Redemption's cloud fog c0ab3c051753e36d reads it through a handle after the billboards and terrain. - It runs in depth-only passes too, counting only reads it can name: pool shaders, dynamic tables and handle tables in c2 (TEXA) stay possible readers in colour passes only, as before, so shadow passes are not cut at every draw. DM_NVN_FB_DEPTH=0 reads the attachment in the pass, as before. A draw that writes depth and reads it through an image slot, a handle, a pool or a dynamic table still reads it in the pass (open, in the reference entry). Unlike the PRs: one rule and one switch; the pass ends before the colour bindings are judged instead of in the middle of the binding scan; depth-only passes are covered; the RED key moved to 163. Checked: check_t_feedback, 251 checks (the copy, its reuse and the barrier, the pass end after a write, and an actual Depth32Float and Depth32Float_Stencil8 attachment copied and read back in a colour and a depth-only pass under Metal validation); the graphics suite (t_nvn passes after the rebase onto 29a921b; t_ubp_running fails as it does on main); test_docs.py, test_env_switches.py; lint_c.py tidy at the baseline. regress.py check on Octopath Traveler II against 518fc22: PASS, both paths taken on its Depth32Float_Stencil8 depth (a copy reused by about 21 draws, about one pass end a frame), no new red line, fps 59.8, frame p99 18.19 -> 18.27 ms, the same screenshots. Fire Emblem Three Houses did not build (stale shader package). Ocarina and Red Dead Redemption were checked by the PR authors, by eye in play; not run on iOS. Co-authored-by: FifteenPen <354289956+FifteenPen@users.noreply.github.com> Co-authored-by: robandstuff <20628004+robandstuff@users.noreply.github.com> Co-authored-by: akratch <70729875+akratch@users.noreply.github.com>
…epare after one look; the tile plan looks textures up only for attachments fb_prepare and fb_tile_plan looked up every fragment texture binding of every draw (a lock and a texture each, with a retain and release in the plan). A first pass over the bindings' serials under one lock returns for draws that sample none of the pass's attachments, and the plan takes a texture only for a binding that names one. Mac, BotW, snapshots on: about 12,020 -> 11,930 instructions a draw (11,030 with DM_NVN_FB_SNAP=0). On the depth rule (PR seedds#1917): the look names the colour attachments only, as fb_depth_rule settles the depth attachment before it; it keeps g_fb_dwrote, and a draw the depth rule gave a copy takes the long way. The instruction counts are from before the depth rule and were not measured again.
…ctions compiled out, DMSH_BORDER_FC), seedds#1930 (native float atomic add), seedds#1920 (GPU-written sliver does not own a texture: terrain tiles), seedds#1917 (depth attachment sampled by a depth-writing draw reads a copy), seedds#1928 (enc_thread on), seedds#1942 (idle-GPU late frames do not step the hold up), seedds#1940 (prewarm lines after the first frame), seedds#1936 (resolution families uncompressed) Hand-resolved: image_texture keeps the virtual pools we admit; fb_prepare keeps our pass split and local dread flag after the depth snapshot; the compute key keeps the NDVE direct draw before the border constant. Co-Authored-By: Claude Fable 5.1 <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#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#1917 by FifteenPen (closed): A draw that writes depth and samples its own depth attachment reads a copy of it: Ocarina of Time's light-blue dashes over the rock at Death Mountain Trail's cave entrance are gone - seedds#1920 by nawangwe (closed): A GPU-written span's sliver inside a texture does not make it the GPU's: Ocarina of Time's terrain tiles no longer vanish or show another tile's surface (seedds#1566) - 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#1936 by TheGoldfish11 (closed): Write resolution-family image textures uncompressed - 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
…1909 seedds#1897 seedds#1917 seedds#1920 seedds#1936) and seedds#1902, ahead of merging main 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#1897 by Know-It-All-Brother (closed): HMMA with an f16 accumulator rounds once per 8 products, as Ampere does (chain change) - seedds#1902 by fxd0h (open): HFMA2 rounds once to half, as Ampere does (chain change) - seedds#1909 by oz-rinkliff (open): Bound translated fragment uniform reads to the NVN binding length - seedds#1917 by FifteenPen (closed): A draw that writes depth and samples its own depth attachment reads a copy of it: Ocarina of Time's light-blue dashes over the rock at Death Mountain Trail's cave entrance are gone - seedds#1920 by nawangwe (closed): A GPU-written span's sliver inside a texture does not make it the GPU's: Ocarina of Time's terrain tiles no longer vanish or show another tile's surface (seedds#1566) - seedds#1936 by TheGoldfish11 (closed): Write resolution-family image textures uncompressed
Snapshots: a draw that samples no attachment of its pass leaves fb_prepare after one look; the tile plan looks textures up only for attachments fb_prepare and fb_tile_plan looked up every fragment texture binding of every draw (a lock and a texture each, with a retain and release in the plan). A first pass over the bindings' serials under one lock returns for draws that sample none of the pass's attachments, and the plan takes a texture only for a binding that names one. Mac, BotW, snapshots on: about 12,020 -> 11,930 instructions a draw (11,030 with DM_NVN_FB_SNAP=0). On the depth rule (PR seedds#1917): the look names the colour attachments only, as fb_depth_rule settles the depth attachment before it; it keeps g_fb_dwrote, and a draw the depth rule gave a copy takes the long way. The instruction counts are from before the depth rule and were not measured again.
What and why
In The Legend of Zelda: Ocarina of Time (
0400486024DEC000, v131072), the dark rock above the cave arch at Death Mountain Trail's cave entrance shows short screen-aligned light-blue dashes, the sky behind it, changing from frame to frame and staying on the same surfaces as the camera moves. A player also reported blocks on Death Mountain Trail's cliffs on an iPhone.Per-pass snapshots (
DM_NVN_TEXDUMPwithDM_NVN_TEXSNAP, taken on a tree where the marks were larger blocks) show the rock drawn whole, then a later full-screen pass painting the marks over it; a pass dump of main's build shows the same draws in the same pass with the same bindings. That pass,25a8adc977ff813d, samples the scene's depth attachment (1536x864 Depth32Float) through a texture slot and writes the same attachment (depth test LEQUAL, depth writes on); five decal draws after it do the same. On the console each fragment reads the depth stored before the draw. On Apple's GPUs a depth attachment sampled by the pass that renders into it reads whole compressed blocks of undefined content, so the shader's comparison against the sample fails or passes block by block.fb_preparemarks the depth attachment as read for such a binding and ends the pass before the next draw that writes depth (the depth half of the snapshot rule), which cannot help a single draw that does both. It is the case PR #1430's assumption leaves out (Pool shaders taken as depth readers): a shader that reads the depth it writes.The change,
fb_depth_snapinnvn_mtl.m, called byfb_prepare, for every game:fb_snap, the colour snapshots' cache), and the draw's binding reads the copy.fb_preparereturns early without a colour attachment, as for the colour rule; noted in the entry).DM_NVN_FB_DSNAP=0reads the attachment directly, as before. The first copy logssame-pass depth sampling: copy #1.#1866 is complementary: it ends the pass before a draw that samples depth an earlier draw of the pass wrote, and leaves alone a draw that writes depth itself. Both touch the same line in
fb_prepare, so whichever lands second needs a one-line rebase.Docs: a new reference entry, Depth sampled by the draw that writes it, the switch's row in
docs/env_switches.tsv, and a line under the game note's "Found on this game".Checked
make -C dm/gfx/tests GAME=0400486024DEC000 check_t_feedback: 218 checks, 0 failed. The newdepth_snap_casecovers a copy for a draw that writes depth and samples it (pass ended), reuse by the next such draw without ending the pass, a fresh copy after a game barrier, a direct read by a draw that leaves depth alone, the switch off, and the copy's content. In one earlier run the existing timestamp check "two reports in one command buffer read two times" failed; it is flaky on unchanged main too (1 failure in 4 runs of d8b2b64 here; A draw that samples the depth attachment its render pass already wrote ends the pass first (Red Dead Redemption's sky blocks) #1866 reports 2 in 6), and the next four runs passed.make -C dm/gfx/tests GAME=0400486024DEC000 run: every test passes.python3 -m unittest dm/tools/tests/test_docs.py dm/tools/tests/test_env_switches.py,python3 dm/tools/docs_symptoms.py.dm/tools/lint_c.py tidy(LLVM 23.1.4) on the changed files: 0 file/check pairs above the baseline.0400486024DEC000, v131072), a player's save at Death Mountain Trail's cave entrance.In play, on main d8b2b64's Mac app (docked, in a window): the light-blue dashes over the rock above the arch. With this change's host in the same app: gone. That app also carried 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 #1901 and 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) #1903, which do not touch this path. Headless captures of the spot at 1440x810 and 1536x864 did not show the dashes, so this check is by eye.
Cost, this change's host on a
--platform macbuild of main d8b2b64,scripts/bench.pyheadless, docked, 45 s standing at the cave entrance, every pipeline from a warmed archive, two runs a side, same binary:DM_NVN_FB_DSNAP=01,000 copies and 3,611–4,696 reuses by frame 3,600–3,800: no measurable cost.
regress.py impact: every NVN game, throughnvn_mtl.m(Breath of the Wild, Pokémon Scarlet and Tears of the Kingdom have usage manifests). The rule acts only on a draw that writes depth and samples its own depth attachment through a texture slot, and logs its first copy.regress.py checkwas not run: of this Mac's NVN games only Mario Kart World has a smoke route, and none has a build for the current chain.Docs
docs/reference/<subsystem>.mdentry, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index)