Skip to content

Pixel shaders without the border-colour correction in their main library, on the shared side-library compile - #1203

Closed
AdyaGMD wants to merge 1 commit into
seedds:mainfrom
AdyaGMD:border-plain-bx
Closed

AdyaGMD wants to merge 1 commit into
seedds:mainfrom
AdyaGMD:border-plain-bx

Conversation

@AdyaGMD

@AdyaGMD AdyaGMD commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

The same fix as #1032, rebuilt on top of #1036's shared alpha test and logic operation compile. #1032 can't be rebased without a force push, and the side-library code it touched has been rewritten since. I've left #1032 and #1073 open so you can choose: if this one lands, #1032 can be closed, and so can the border part of #1073.

What and why

border_emu wraps every sample through a sampler member in dm_bs(…), a branch the host turns on only for a sampler whose border colour Metal can't express. That branch almost never runs, but the GPU allocates registers for it in every pixel. In the shaders below, that was enough to push them into spilling.

  • ps_libraries compiles <key>_b from the source without the correction (border_plain: each dm_bs(sample, …) is turned back into the sample). The twin samplers and the dm_border block stay declared, so the argument buffer is identical either way.
  • The compile unit of the correcting source (dmsh_side.unit) is cut into <key>_bb, the shader's own entry points with the correction, and <key>_bx, the alpha test and logic operation entry points. So _bx keeps correcting, as before. A shader without a compile unit gets <key>_bb from the correcting source as a whole. A shader without corrections is built exactly as on main, and a stale _bb is removed.
  • The reflection gets border when _bb exists, and publish_cache drops it so a cache hit makes the libraries again from the restored correcting <key>_b.metal.
  • Host: border_key sets bit 1 of pso_key.logic while a sampler member of the pixel shader has a twin. bb_fn then loads _bb on first need, and the draw compares border_key with the last pipeline's, since binding a sampler doesn't mark the pipeline dirty. g_ntwin lets it skip looking at the bindings when no registered sampler has a twin. The existing k->logic checks test bit 0.

The cache-hit path, the per-draw comparison and g_ntwin come from @givethesourceplox's iPhone port (#1073), credited in the commit.

Measured (M1, handheld)

Before After
Splatoon Raiders c0ae76e0ca76b574: spill per thread 592 B 0 B
Splatoon Raiders, after the first cutscene: the 236-draw 1920x1080 pass 20–21 ms 9.3 ms
Splatoon Raiders, same spot: frame time (median) 31.6 ms 19.8 ms
Donkey Kong Bananza 6f9fd9cb5fbc873b: spill per thread 336 B 160 B
Donkey Kong Bananza heavy area: scene pass 5.1 ms 3.2 ms

The pass dump shows that Splatoon pass binds no sampler that needs a border colour, so all its draws use the plain library.

These numbers come from builds of the same fix on the older per-variant side libraries (_ba, _bl, _bb). This branch is the port onto _bx. The spill counts come from compiling with metal-tt -arch applegpu_g13g and reading the per-thread scratch size.

Tests

test_dmsh_border.py gets border_plain cases: every sample taken back, the argument buffer unchanged, a source without corrections left alone, and the result compiling. t_border gets border_key's choice of library for a sampler with a twin, with Metal's own colour, with none bound, with a member that has no bit, for a constant sampler ID, and for a shader without _bb.

I didn't run the regression suite or the tests locally.

🤖 Generated with Claude Code

…ary, on the shared alpha test and logic operation compile (seedds#1036)

border_emu wraps every sample through a sampler member in dm_bs, a branch the host turns on only for a sampler whose border colour Metal lacks. It almost never runs, but the GPU allocates registers for it in every pixel. ps_libraries now compiles <key>_b from the source without it (border_plain) and cuts the compile unit of the correcting source into <key>_bb (the shader's own entry points, correcting) and <key>_bx (alpha test and logic operation, which keep correcting). The host takes <key>_bb only while a sampler member of the pixel shader has a twin (border_key, bit 1 of pso_key.logic; bb_fn).

On an M1, handheld:
- Splatoon Raiders, after the first cutscene: c0ae76e0ca76b574 spilled 592 bytes a thread, now 0; the 236-draw 1920x1080 pass 20-21 -> 9.3 ms, the frame 31.6 -> 19.8 ms.
- Donkey Kong Bananza, heavy area: 6f9fd9cb5fbc873b 336 -> 160 bytes; the scene pass 5.1 -> 3.2 ms.

The cache-hit path (fin calls ps_libraries on the restored correcting source), the per-draw border_key comparison and the g_ntwin early exit come from the iPhone port (seedds#1073).

Co-authored-by: givethesourceplox <272928635+givethesourceplox@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.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 game: 0400380020056000 Splatoon Raiders game: 040074A01BF12000 Donkey Kong Bananza labels Nov 4, 2026
@oliverames

Copy link
Copy Markdown
Contributor

I ran the same border split design on an iPhone 17 Pro Max (A19 Pro, iOS 27.2) with Donkey Kong Bananza, so here is the phone-side evidence. The build was a local tree that carries a border split like the one in #1032, with shader chain 215a6d278114. It was not this PR's exact head.

On a 70-second movement and attack route, six complete 300-frame reporting windows held a p99 of 33.34 ms with no dropped or discarded frames, at thermal state 0. The scaler logged 1676x943 to 2304x1296, the drawable measured 2346x1320, and the footprint ended at 5131 MiB with 3060 MiB of headroom. All 16 probed Metal 4 archive bases were found.

This doesn't isolate what the split does for performance. The comparison run before it was warmer (thermal state 1) and the two views weren't pixel matched. I'm not claiming a pacing or fur shimmer change from it.

seedds added a commit that referenced this pull request Nov 11, 2026
…sampler with a twin: every pixel shader with border-colour corrections declares the function constant dm_border_off (DMSH_BORDER_FC, 216) and tests it before the mask bit in dm_bs, and the host specializes each function with it true when no sampler bound to the draw's stages has a border twin (border_need, pso_key.border), so the branch and the registers it kept live across every sample are gone from the pipeline instead of skipped per pixel. The mask branch made Apple's compiler keep the sample's operands for the correction that never ran, which spilled registers in shaders with many samples. A pipeline builder takes an explicit off flag from the draw's key (pso_build, pso_build_ex, pso_build_dev, pso_desc4, pbase_key, cps_new_b, the mesh and capture pipelines, the prewarm and pso_offline.py's archives), and the specialized function is found there (fn_off, fn4_of_b, fn_dev_b); descriptors hold only real Metal functions. A draw compares its stages' need with the bound pipeline's (prog_border), so a sampler with a twin bound after the pipeline was picked gets the correcting pipeline. DM_NVN_BORDER_KEEP=1 keeps the corrections in every pipeline and =ab alternates by DM_AB phases. Ocarina of Time standing on the forest deck, in-process A/B on an Apple M6: GPU p50 10.66-10.88 ms without the corrections, 11.01-11.26 ms with them, frame time 16.67 ms both. Compiled for the M1's GPU (applegpu_g13g, 300 of its pixel shaders): mean code 16,232 -> 14,531 bytes, per-thread scratch lower in 77 shaders, higher in 11, the worst 240-352 bytes -> 0. AdyaGMD's measurement of the side-library form on an M1, Splatoon Raiders: 31.6 -> 19.8 ms a frame; not reproduced here. t_border (113 checks), the 136 gfx suites, test_dmsh_border, test_pso_offline, test_docs pass; regress.py check on Bastion and NieR:Automata: 60 fps, every shader builds, no new red line. The side library of PR #1203 is not taken: it compiles every pixel shader with corrections twice. Not run: Tears of the Kingdom (the one known game that binds a twin; its build is not on hand), the iPhone, an M1. The function constant is FeReuss's design and code (PR #1929); the diagnosis of the spill and the M1 numbers are AdyaGMD's (PRs #1203, #1032); the need for a per-draw choice is givethesourceplox's finding (PR #1073)

Front-ends: the shader chain version changes; shaders are rebuilt.

Co-authored-by: FeReuss <102842515+FeReuss@users.noreply.github.com>
@seedds

seedds commented Nov 11, 2026

Copy link
Copy Markdown
Owner

The problem this PR found is fixed on main by 511a0a2, in the form of PR #1929: a function constant that the host sets when no bound sampler has a border twin, so the correction's branch is compiled out of the pipeline. The diagnosis (the mask branch keeps the sample's registers live and spills) and the M1 measurement of Splatoon Raiders, 31.6 -> 19.8 ms, are credited to you in the commit message.

The side library is not taken because on today's main it compiles every pixel shader that has corrections a second time and stores a second library for it, where the constant costs one specialization of the pipelines that need it. Compiled for the M1's GPU (applegpu_g13g), 300 of Ocarina of Time's pixel shaders have 10% less code with the constant set and 77 of them less per-thread scratch (the worst from 240-352 bytes to 0).

Not measured here: Splatoon Raiders or any game on an M1. If you can rerun your scene on main, the frame time there would settle whether the constant gives all of the 31.6 -> 19.8 ms. Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400380020056000 Splatoon Raiders game: 040074A01BF12000 Donkey Kong Bananza 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.

3 participants