Skip to content

Metal compiler workarounds after KosmicKrisp: shader_metalwa rewrites listed shaders around a ballot that sees every lane on macOS 26 (KK3) and a ballot before a loop's only break (KK9) - #2397

Open
computerguy526 wants to merge 1 commit into
seedds:mainfrom
computerguy526:metalwa-kk3-kk9

Conversation

@computerguy526

@computerguy526 computerguy526 commented Nov 11, 2026 •

Copy link
Copy Markdown

Related to the PRAGMATA bring-up, ccd834e (@seedds).

The GPU compiler of macOS 15.4 to 26 if-converts a branch whose ballot result merges after it, and evaluates simd_ballot with every lane active: if (c) m = ballot(true); else m = 2; gives 0xffffffff where only the lanes with c belong. KosmicKrisp lists this as KK_WORKAROUND_3 (reported as FB20113490, fixed in macOS 27). A ballot waterfall then never ends once a SIMD group holds two keys, and the kernel spins until the GPU faults. The speculation happens when the running OS compiles the AIR for the GPU, so what counts is the OS the game runs on, not the one that built the library. KK9: a ballot before a loop's only break also sees every lane, at -O0 too; KosmicKrisp keeps that workaround on macOS 27.

What changes

  • dm/gfx/tools/dmsh_metalwa.py (new), after KosmicKrisp's workaround forms (Mesa, MIT):
    • KK3 rewrites each spvSubgroupBallot as simd_or of the lane bit, with the lane id threaded to the function that needs it. It sits behind the bool function constant DMSH_MWA_FC (220, default on).
    • KK9 guards every loop break of a function that uses subgroup operations with if ((ulong)simd_active_threads_mask()).
    • Where the pass would leave a ballot unrewritten, it raises, and the build fails that shader with metal workaround rewrite failed.
    • Over 24,403 translated MSL files it raised on none; a sample of 150 of the 4,594 files it changed all compile.
  • Profile key shader_metalwa <kk3|kk9> ... [<shader key prefix> ...]: applies to the listed shaders, or to every shader when no prefix is given. No profile has the row, so no game's MSL changes with this PR.
  • nvn_mtl.m: every set of function constant values the host specializes a game's shader with now comes from fc_values. That sets DMSH_MWA_FC false on macOS 27 and later (DM_NVN_METALWA=0 / 1 overrides; iOS keeps the default). Below 27 the set stays empty, as before.
  • Chain 0211f0a52894 (the new module is a chain file and part of CODE_FP).

Tests

  • python3 dm/gfx/tests/test_dmsh_metalwa.py covers the rewrites, lane threading, nested calls, the refusals, the gate, the profile row, and that every specialization goes through fc_values. None of it exists on main.

  • With DMSH_TEST_METAL=1, t_metalwa.metal (a ballot in a branch, a ballot waterfall on seven lane patterns, a ballot before a loop's only break, all bounded) runs on the GPU. On macOS 26.6.2 (M4 Pro), built as is at -O1:

    • the branch is wrong on 1,024 of 2,048 lanes;
    • the waterfall hits its 4,096-round cap on every pattern with two or more keys;
    • the break is wrong on 1,984 lanes.

    Rewritten, every check passes.

  • Reference entry: A ballot under divergent control flow sees every lane (macOS 26), plus the key's row in dm/games/README.md, env_switches.tsv and the symptom index.

Regression

  • regress.py impact picks The Legend of Zelda: Breath of the Wild, Pokémon Scarlet and The Legend of Zelda: Tears of the Kingdom (the profiles with a usage manifest, through nvn_mtl.m and the chain tools). They were not run.

  • Reach: with no shader_metalwa row the MSL is byte-identical, so no library changes. One host-side difference: on macOS 27 and later every specialization carries DMSH_MWA_FC = false. Metal ignores a value for a constant a function does not declare (checked through MTLDevice and MTL4Compiler). A pipeline archive recorded before this change was keyed without that value, though, so I would expect it to be looked up again rather than reused. I have no macOS 27 machine to measure that.

  • Credit: KosmicKrisp (LunarG / Mesa) found, reported and documented both bugs.

  • Working with UncraftableXD

…metalwa rewrites the shaders it lists around a ballot that sees every lane on macOS 26 (KK3) and a ballot before a loop's only break (KK9)

The GPU compiler of macOS 15.4 to 26 if-converts a branch whose ballot result
merges after it and evaluates simd_ballot with every lane active:
`if (c) m = ballot(true); else m = 2;` gives 0xffffffff where the lanes with c
belong (KosmicKrisp's KK_WORKAROUND_3, FB20113490, fixed in macOS 27). A ballot
waterfall then never ends once a SIMD group holds two keys, and the kernel
spins until the GPU faults. The front end keeps the ballot in its branch in
the AIR; the speculation happens when the running OS compiles it for the GPU,
so it depends on the OS the game runs on, not the one that built the library.
KK9: a ballot before the only break of a loop also sees every lane, at -O0
too; KosmicKrisp keeps that workaround on macOS 27.

- dmsh_metalwa.py (new), after KosmicKrisp (Mesa, MIT): KK3 rewrites each
  spvSubgroupBallot as simd_or of the lane bit, with the lane id brought to
  the function that needs it, behind the bool function constant DMSH_MWA_FC
  (220, default on); KK9 puts every loop break of a function with subgroup
  operations behind `if ((ulong)simd_active_threads_mask())`. Vertex shaders
  are left alone (one-lane subgroups). Where it would leave a ballot
  unrewritten it raises, and the build fails that shader with "metal
  workaround rewrite failed". Over 24,403 translated MSL files it raised on
  none; 150 of the 4,594 it changed, sampled, all compile.
- Profile key shader_metalwa <kk3|kk9> ... [<shader key prefix> ...]: on the
  listed shaders, or every shader when no prefix is listed. Without the row
  the MSL is unchanged; no profile has it, so no game's shaders change.
- nvn_mtl.m: every set of function constant values the host specializes a
  game's shader with comes from fc_values, which sets DMSH_MWA_FC false on
  macOS 27 and later (DM_NVN_METALWA=0 / 1 overrides; iOS keeps the default).
  Below 27 the set stays empty, as before.
- Tests: test_dmsh_metalwa.py (rewrites, lane threading, nested calls,
  refusals, the gate, the profile row, that every specialization goes through
  fc_values). t_metalwa.metal: a ballot in a branch, a ballot waterfall on
  seven lane patterns, a ballot before a loop's only break, all bounded; with
  DMSH_TEST_METAL=1 they run on the GPU. On macOS 26.6.2 (M4 Pro), as built
  at -O1, the branch is wrong on 1,024 of 2,048 lanes, the waterfall reaches
  its 4,096-round cap on every pattern with two keys or more and the break is
  wrong on 1,984 lanes; rewritten, every check passes.

Chain 0211f0a52894. Credit: KosmicKrisp (LunarG / Mesa) found, reported and
documented both bugs; the rewrite follows its workaround forms.

Co-authored-by: uncraftablexd <352372032+uncraftablexd@users.noreply.github.com>
@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation shader chain The shader translation chain (DMSH_CHAIN): a change rebuilds every game's shaders 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) shader chain The shader translation chain (DMSH_CHAIN): a change rebuilds every game's shaders

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant