Repository navigation
Passes ended for same-pass reads are counted by cause, and a pool shader can count as a depth reader only when its tables can hold the depth storage (DM_NVN_FB_DPOOL=1, opt-in) - #1396
Closed
PucaVaz wants to merge 1 commit into
Conversation
…der can count as a depth reader only when its tables can hold the depth storage (DM_NVN_FB_DPOOL=1, opt-in) In The Legend of Zelda: Ocarina of Time, docked in Link's house, fb_prepare ends about 72 of 175 render passes a frame (seedds#1385). The periodic log now counts these passes by cause. All of them are depth splits after a fragment shader with texture pool bindings: colour 0, depth named by a slot or handle 0, depth after a pool shader 71.7 a frame. fb_prepare takes every shader with TEXP or TEXD bindings as a depth reader, because which texture a pool handle names is known only on the GPU. The next depth write then ends the pass. Ocarina's material shader dbda7678a92ef6e5 reads a texture2d<float> pool and writes depth, in about 75 draws a frame. A pool shader reads through a table of every ID registered from the bound base up (pool_gpu). So it can reach the depth attachment only through an ID whose storage lies over the attachment's. With DM_NVN_FB_DPOOL=1, such a shader counts as a depth reader only when an ID does, or when an ID's storage isn't known (a sparse pool, no address, or a virtual pool mapped since). Storage is compared in guest memory, so aliases and views count. No ID is resolved to a texture, and the walk's answer is kept per registration generation. 0, the default, keeps the old rule; ab alternates with DM_AB. The rule is off by default because it ends no fewer passes in Ocarina. The game registers the depth texture itself in the pool it reads (a view of the 1920x1080 Depth32Float, ID 12413 in one run). About 8,000 of its 16,600 IDs also lie over the depth storage. A throwaway build that never took a pool shader as a depth reader (unsound, not part of this change) ran Link's house at a frame time p50 of 33.7 -> 27.4 ms, with 175 -> 103 passes a frame. So these splits cost about 6 ms a frame there. Giving such draws a snapshot of the depth attachment in place of the table entries that hold it would take about 4 copies a frame. That approach is left open in the reference entry. Measured on a MacBook Air M4 (24 GB, on the charger). The host was built from lunks's fix/gmem-pool-table with this change; docked, standing in Link's house, with DM_AB=600 DM_NVN_FB_DPOOL=ab: - Run 1, 16 phases each: frame time p50 34.26 / 34.26 ms, GPU span p50 35.43 / 35.12 ms. Both had 175.0 passes and 444 / 460 MB of attachment loads / stores a frame. - Run 2, throttled (its title screen also ran slower): frame time p50 49.08 / 48.95 ms. Screenshots of frames 4800, 5400 and 6000 with the rule off and on differ only where the scene animates, by as much as two frames of one run do. Checked: make -C dm/gfx/tests GAME=010015100B514000 check_t_fb_dpool (new, 49 checks) and the full make -C dm/gfx/tests GAME=010015100B514000 run pass. test_docs and test_env_switches pass. clang-tidy finds nothing above the baseline in the changed files, and nvn_mtl.m keeps its 56 warnings with the host's flags. _Drafted with Claude; reviewed by me._
2 tasks done
3 tasks done
fpedrolucas95
pushed a commit
to fpedrolucas95/nxnative
that referenced
this pull request
Nov 6, 2026
…0 passes a frame stay open in Ocarina of Time fb_prepare took every draw with pool handles or dynamic tables as a depth reader, because only the GPU knows what the handle names, and the next draw writing depth ended the pass. Ocarina of Time's material shaders are pool shaders that write depth, so each draw ended the pass the one before it opened. PucaVaz found the rule and the cost in Link's house (issue seedds#1385, PR seedds#1396), and that asking the pool tables doesn't help: the game registers its depth textures in the pool those shaders read. A draw that writes depth itself is no longer taken as a reader: sampling the attachment it writes would be a feedback loop, undefined on the console too. In Kokiri Forest the reader wrote depth in 67,730 of 67,739 splits. Pool draws that leave depth alone still count, as do real slots and TEXU handles. This is an inference from the API, not a proof about any shader. DM_NVN_FB_DSELF=0 counts them as before; ab alternates with DM_AB. On the author's stack (M5 Pro, 1280x720, game upscaler, encoding thread off, A/B over 6 paired phases): 183 to 112 passes a frame, attachments 286 / 316 to 244 / 273 MB, GPU p50 20.3 to 16.5 ms, frame p50 20.7 to 16.8 ms. Not run on this commit, on main, or on another game. Check: check_t_feedback, with a new case: 176 checks, 0 failed; with the change taken out, 3 of them fail. test_docs, test_env_switches, test_gfx_tests_makefile, docs_symptoms.py --check; lint_c.py tidy on nvn_mtl.m and t_feedback.m: 0 file/check pairs above the baseline.
fpedrolucas95
pushed a commit
to fpedrolucas95/nxnative
that referenced
this pull request
Nov 6, 2026
…0 passes a frame stay open in Ocarina of Time fb_prepare took every draw with pool handles or dynamic tables as a depth reader, because only the GPU knows what the handle names, and the next draw writing depth ended the pass. Ocarina of Time's material shaders are pool shaders that write depth, so each draw ended the pass the one before it opened. PucaVaz found the rule and the cost in Link's house (issue seedds#1385, PR seedds#1396), and that asking the pool tables doesn't help: the game registers its depth textures in the pool those shaders read. A draw that writes depth itself is no longer taken as a reader: sampling the attachment it writes would be a feedback loop, undefined on the console too. In Kokiri Forest the reader wrote depth in 67,730 of 67,739 splits. Pool draws that leave depth alone still count, as do real slots and TEXU handles. This is an inference from the API, not a proof about any shader. DM_NVN_FB_DSELF=0 counts them as before; ab alternates with DM_AB. On the author's stack (M5 Pro, 1280x720, game upscaler, encoding thread off, A/B over 6 paired phases): 183 to 112 passes a frame, attachments 286 / 316 to 244 / 273 MB, GPU p50 20.3 to 16.5 ms, frame p50 20.7 to 16.8 ms. Not run on this commit, on main, or on another game. Check: check_t_feedback, with a new case: 176 checks, 0 failed; with the change taken out, 3 of them fail. test_docs, test_env_switches, test_gfx_tests_makefile, docs_symptoms.py --check; lint_c.py tidy on nvn_mtl.m and t_feedback.m: 0 file/check pairs above the baseline.
Owner
|
Thank you for the counts and A/B measurements: they showed that what a pool could hold doesn't narrow the rule here. Main now uses the shaders' read tracking instead (pool_ds_read, 6121fd7), which took the walk from 182 to about 101 passes a frame, so I'm closing this. Final checks on the rebased tree (full shader build, walks, regression) are still running, so please report anything you see. |
cheemu21
pushed a commit
to cheemu21/kid-with-sword-remake
that referenced
this pull request
Nov 6, 2026
…0 passes a frame stay open in Ocarina of Time fb_prepare took every draw with pool handles or dynamic tables as a depth reader, because only the GPU knows what the handle names, and the next draw writing depth ended the pass. Ocarina of Time's material shaders are pool shaders that write depth, so each draw ended the pass the one before it opened. PucaVaz found the rule and the cost in Link's house (issue seedds#1385, PR seedds#1396), and that asking the pool tables doesn't help: the game registers its depth textures in the pool those shaders read. A draw that writes depth itself is no longer taken as a reader: sampling the attachment it writes would be a feedback loop, undefined on the console too. In Kokiri Forest the reader wrote depth in 67,730 of 67,739 splits. Pool draws that leave depth alone still count, as do real slots and TEXU handles. This is an inference from the API, not a proof about any shader. DM_NVN_FB_DSELF=0 counts them as before; ab alternates with DM_AB. On the author's stack (M5 Pro, 1280x720, game upscaler, encoding thread off, A/B over 6 paired phases): 183 to 112 passes a frame, attachments 286 / 316 to 244 / 273 MB, GPU p50 20.3 to 16.5 ms, frame p50 20.7 to 16.8 ms. Not run on this commit, on main, or on another game. Check: check_t_feedback, with a new case: 176 checks, 0 failed; with the change taken out, 3 of them fail. test_docs, test_env_switches, test_gfx_tests_makefile, docs_symptoms.py --check; lint_c.py tidy on nvn_mtl.m and t_feedback.m: 0 file/check pairs above the baseline. (cherry picked from commit 5cb7d71)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Follows #1385. In The Legend of Zelda: Ocarina of Time, Link's house, docked,
fb_prepareends about 72 of 175 render passes a frame. This PR finds out which rule ends them, and tries the narrowing the issue proposed. The narrowing is sound but ends no fewer passes in this game, so it's opt-in and this PR is a draft.What the counts show. The periodic log has a new line that counts these passes by cause. In Link's house, all of them are depth splits after a fragment shader with texture pool bindings: colour 0, depth named by a slot or handle 0, depth after a pool shader 71.7 a frame. They follow pool-reading material shaders that write depth themselves (
5490496d71e49df2,8c9918f14dc7b061,f906b8026cdd7c4band others, into a 128×128 Depth32Float target); 222 of 227 splits in a 400-pass window were of that kind. Correction: an earlier version of this text nameddbda7678a92ef6e5, which does not draw in Link's house in later runs; @deanpektas27 caught the same on #1385. In #1385, @bettterlabs counted about 77 a frame with the existingsame-pass feedback samplingline: 76,858 over frames 1,241–2,240. That was on a Mac mini M4, handheld, in Link's bedroom, on an older base without lunks's table fix. That line doesn't split them by cause.The change.
DM_NVN_FB_DPOOL=1takes a pool shader as a depth reader only when an ID from the bound pool base up lies over the depth attachment's storage in guest memory, or has storage that isn't known (a sparse pool, no address, a virtual pool mapped since).pool_gpu's tables hold nothing else, so the rule is sound. It compares storage, not serials, so aliases and views count. No ID is resolved to a texture; the walk runs once per registration generation and is cached.0, the default, keeps today's rule;abalternates withDM_AB.passes ended before a write to an attachment an earlier draw readcounts passes ended by cause (colour; depth named by a slot or handle; depth only a pool or dynamic shader could read) and pool draws found able or unable to reach the depth attachment.Why it's off by default. In Ocarina of Time every pool draw with a depth attachment has a table that can hold it:
I also tried a stricter test that compares Metal memory instead (the attachment's texture, views of it, shared buffers or heaps). It found the depth texture's own view in every table too, so it isn't in this PR.
What the splits cost, and what could get it. These measurements come from a throwaway build, not this PR.
DM_AB=600.Reference entry: Pool shaders taken as depth readers. Related: #1068 narrowed the same rule for TEXU handles (
fb_texu_reads, 185b7db). #901 gives slot reads of the depth attachment a copy. #1340 (bindless readers in the TBDR store lookahead) and #1367 (deferred uniform updates in TBDR mode) are other steps toward fewer passes (#159).Checked
Benchmark. The Legend of Zelda: Ocarina of Time (
0400486024DEC000, v131072), docked (DM_OPMODE=1,DM_SAVED_CHOICES=0, windowed).fix/gmem-pool-table(574a38f) plus this commit, because the profile is in [WIP] The Legend of Zelda: Ocarina of Time (Switch 2, 0400486024DEC000, v0, SDK 22.2.8) runs on the Mac through the title movie into Kokiri Forest #1299. The app is a clone of that build with onlydmhostswapped.DM_AB=600 DM_NVN_FB_DPOOL=ab. The first 150 frames of each phase and the loading phases are left out.@frame_msfromDM_NVN_TSV. GPU span is@gpu_end_ms − @gpu_beg_msper frame (first command buffer start to last end), not the host's summed command-buffer time, which adds up overlapping intervals.DM_NVN_GPUPROF=every:600gave the encoder span and idle time.dm/tools/gpuprof.py --ab(summed command-buffer time; its base includes the loading phases): FB_DPOOL − base GPU p50 −0.44 ms and frame p50 +0.12 ms in run 1; −0.75 and +0.54 in run 2.pmset -g thermrecorded no warning. Its profiled frames also show 237 render passes against 192 in run 1, with the same count in the TSV; I didn't find out why.Correctness.
DM_NVN_SHOTat frames 4800, 5400 and 6000, in two runs withDM_NVN_FB_DPOOL=0and=1: same save, the same menu presses at the same frames, camera still.=1run the new log line shows every pool draw found able to reach the depth attachment (could not: 0.0), so the rule took the same decisions as=0.Tests:
make -C dm/gfx/tests GAME=010015100B514000 check_t_fb_dpool: 49 checks, 0 failed (new test). The fullmake -C dm/gfx/tests GAME=010015100B514000 runpasses.python3 -m unittest dm.tools.tests.test_docs dm.tools.tests.test_env_switches: OK.docs_symptoms.pywas run.Lint and build:
dm/tools/lint_c.py tidyon the three changed C and Objective-C files finds 0 file/check pairs above the baseline, and 0 findings in the new test. (This machine has LLVM 22.1.1; the baseline was made with 23.1.4.)nvn_mtl.mwith the host's flags gives 56 warnings before and after;hle_nvn.cgives 0.Device and OS: MacBook Air M4 (fanless, 24 GB), macOS 27.2, Xcode 27.2, on the charger the whole time (
pmset -g batt).pmset -g thermrecorded no thermal or performance warning, and the host's thermal field reads -1 on the Mac.Not checked:
scripts/regress.py, because only Ocarina of Time is built here. The default path changes only by the counters, butDM_NVN_FB_DPOOL=1could change the passes in any game with pool shaders, such as Mario Party Superstars. If you want, runDM_NVN_FB_DPOOL=1on the TBDR-sensitive games. A table with no ID over the depth storage costs a walk of every registered ID once per registration generation. That could matter in a game that registers on every bind (KTGL, Hyrule Warriors: Age of Calamity).Docs
docs/reference/rendering.mdentry, the switch is indocs/env_switches.tsv, andpython3 dm/tools/docs_symptoms.pywas run (the symptom index)Drafted with Claude; reviewed by me.
🤖 Generated with Claude Code