Skip to content

A float add to storage memory with no result read is one native atomic add instead of a compare-and-swap loop - #1930

Open
FeReuss wants to merge 1 commit into
seedds:mainfrom
FeReuss:pr/float-atomic-add
Open

FeReuss wants to merge 1 commit into
seedds:mainfrom
FeReuss:pr/float-atomic-add

Conversation

@FeReuss

@FeReuss FeReuss commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What and why

A RED.F32 (InstEmitMemory.EmitAtomicAddF32) reaches the MSL as a compare-and-swap loop, because MSL 2.3 has no float atomics. Metal has had atomic_float on device memory since MSL 3.0, and the compile takes the default language version. agl's scene analysis adds every thread's luminance into the same few words this way, so the threads' loops retry for each other.

dmsh_build.py now replaces, after the translation, only the exact loop the translator writes for a float add whose result is not read (float_add_msl) with one atomic_fetch_add_explicit((device atomic_float*)&word, value, memory_order_relaxed). Anything else stays as it was. On a MacBook Air M4, Ocarina of Time's 6e28359aa3558f61 (one workgroup of 256 threads, four words) took 0.25 ms a frame and 1bba09ec4bc8ff67 0.40 ms, and both are under 0.13 ms; the picture's exposure is unchanged.

It is a chain change (the translation tool dmsh_build.py): dmsh_chain.h is regenerated, and every game's shaders are built again.

Checked

  • Tests: dm/gfx/tests/test_dmsh_float_add.py (3 tests OK: the loop becomes one atomic add; other words and shapes are left alone; on the GPU the loop and the rewrite reach the same exact sums); dm/tools/tests test_nsp_chain (stale header) and test_docs (28 tests OK).
  • Games run: Ocarina of Time 0400486024DEC000 v131072, the Hyrule Field view, 1920x1080 docked, 120 s, window, 60 fps cap, enc_thread on. Two builds of the same host and view that differ in this alone (the chain differs, so there is no in-process A/B): frame p50 23.42 -> 22.39 ms (41.6 -> 44.6 fps), GPU p50 21.35 -> 20.45 ms, 0 [RED] lines, the foliage present in the screenshots. The two shaders alone explain about 0.4 ms; the rest is the GPU no longer waiting on the contended words. Measured on a tree of 2026-11-08 (main 7f502220 plus the series); this branch is rebased on 7a7acfee (the pattern is Python and applies as it was), and the shaders of this tree were not built (see below).
  • Devices and OS: MacBook Air M4 (10-core GPU, 16 GB, no fan), macOS 27.2.
  • For a change to the shader chain: scripts/regress.py check --games is not run. Only Ocarina of Time is built on this Mac, and a chain rebuild of it needs about 10 GB of free disk (4 GB here). The games that can reach it are those with a float RED/ATOM in a shader: regress.py chain on a Mac that has them built would list them, and I could not run it.

Front ends

None.

Docs

  • The finding is in docs/reference/shaders.md (the float add entry); the symptom index is unchanged.
  • A chain tool changed: dmsh_chain.py --header was run and dm/gfx/nvn/dmsh_chain.h is part of the change.
  • No game files, keys or data extracted from a game in the change or the attachments

Series: what these PRs add up to

This PR is one of a set from one effort to make Ocarina of Time (0400486024DEC000 v131072) hold its frame rate on a MacBook Air M4. The set, and how its parts combine into the last measured result:

The result (the Hyrule Field view, the heaviest of the game: 1920x1080 docked, 60 fps cap, window, 120 s runs, M4 with 10 GPU cores, 16 GB, macOS 27.2):

Tree fps frame p50 GPU p50
main 7f502220 + #1304, fps_hold at its default 30.1 33.31 ms 27.56 ms
+ fps_hold off (#1927) 33.7 29.38 ms 27.28 ms
build 3: + #1833 with the M4 window fix, border-FC (#1929), the barrier set and cut_draws 128 below, and the PRs listed as "also in the tree" 40.2 24.48 ms 22.09 ms
+ enc_thread on (#1928) 41.6 23.46 ms 21.39 ms
+ native float add (#1930) = build 4 44.6 22.39 ms 20.45 ms

(The step from 33.7 to 40.2 changes the shader chain, so it has no in-process A/B; #1833 is most of it. Kokiri Forest, walking, is at 60 fps on build 4; the Hyrule view is the worst case.)

Each part switched in one process on build 4 (DM_AB, 210 s, 600-frame phases, frame p50 with the part active against without; the noise between phases is about 0.3 ms): border-FC (#1929) 22.61 vs 23.98 ms; the barrier set (FRAG_ORDER, PASS_OVERLAP and HEAP_RELAX alternating together) 22.41 vs 22.58 ms; CUT_IDLE 22.74 vs 22.85 ms; DISPATCH_CHAIN 22.32 vs 22.48 ms.

Which PRs make that tree:

Part Where
fps_hold off #1927
enc_thread on #1928
border-FC #1929
native float add #1930
launcher's Render scale / GPU share (no frame-time effect) #1931 (draft, on #1304)
M4 address window of shader_raw_direct (without it every raw store is dropped on an M4 and the foliage is not drawn: the 48-50 fps of a build without it are false) the branch FeReuss:pr1833-m4-window, one commit on #1833's head (not yet offered on #1833 itself)
Base, other authors, open, in the measured tree #1304 (tier 0), #1714 (gpu_ts_hz), #1833 (shader_raw_direct), #1720, #1377, #1840
Base, in the measured tree, already in main or withdrawn #1844 (-fwrapv, now e4e5ff64), #1368 / #1708 (native mesh prewarm and mesh without task: main's own versions), #1707 (withdrawn)
Measured, not submitted (the net is within the noise): barrier set (FRAG_ORDER + PASS_OVERLAP + HEAP_RELAX), CUT_IDLE branches pr/pass-overlap and pr/cut-idle in FeReuss/nxnative-oot
Not ported: dispatch chain (the same idea as #1901), cut_draws 128 (64 and 128 tie on this tree)

All of it was measured on main 7f502220 plus the above; these PRs are rebased on main 8d5d4f1b; the tool and docs tests that touch them were run again there (test_env_switches, test_docs, test_nsp_chain, and each PR's own), while the host build and the graphics suite are those of 7a7acfee, and none of it has been run in the game on the newer main (a shader rebuild of the new chain takes about an hour and 10 GB of disk). This PR is #1930.

🤖 Generated with Claude Code

…c add instead of a compare-and-swap loop

A RED.F32 (InstEmitMemory EmitAtomicAddF32) reaches the MSL as a compare-and-swap loop on the word, because MSL 2.3 has no float atomics. Metal has had atomic_float on device memory since MSL 3.0, and the compile takes the default language version. agl's scene analysis adds every thread's luminance into the same few words this way, so the loops retried for each other: on a MacBook Air M4 Ocarina of Time's 6e28359aa3558f61 (one workgroup of 256 threads, four words) took 0.25 ms a frame and 1bba09ec4bc8ff67 0.40 ms, under 0.13 ms each as one atomic_fetch_add_explicit; the picture's exposure is the same. dmsh_build.py replaces only the exact loop the translator writes (float_add_msl), after the translation.

Between two builds of the same host and view that differ in this alone (the chain differs, so no in-process A/B), 1920x1080 docked, the Hyrule Field view: GPU p50 21.39 -> 20.45 ms, frame p50 23.46 -> 22.39 ms (41.6 -> 44.6 fps). The chain changes: dmsh_chain.h is regenerated and every game's shaders are built again; test_dmsh_float_add.py covers the pattern and its refusals.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@FeReuss
FeReuss force-pushed the pr/float-atomic-add branch from a6449ba to ef7f237 Compare November 9, 2026 16:44
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…ctions compiled out, DMSH_BORDER_FC), seedds#1930 (native float atomic add), seedds#1920 (GPU-written sliver does not own a texture: terrain tiles), seedds#1917 (depth attachment sampled by a depth-writing draw reads a copy), seedds#1928 (enc_thread on), seedds#1942 (idle-GPU late frames do not step the hold up), seedds#1940 (prewarm lines after the first frame), seedds#1936 (resolution families uncompressed)

Hand-resolved: image_texture keeps the virtual pools we admit; fb_prepare keeps our pass split and local dread flag after the depth snapshot; the compute key keeps the NDVE direct draw before the border constant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7

Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR):
- seedds#1774 by uncraftablexd (closed): Compute threads that exit before a barrier keep arriving at it, and every shader loop has a cap (GPU hang defences), chain change
- seedds#1917 by FifteenPen (closed): A draw that writes depth and samples its own depth attachment reads a copy of it: Ocarina of Time's light-blue dashes over the rock at Death Mountain Trail's cave entrance are gone
- seedds#1920 by nawangwe (closed): A GPU-written span's sliver inside a texture does not make it the GPU's: Ocarina of Time's terrain tiles no longer vanish or show another tile's surface (seedds#1566)
- seedds#1928 by FeReuss (open): Ocarina of Time: enc_thread on, the replay encodes on a thread of its own
- seedds#1929 by FeReuss (open): Unused border corrections are compiled out of pipelines that bind no sampler with a twin (function constant DMSH_BORDER_FC)
- seedds#1930 by FeReuss (open): A float add to storage memory with no result read is one native atomic add instead of a compare-and-swap loop
- seedds#1936 by TheGoldfish11 (closed): Write resolution-family image textures uncompressed
- seedds#1940 by uncraftablexd (open): A prewarm list's lines after its #@after_first_frame line are built after the first frame, in the background, instead of in the prepare step (issue seedds#1747)
- seedds#1942 by uncraftablexd (open): Frame step: late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
seedds#1901 seedds#1459 seedds#1928 seedds#1942 seedds#1940 seedds#1952 seedds#1611 seedds#2065 seedds#2083 seedds#2008 seedds#2079 seedds#2022 seedds#2075 seedds#2077 seedds#2020 seedds#2021 onto main 04811a6

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7

Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR):
- seedds#1459 by PromethOps (open): DM_UPSCALER=metalfx takes agl's agl_ndsr_vr.pck networks too, and a variant with no curve and no power is drawn as one colour space: Ocarina of Time's upscaler is MetalFX's
- seedds#1611 by Esteban-V (open): An argument block whose only change is a uniform buffer is patched instead of encoded again
- seedds#1774 by uncraftablexd (closed): Compute threads that exit before a barrier keep arriving at it, and every shader loop has a cap (GPU hang defences), chain change
- seedds#1824 by cocoman93 (open): Clamp texel fetches past a sampled 2D texture's edge: no hard edge in Ocarina of Time's mist
- seedds#1901 by FifteenPen (open): The game's compute dispatches with no game barrier between them run side by side (profile key dispatch_overlap, on for Ocarina of Time): Kokiri Forest GPU p50 15.63 → 14.76 ms
- seedds#1928 by FeReuss (open): Ocarina of Time: enc_thread on, the replay encodes on a thread of its own
- seedds#1929 by FeReuss (open): Unused border corrections are compiled out of pipelines that bind no sampler with a twin (function constant DMSH_BORDER_FC)
- seedds#1930 by FeReuss (open): A float add to storage memory with no result read is one native atomic add instead of a compare-and-swap loop
- seedds#1940 by uncraftablexd (open): A prewarm list's lines after its #@after_first_frame line are built after the first frame, in the background, instead of in the prepare step (issue seedds#1747)
- seedds#1942 by uncraftablexd (open): Frame step: late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed
- seedds#1952 by AdyaGMD (open): Compute moves ahead of scene passes past occlusion queries and conditional rendering; clears wait across dispatches
- seedds#2008 by bquilty25 (open): nvn: reactive memory watchdog, 16-way view cache, and texture upload batching
- seedds#2020 by padsiwono (open): Ocarina of Time v131072: Output Native draws the full output size instead of the game's 80 % frame (`DM_OUTPUT`)
- seedds#2021 by padsiwono (open): Ocarina of Time v131072 renders at the launcher's Resolution (`DM_RENDER_SIZE`), docked
- seedds#2022 by sleepyeyedjoe (closed): Ocarina of Time: large models no longer go black for one frame twice, 4–5 frames apart, when the game streams new textures into them (a texture ID a shader has read keeps its read mark when it gets another texture)
- seedds#2030 by humanperzeus (open): A NaN colour at alpha 0 is returned as 0, as NVIDIA's blender takes a zero factor: Ocarina of Time's white clouds over the ferns are gone
- seedds#2035 by Know-It-All-Brother (open): The shader build runs as worker processes, and no shader waits for a batch (Ocarina of Time's cold Mac map: 3,002 s to 1,298 s)
- seedds#2065 by Raymon82777 (closed): A vertex library with layer entry points of its own gets bases for its main0 pairs: Ocarina of Time's 230 ms camera-turn hitch in Hyrule Field goes to 39 ms
- seedds#2075 by sleepyeyedjoe (closed): Ocarina of Time: a patch of scenery no longer draws dark for one frame when the game gives it a sharper texture (a pool texture the walk does not make holds a stand-in of its own colours until its first read)
- seedds#2077 by roandegraaf (closed): Ocarina of Time: fix the "plaid" terrain / missing texture tiles (GPU-drawn textures now reach the storage the game copies)
- seedds#2079 by uncraftablexd (open): Render pipelines that share a vertex function are taken from the archive or compiled one at a time: the AGX driver crashes in createVertexProgramVariant's completion block
- seedds#2083 by FifteenPen (closed): Ocarina of Time's prewarm list gets 41 more task / mesh pipelines from later play
rafaeliga pushed a commit to rafaeliga/kid-with-sword-remake that referenced this pull request Nov 11, 2026
… one vector load, and a float add with no result read is one native atomic add (chain 450b0e1e44f6)

- From nxnative main 04419bc (seedds), the translator half: LDG.64 / .128 through the pool table load a uvecN once
  (GlobalMemoryVector) instead of one word at a time. Ocarina of Time's mesh shaders' 128-bit raw loads were about
  3.1 ms of its frame; GPU 16.3 -> 12.6 ms on an M4 Pro (with the commit's host half, not taken here: V has no
  nvn_queue.inc). The translator's DLLs are rebuilt from this source (dotnet build dm/gfx/tools/dmsh_xlate -c Release).
- From nxnative PR seedds#1930 (FeReuss): dmsh_build.py float_add_msl replaces the translator's compare-and-swap loop for
  a float add whose result is not read with atomic_fetch_add_explicit on an atomic_float. It is the two compute
  shaders at the top of the iPhone's per-dispatch profile (log 64: 1bba09ec 0.56 ms, 6e28359a 0.15 ms).
  The PR's other build steps (mul_extended, gather_offsets) are not in V and are left out.

Checked: test_dmsh_raw_last (4, with DMSH_TEST_XLATE: one vector load for four words, reads on the GPU),
test_dmsh_float_add (3). Every game's shaders build again.

Co-authored-by: seedds <seedds@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@XIIYAZ

XIIYAZ commented Nov 13, 2026

Copy link
Copy Markdown

This PR conflicts with main now: main moved the MSL rewrites (precise, mul_extended, gather_offsets and the rest) out of dmsh_build.py into dm/gfx/tools/dmsh_rewrite.py. Here is how I merged it so it builds and runs:

  • FLOAT_ADD and float_add_msl go into dmsh_rewrite.py unchanged, with their comment, after tg_fence.
  • In dmsh_build.py's finish, keep main's sequence and add one call after mul_extended: src = dmsh_rewrite.float_add_msl(src).
  • dm/gfx/tests/test_dmsh_float_add.py reads the functions from dmsh_rewrite.py instead of dmsh_build.py (the path in ast.parse and in compile, and the docstring).
  • dmsh_chain.h regenerated with dmsh_chain.py --header.

I've played Ocarina of Time on that merge for several days on an M4 Max, with no stutter and no picture problem.

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.

3 participants