Repository navigation
Conversation
…ary: it spilled registers in every pixel where it never ran 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. The GPU gives registers to the whole shader, the branch included, so every pixel paid for it: in Donkey Kong Bananza's heavy area no draw set a bit, while the SilverStar material 6f9fd9cb5fbc873b spilled 336 bytes a thread (160 without) on an M1. A pixel shader's main library is now built without the correction (border_plain: the samples back, the twins and the block still declared, so the argument buffer is the same); the correcting source goes into the side library <key>_bb. The host takes it only for a pipeline drawn while a sampler member has a twin (border_key, bit 1 of pso_key.logic; bb_fn loads it on first need). The alpha test and logic operation libraries keep being built from the correcting source. Measured with the 486 shaders of one heavy frame built this way (handheld, M1): the scene pass 5.1-5.3 -> 3.2 ms with more draws, GPU busy 18.8-22.5 -> 14.8 ms a frame. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
by the way @seedds, DK Bananza uses upscaling which is fine on newer M chips that have tensor chips, but uses 4-5ms on chips without, I'm experimenting with replacing it with MetalFX, this will make the game look slightly different though (I haven't tested yet), so I'm going to add it as an optional setting in the menu before the game boots, this will finally make the game run at 60fps on M1 devices (alongside some other optimizations I found but haven't PRed yet), but I don't know if this is "too hacky" for nxnative, what do you think? |
|
Thanks, the register-spill measurements are convincing. I'm not landing it in tonight's unattended run, for two reasons. It's a chain change (every game's border-capable pixel shaders change MSL). And it adds a third side library ( |
…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>
|
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. |



What and why
border_emuwraps every sample through a sampler member indm_bs, a branch the host turns on only for a sampler whose border colour Metal has no sampler state for. The GPU allocates registers for the whole shader, the branch included, so every pixel paid for it even where it never ran. In Donkey Kong Bananza's heavy area no draw set a border bit, yet the material6f9fd9cb5fbc873bspilled 336 bytes a thread on an M1 (160 without the correction);6d1b110994db5413288 → 80. Seven of the frame's pixel shaders spilled, and the GPU counters showed it underfilled (about 39 % occupancy).A pixel shader's main library is now built without the correction (
border_plain: the samples are taken back to their first argument, the twin members and the border block stay declared, so the argument buffer is unchanged). The correcting source goes into a side library<key>_bb, built throughps_side_finishlike_ba/_bl. The host uses it only for a pipeline drawn while a sampler member has a twin (border_key, bit 1 ofpso_key.logic;bb_fnloads it on first need). The alpha-test and logic-op libraries are still built from the correcting source, and the two!k->logicchecks upstream added since are now!(k->logic & 1)so a border pipeline keeps its blending.Reference entry: a note under Border colours Metal has no sampler state for; the Bananza README lists where it was found.
Checked
Tests (M1, Metal):
dm/gfx/tests/test_dmsh_border.py(13, new: every sample taken back, argument buffer kept, unchanged without corrections, the plain source compiles),t_border(50, newborder_keycases: twin sampler, Metal's own sampler,SAMPI, no border block), alpha replay (788), logic replay (929),t_nvn(227),t_pso_skey,test_dmsh_wide,dm/tools/tests/test_docs.py.scripts/regress.py chain --baseline origin/main(03d92a9 → 6e2bed0), both games it reaches most: 0 newly failing, 0 interface changes.Neither game has a smoke route on this Mac, so there is no runtime section in the report.
Games run: Donkey Kong Bananza 040074A01BF12000 v458752, handheld (
DM_OPMODE=0), the same heavy area, camera moving,DM_NVN_GPUPROF=every:60:DM_MSL_DIRoverlay): scene pass 5.1–5.3 → 3.24 ms, GPU busy 18.8–22.5 → 14.8 ms a frame;_bblibraries): scene pass 3.35 ms, no_bblibrary loaded in that area, no RED lines. Whole-frame GPU busy (17.5 ms in that run) varies with the view, so I'd treat the scene-pass number as the like-for-like one.Devices and OS: MacBook Pro (13-inch, M1, MacBookPro17,1), 8-core GPU, macOS 26.6.2, Xcode 27.2.
Not checked: Splatoon Raiders in game (static chain check only), the iPhone, a game that actually draws with a twin sampler at runtime (covered by
t_borderonly).Docs
docs/reference/rendering.mdentry and the game note, andpython3 dm/tools/docs_symptoms.pywas rundmsh_chain.py --headerwas run anddm/gfx/nvn/dmsh_chain.his part of the change🤖 Generated with Claude Code