Skip to content

nvn/feedback: a draw that opens its pass reads the tile too - #2092

Closed
Xipzer wants to merge 1 commit into
seedds:mainfrom
Xipzer:fb-tile-early
Closed

Xipzer wants to merge 1 commit into
seedds:mainfrom
Xipzer:fb-tile-early

Conversation

@Xipzer

@Xipzer Xipzer commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

A draw that samples an attachment it writes now reads the tile when it opens its render pass too, not only when it meets an open one.

Cause. fb_tile_plan needed an open pass, since the tile-reading pipeline is named by the pass's formats. A draw that met no pass took a snapshot: a full-size copy, where the pass it opens loads the same content into the tile. Breath of the Wild opens about 3.75 passes a frame with such a draw (shaders 753d0169ae3ce4f0 and 84862343ae82c6fa read G-buffer targets they write).

Change. The formats are learned:

  • The first draw of a program on a set of targets that opens its pass and samples an attachment it writes takes its snapshot, and fb_early_after notes the pass rp_begin_ex opened for it (64 entries, keyed by the two shaders and the targets).
  • Later ones plan the tile read with those formats while no pass is open, and are checked once the pass opens.
  • A pass that differs (formats, size, slice-merged) is ended, the draw goes through fb_prepare again for its snapshot, and the pass reopens. Three such passes and the entry plans no more.
  • A clear still waiting becomes the opened pass's load action, so the tile holds the cleared content, as the snapshot does since b54968e.
  • DM_NVN_FB_TILE_EARLY=0 turns it off.

Fitted to main. The change was written on a fork over an older main (413e880), where fb_prepare returned early for a draw that samples no attachment of its pass (PR #1469, still open) and had no depth rule. Two differences here:

Measured on that fork, Breath of the Wild:

  • M4 Max, 190% render scale, uncapped: snapshots 30,000 to 4 over 11,000 frames; GPU p50 5.99 to 5.56 ms and 6.9 to 5.9 ms in two alternated pairs; pictures the same.
  • iPad Pro 13" M4, 120% render scale, target 120 fps: snapshots 150 a second to 0; GPU p50 10.88 to 10.77 ms and 11.62 to 10.90 ms; 75.9 to 75.6 fps and 70.7 to 74.7 fps in two alternated pairs (the iPad's runs drift with its temperature).

Checked on this branch (dbf8499 plus the commit), Apple M4 Max:

  • make -C dm/gfx/tests GAME=01007EF00011E000 check_t_feedback: 293 checks under Metal validation, 0 failed. New cases: a draw that writes no attachment it samples learns nothing; the first pass-opening draw takes its snapshot and the pass is noted; the next plans with no pass open and gets the pipeline built for the open-pass case; the GPU reads green and red that the opened pass loaded; a pass other than the planned one is refused and counted; the draw then takes a snapshot; no plan after three.
  • make -C dm/host GAME=01007EF00011E000: builds, no errors.
  • dm/ios/tools/compile_check.py: 262 files, 0 failed.
  • test_env_switches, test_docs, test_lint_c, test_architecture: 31 tests, OK.

Not shown. No game was run on this branch, and scripts/regress.py check was not run (see the comment below). The numbers are from the fork, whose fb_prepare differs as described above. No reference entry is added to docs/reference/rendering.md.

@Xipzer

Xipzer commented Nov 10, 2026

Copy link
Copy Markdown
Contributor Author

Note: I could not run scripts/regress.py check for this one. This Mac has Xcode 27.1 (Metal 32023.921), and the chain lock on main names 27.2's 32023.922, so the local games' shaders do not build against main's chain here. Measured on a fork that carries the same change over an older main (numbers in the description).

@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation labels Nov 10, 2026
A draw that samples an attachment it writes reads the tile when it opens its pass too, not only in an open one: Breath of the Wild opens about 3.75 passes a frame with such a draw, each a full-size snapshot before (30,000 snapshots to 4 over 11,000 frames on an M4 Max)

fb_tile_plan needed an open pass, since the tile-reading pipeline is named by the pass's formats, so a draw that met no pass took a snapshot: a full-size copy, where the pass it opens loads the same content into the tile. Breath of the Wild's 753d0169ae3ce4f0 and 84862343ae82c6fa read G-buffer targets they write and open their pass.

The formats are now learned. The first draw of a program on a set of targets that opens its pass and samples an attachment it writes takes its snapshot, and fb_early_after notes the pass rp_begin_ex opened for it (64 entries, keyed by the two shaders and the targets). Later ones plan the tile read with those formats while no pass is open and are checked once the pass opens: a pass that differs (formats, size, slice-merged) is ended, the draw takes its snapshot through fb_prepare again and the pass reopens; three such passes and the entry plans no more. A clear still waiting becomes the pass's load action, so the tile holds the cleared content, as the snapshot does since b54968e. Where fb_depth_rule ends the pass before the plan, the draw counts as opening its pass. DM_NVN_FB_TILE_EARLY=0 turns it off.

Measured on a fork that carries the change over an older main (413e880 plus the fork's commits), Breath of the Wild. M4 Max, 190% render scale, uncapped: snapshots 30,000 to 4 over 11,000 frames, GPU p50 5.99 to 5.56 ms and 6.9 to 5.9 ms in two alternated pairs, pictures the same. iPad Pro 13" M4, 120% render scale, target 120 fps: snapshots 150 a second to 0; GPU p50 10.88 to 10.77 ms and 11.62 to 10.90 ms, 75.9 to 75.6 fps and 70.7 to 74.7 fps in two alternated pairs (the iPad's runs drift with its temperature). That fork's fb_prepare returned early for a draw that samples no attachment of its pass (PR seedds#1469) and had no depth rule; here the plan itself notes only draws that sample an attachment they write.

Checked: check_t_feedback, 293 checks under Metal validation, 0 failed, with new cases (nothing learned by a draw that writes no attachment it samples, the first pass-opening draw's snapshot and the noted pass, the next one's plan with no pass open and its pipeline, the GPU reading green and red that the opened pass loaded, a refused pass, the snapshot after it, no plan after three). The host builds for 01007EF00011E000, the iOS compile check has 0 of 262 failed, test_env_switches, test_docs, test_lint_c and test_architecture pass. Not run on this main: any game (regress.py check).
@Xipzer Xipzer changed the title Same-pass feedback: a draw that opens its pass reads the tile too nvn/feedback: a draw that opens its pass reads the tile too Nov 13, 2026
@Xipzer

Xipzer commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main (2e6edda) without conflicts, retitled to the area/component form.

Checked on this commit (M4 Max): host build, make -C dm/gfx/tests run (every target passes), the settings / docs / switch-table tool tests. The host tests pass except audio-device checks (t_audio's device stall here, t_audio_device on clean main in the same session).

@Xipzer

Xipzer commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

Now that this Mac has the chain lock's compiler (Xcode 27.2), scripts/regress.py check on Breath of the Wild 1.9.0, main (2e6edda) against this branch, replacing my earlier note that I could not run it:

regress.py check (full): PASS

Baseline 2e6edda4d334feaf48326407c1718cb96c43e2de 2e6edda, candidate 501f12a8135a3566c97b87004a89b7b97e2e59a0 501f12a; Apple M4 Max, macOS 27.2.1, Xcode 27.2; 328.5 s

game status shaders (base → cand) newly failing newly building changed MSL interface (new / known) boot fps p50 frame p99 ms peak MB prepare s new RED
BOTW 01007EF00011E000 PASS 19592/19592 → 19592/19592 0 0 0 0 / 0 ok 59.7 → 59.8 18.13 → 17.25 3236 → 3239 0.21 → 0.06 0

BOTW 01007EF00011E000: PASS

  • screenshots (s: dHash bits / RGB diff / baseline noise): 60: 12/3.35/0, 80: 8/3.41/0
  • baseline: exit None, first frame 2.4 s, max frame 5928, reached True, prepare 0.21 s, prebuild 203.7 s (15767 built, 0 failed), fps 59.7, p50/p99 16.67/18.13 ms, peak 3236 MB
  • candidate: exit None, first frame 1.8 s, max frame 6266, reached True, prepare 0.06 s, prebuild 169.7 s (15767 built, 0 failed), fps 59.8, p50/p99 16.67/17.25 ms, peak 3239 MB
  • time: chain 47.9 s, smoke 280.4 s

The route's save is my own outdoors save, not the one smoke.tsv was written with. The early tile read is on by default, so the route runs it.

seedds added a commit that referenced this pull request Nov 14, 2026
A draw that samples an attachment it writes now reads the tile when it opens its render pass too, not only when it meets an open one. fb_tile_plan needed an open pass, since the tile-reading pipeline is named by the pass's formats, so such a draw took a full-size snapshot where the pass it opens loads the same content into the tile. Breath of the Wild opens about 3.75 passes a frame with one (753d0169ae3ce4f0, 84862343ae82c6fa).

The formats are learned: the first such draw of a program on a set of targets takes its snapshot and fb_early_after notes the pass rp_begin_ex opened for it (64 entries); later ones plan with those formats and are checked once the pass opens. A pass that differs (formats, size, slice-merged) is ended, the draw takes its snapshot and the pass reopens; after three the entry plans no more. A waiting clear becomes the opened pass's load action. DM_NVN_FB_TILE_EARLY=0 turns it off.

Measured: Breath of the Wild, regress route on an M4 Pro: 4.0 -> 0 snapshots a frame, 293 -> 295 tile reads, no pass refused; regress.py check PASS (19,592 shaders, no MSL change, no new RED). The author's M4 Max: 30,000 -> 4 snapshots over 11,000 frames, GPU p50 5.99 -> 5.56 and 6.9 -> 5.9 ms. check_t_feedback: 336 checks, 0 failed. Docs: rendering.md's tile-read entry, the game's note.

Co-authored-by: Xipz <60103178+Xipzer@users.noreply.github.com>
@seedds

seedds commented Nov 14, 2026 •

Copy link
Copy Markdown
Owner

Landed on main as 854cdc0, your code unchanged and credited as co-author. Thank you for this one.

Added on top: the pass-opening case in docs/reference/rendering.md ("Same-pass feedback read from the tile": Fix, Check, Note) and a sentence in Breath of the Wild's note.

Checked on current main (Apple M4 Pro): check_t_feedback 336 checks, 0 failed; regress.py check on Breath of the Wild 1.9.0's outdoors route PASS (19,592/19,592 shaders, no MSL change, no new RED, screenshots 2–4 dHash bits). Snapshots went from 4.0 to 0 a frame and tile reads from 293 to 295 (frames 5,400–6,300). No pass was refused. GPU time was not measured here. Your M4 Max and iPad numbers are in the reference entry.

@seedds seedds closed this Nov 14, 2026
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)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants