Skip to content

Blend-zero word follows a merged pass's output mapping - #2329

Open
dean0396 wants to merge 1 commit into
seedds:mainfrom
dean0396:pr-bzmap
Open

dean0396 wants to merge 1 commit into
seedds:mainfrom
dean0396:pr-bzmap

Conversation

@dean0396

Copy link
Copy Markdown
Contributor

What and why

The blend weight word that 0662f9d added (PR #2030, issue #1839) is computed in bz_word for each physical attachment, through g_pso_lmap, the merged pass's physical → logical mapping. But draw_ clears g_pso_lmap before it encodes the fragment arguments, and bz_bind runs inside that encode_args call. 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_word reads g_pso_lmap
  • nvn_mtl.m:8011: bz_bind calls bz_word(); encode_args_ calls bz_bind at :10921
  • nvn_mtl.m:13910: draw_ sets g_pso_lmap = NULL, right after it takes raster_mask (which is taken there for this same reason)
  • nvn_mtl.m:13964: the fragment encode_args call, after that reset

Only one kind of draw sends an output to an attachment with another number: the later pass of a readable-tile merge, when fz_p2_draw moves an output (lmap[p2map[c]] = c at :12677, drawn through its _bf function). 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_try is 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 to raster_mask, before the reset. It hands them to bz_bind through g_bz_lmap for the fragment encode_args call only, and sets g_bz_lmap back to NULL right after. bz_word now takes the mapping as an argument, and g_bz_last keeps the last word bound, for the test. This changes the host only: no shader change and no chain change. DM_NVN_BLEND_ZERO=0 still 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

  • Devices and OS: M3 Max, macOS 26.7.2, Xcode 27.2, Apple metal 32023.922. All tests used GAME=0400486024DEC000.
  • Tests:
    • t_blend_zero has a new case. It draws through draw() with g_pso_lmap set before the call (as defer_replay passes a mapping in), using the kind of mapping fz_p2_draw makes when it moves an output. Physical attachment 0 is RGBA8 with no output; physical attachment 1 is RGBA16F and holds output 0. Results:
      • With the rule, the bound word is 1; with DM_NVN_BLEND_ZERO=0 it is 0.
      • The mapping is cleared after the draw.
      • The same draw without a mapping binds 0.
      • With the new test, and bz_bind reading g_pso_lmap as main does, the mapped draw binds 0 and that check fails (1 failure with the rule, 0 with DM_NVN_BLEND_ZERO=0).
    • make check_t_blend_zero, as it is:
      • This branch: 165 checks, 0 failed, with the rule and with DM_NVN_BLEND_ZERO=0.
      • Main: 160 checks, 0 failed, both ways.
      • No Metal validation errors (the target runs with MTL_DEBUG_LAYER=1).
    • test_dmsh_blend_zero.py: all 7 tests pass, on main and here, test_compiles included (macOS and the iOS 18 target). This PR does not change the file.
    • These pass, with the same counts as on main:
      • check_t_alpha: 788 checks, 0 failed
      • check_t_logic: 929 checks, 0 failed
      • t_defer_chain: 59 checks, 0 failed
      • t_fuse_check: 10 checks, 0 failed
      • t_pso_warm: 123 checks, 0 failed, with both encoders
      • t_image_attachment_store: 120 checks, 0 failed
    • check_t_feedback: 278 checks. Its check with nothing running: a commit fails 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.py passes. docs_symptoms.py leaves the index unchanged.
    • The Mac host builds with dm/host make's compile line and has the same 58 warning lines as main.
  • Games run: none. I did not run Ocarina of Time or any other game, on main or on this branch.
  • 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 need DM_NVN_TBDR=1, so a report with default settings would show nothing.
  • Not run: the full 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.m or shaders.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 fragment encode_args call this PR changes, so that hunk's context needs updating. #2092 (same-pass feedback) also passes a mapping (g_fb_tlmap) into draw_; 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 of shaders.md, and that hunk applies on top of this one.

Docs

  • The finding is in its docs/reference/<subsystem>.md entry (shaders.md, "A NaN colour under a zero blend weight": Fix and Check), and python3 dm/tools/docs_symptoms.py was run (no change to the index)
  • A chain tool changed: does not apply, no chain tool changed
  • No game files, keys or data extracted from a game in the change or the attachments

…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
@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation labels Nov 11, 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.

1 participant