Repository navigation
Conversation
…its outputs are drawn to: bz_word (0662f9d, PR seedds#2030) maps physical attachment i to its logical output through g_pso_lmap, but draw_ sets g_pso_lmap to NULL before it encodes the fragment arguments (encode_args, which calls bz_bind), so in the later pass of a readable-tile merge (DM_NVN_TBDR=1, off by default), when fz_p2_draw moves an output to another attachment, its bits were taken with the format of the attachment that has the output's number, not the one it is drawn to; the deferred pass merge and same-pass tile reads map each output to itself, so no pixel of theirs changes. A later-pass draw whose output 0 blends SRC_ALPHA / ONE_MINUS_SRC_ALPHA into an RGBA16F attachment 1, behind the earlier pass's RGBA8 attachment 0, got the word 0, so a NaN colour at alpha 0 was stored there as Apple's blender stores it. Found reading the code, not seen in a game. draw_ now copies the eight mapping bytes beside raster_mask, before the reset (raster_mask is taken there for the same reason), and hands them to bz_bind through g_bz_lmap for the fragment encode_args call only; bz_word takes the mapping as an argument, and the word bz_bind bound last is kept in g_bz_last for the test. The host alone: no shader or chain change, and DM_NVN_BLEND_ZERO=0 still gives 0 for every draw. t_blend_zero has a new case drawn through draw() with g_pso_lmap set, as defer_replay passes a mapping in: physical attachment 0 RGBA8 with no output, physical 1 RGBA16F holding output 0; the bound word is 1 with the rule (0 with DM_NVN_BLEND_ZERO=0), the mapping is gone after the draw, and the same draw unmapped binds 0. Built with bz_bind reading g_pso_lmap as before, the mapped draw binds 0 and that check fails. make check_t_blend_zero: 165 checks, 0 failed, with the rule and with DM_NVN_BLEND_ZERO=0 (main: 160 and 160). Also checked: test_dmsh_blend_zero.py passes (its compile test for macOS and for iOS 18 too), t_alpha, t_logic, t_defer_chain, t_fuse_check, t_pso_warm and t_image_attachment_store pass as on main, and so does t_feedback but for its check "with nothing running: a commit", which fails in most runs on main too (13 of 15 runs here, 12 of 15 on main), test_docs passes, the Mac host builds with the same 58 warnings as main (M3 Max, macOS 26.7.2, Xcode 27.2, metal 32023.922). Not run: a game, regress.py (the readable-tile merge is off with default settings), lint_c.py (no clang-tidy here), iOS
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
The blend weight word that 0662f9d added (PR #2030, issue #1839) is computed in
bz_wordfor each physical attachment, throughg_pso_lmap, the merged pass's physical → logical mapping. Butdraw_clearsg_pso_lmapbefore it encodes the fragment arguments, andbz_bindruns inside thatencode_argscall. So the mapping is always NULL at the point where the word is made. On main (8fb5ffe):dm/gfx/nvn/nvn_mtl.m:7998:bz_wordreadsg_pso_lmapnvn_mtl.m:8011:bz_bindcallsbz_word();encode_args_callsbz_bindat:10921nvn_mtl.m:13910:draw_setsg_pso_lmap = NULL, right after it takesraster_mask(which is taken there for this same reason)nvn_mtl.m:13964: the fragmentencode_argscall, after that resetOnly one kind of draw sends an output to an attachment with another number: the later pass of a readable-tile merge, when
fz_p2_drawmoves an output (lmap[p2map[c]] = cat:12677, drawn through its_bffunction). There each output's bits were worked out from the format of the attachment with the output's number, not the one the output is drawn to. Example: the later pass's output 0 blends SRC_ALPHA / ONE_MINUS_SRC_ALPHA into an RGBA16F attachment 1, behind the earlier pass's RGBA8 attachment 0. That draw got the word 0, so a NaN at alpha 0 is stored there the way Apple's blender stores it. Stopping that is the whole point of the rule.The readable-tile merge runs only with
DM_NVN_TBDR=1, which is off by default (tbdr_on,:4896;fuse_tryis called only under it,:13872). So with default settings no pixel changes. The deferred pass merge keeps every output at the attachment with its own number (defer_q_superset,:12182). Same-pass tile reads (fb_tile_plan,:12970) map each output to itself. So their draws only lose bits for outputs the shader does not write. I found this by reading the code and have not seen it in a game.Fix:
draw_copies the 8 mapping bytes next toraster_mask, before the reset. It hands them tobz_bindthroughg_bz_lmapfor the fragmentencode_argscall only, and setsg_bz_lmapback to NULL right after.bz_wordnow takes the mapping as an argument, andg_bz_lastkeeps the last word bound, for the test. This changes the host only: no shader change and no chain change.DM_NVN_BLEND_ZERO=0still gives 0 for every draw. The reference entry ("A NaN colour under a zero blend weight",docs/reference/shaders.md) now says that in the later pass of a readable-tile merge an output's target is the attachment its mapping gives it.Checked
GAME=0400486024DEC000.t_blend_zerohas a new case. It draws throughdraw()withg_pso_lmapset before the call (asdefer_replaypasses a mapping in), using the kind of mappingfz_p2_drawmakes when it moves an output. Physical attachment 0 is RGBA8 with no output; physical attachment 1 is RGBA16F and holds output 0. Results:DM_NVN_BLEND_ZERO=0it is 0.bz_bindreadingg_pso_lmapas main does, the mapped draw binds 0 and that check fails (1 failure with the rule, 0 withDM_NVN_BLEND_ZERO=0).make check_t_blend_zero, as it is:DM_NVN_BLEND_ZERO=0.MTL_DEBUG_LAYER=1).test_dmsh_blend_zero.py: all 7 tests pass, on main and here,test_compilesincluded (macOS and the iOS 18 target). This PR does not change the file.check_t_alpha: 788 checks, 0 failedcheck_t_logic: 929 checks, 0 failedt_defer_chain: 59 checks, 0 failedt_fuse_check: 10 checks, 0 failedt_pso_warm: 123 checks, 0 failed, with both encoderst_image_attachment_store: 120 checks, 0 failedcheck_t_feedback: 278 checks. Its checkwith nothing running: a commitfails in most runs on main too: 13 of 15 runs on this branch, 12 of 15 on main. Every other check passes.dm/tools/tests/test_docs.pypasses.docs_symptoms.pyleaves the index unchanged.dm/hostmake's compile line and has the same 58 warning lines as main.scripts/regress.py check: not run. The only draws whose written outputs get a different word are the readable-tile merge's later-pass draws with moved outputs, which needDM_NVN_TBDR=1, so a report with default settings would show nothing.make -C dm/gfx/tests run,lint_c.py(no clang-tidy on this Mac), an iOS build or run of the host.Overlaps: no other open PR changes these lines. I dry-ran 258 of the open PRs that touch
nvn_mtl.morshaders.md, on main and on this branch. None applies on main and fails here. One fails on main already and gets one more failing hunk here: #970 (HDR output) adds a line right after the fragmentencode_argscall this PR changes, so that hunk's context needs updating. #2092 (same-pass feedback) also passes a mapping (g_fb_tlmap) intodraw_; its diff applies on top of this one as it does on main. Its mapping sends each output to its own attachment, so its draws bind the same word with or without this change. #2318 adds its own entry at the end ofshaders.md, and that hunk applies on top of this one.Docs
docs/reference/<subsystem>.mdentry (shaders.md, "A NaN colour under a zero blend weight": Fix and Check), andpython3 dm/tools/docs_symptoms.pywas run (no change to the index)