WIP: perf(vk): zero local memory at computed offsets by a loop on MoltenVK, not a written-out initializer #697

Closed
mooki wants to merge 88 commits from mooki/Longshot:mac/local-mem-buffer into main
Member

Type: perf · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (MoltenVK 1.4.2), game-free at this head; GPU probe LM-02 on arm64 (history) · Needs: Windows, Linux, macOS game run

Why

On a Mac, Metal compiles a program with a lot of local memory (the guest's LDL/STL, per thread) slowly: the probe LM-01 took
493 ms to make a pipeline with 4 KiB of it, 1,869 ms with 16 KiB and 7,518 ms with 64 KiB. The translator zeroes local memory with
an OpConstantNull initializer, and SPIRV-Cross, MoltenVK's translator to Metal, writes that initializer out element by element:
366,619 bytes of Metal source for 64 KiB a thread, 1.4 MB for 256 KiB. This PR zeroes local memory that the program indexes at
computed offsets with a loop at the start of main instead, on MoltenVK only. It is part of the macOS port. See #420.

What changed

On Apple, for MoltenVK only, Profile::zero_local_memory_by_loop declares local memory that the program indexes at a computed
word without its null initializer, and main opens with a block of its own that stores 0 to every word in a loop, then branches
to the program's first block: every word is 0 before the program reads one, as with the initializer. Local memory indexed only at
constant words keeps its initializer (Metal can keep that array in registers). RX_VK_LOCAL_ZERO_LOOP=0 turns it off. The program
cache's stamp gets a word only when it is on; every other driver's SPIR-V, and every program without computed local offsets, is
byte for byte as before.

  • back end, gpu/vk/eden/shader_recompiler/profile.h: Profile::zero_local_memory_by_loop; backend/spirv/spirv_emit_context.{h,cpp}
    DefineLocalMemory: no initializer when the flag is on and some LoadLocal/WriteLocal offset is not an immediate
    (local_memory_zero_loop, local_memory_words); emit_spirv.cpp DefineMain: ZeroLocalMemory before the program
    (a Function counter, a structured loop, then the program's first block). EDEN_NX2_LOCAL_ZERO=0 still zeroes neither way.
  • renderer, gpu/vk/vk_shader.cpp: inside #if defined(__APPLE__), local_zero_loop_wanted(driver, env) and
    local_zero_loop_on(); make_profile sets the flag in its Apple block; translator_stamp adds "lzlp" only when it is on,
    after #696's "gmlp".
  • docs: docs/development.md, a row for RX_VK_LOCAL_ZERO_LOOP.
  • tests:
    • tests/test_local_zero.py (new) with tests/local_zero_ir.cpp (new): LDL/STL as the frontend translates them (6 B to
      256 KiB; computed and constant offsets; 8- to 128-bit; a load past a 6-byte size) in compute, vertex and fragment stages and
      the mesh fallback's, off and on, in the renderer's and the Mac's profiles: spirv-val, the module shapes, every CPU run against
      a model of the window (uninitialized Private words started as a pattern), the check of the check (EDEN_NX2_LOCAL_ZERO=0:
      every read before a write sees the pattern), the flag alone changing no program without local memory, the switch for 10
      driver/env cases, and with --base the off forms byte for byte against an older tree.
    • tests/spirv_run.h: Private and Output words without an initializer start as Machine::undefined_word (0 by default).
    • tests/test_amd_subgroup_profiles.py, tests/test_subgroup_vertex.py: a local_zero_loop_on stand-in where make_profile
      is compiled cut out; the first checks the flag in every stage's profile.

Measurements

Verdict: faster on the GPU (history: LM-02 on arm64, pipeline creation 15-16 ms at every size against LM-01's 493 / 1,869 /
7,518 ms at 4 / 16 / 64 KiB, every output word exact, Metal's scratch for 64 KiB 6.1 GB against 8.3); game-free at this head
the Metal source of a program with 64 KiB of local memory at computed offsets falls from 366,619 to 6,322 bytes and stays that
size whatever the array's, with every CPU run the model's.

Hardware: macOS 26.7.1, Apple M4 Max, 64 GB, Apple clang 21 (game-free at this head; GPU results are the probe's history)
Scene: none (synthetic local-memory programs in the frontend's shapes, emitted, validated, run on the CPU interpreter and
converted by SPIRV-Cross)
Method: tests/test_local_zero.py at this head with --base a git archive of this PR's base and the Vulkan SDK's
spirv-cross (1.4.363.1), arm64 and --arch x86_64 (Rosetta); with this PR's test files on the base, which must fail. On the
GPU: probe LM-02 (LocalMemBuffer's report), one admission on arm64, on LM-01's program in the loop form, checked block for block
against the back end's module at the topic's commit 56092389. Exact checks: one run per route.
How to reproduce: python -I -B tests/test_local_zero.py --headers <SPIRV-Headers/include> --spirv-val spirv-val [--spirv-cross <spirv-cross>] [--arch x86_64] [--base <git archive of the base>]
Baseline: the "before" column is this PR's base, #696 (be4a1387), whose stamp words this PR extends (main has none of
them); its modules are the test's off form (38 of 38 byte-identical).

Before: base (#696) @ be4a1387 After: this PR @ bb02ac38
Metal source (SPIRV-Cross 1.4.363.1), compute, local memory at computed offsets: 4 KiB / 16 KiB / 64 KiB / 256 KiB, bytes 28,682 / 96,276 / 366,619 / 1,447,979 6,307 / 6,320 / 6,322 / 6,335
the same, vertex 1 KiB / fragment 16 KiB / compute 6 B 11,637 / 96,025 / 4,684 6,157 / 6,069 / 4,796
CPU runs against the model of the window (initializer) every run differ=0, arm64 and x86_64
modules without the flag or with constant offsets (--base) 38 38 of 38 byte-identical; the flag alone changes 0 of 22 programs without local memory
test_local_zero (this PR's test) fails with this PR's test files (vk_shader.cpp: local_zero_loop_wanted not found; no loop forms) passes, arm64 and x86_64
LM-02 on the GPU, arm64 (history), pipeline creation LM-01 (initializer): 493 ms (4 KiB), 1,869 ms (16 KiB), 7,518 ms (64 KiB) 15-16 ms at every size; every word PASS
this PR @ bb02ac38, arm64 (x86_64: the same MODULE, CPURUN, NOZERO, FLAG and BASE lines):
MSL program=compute-65536 profile=mac off_bytes=366619 on_bytes=6322 off_lines=124 on_lines=138 off_zero_elements=32768 on_zero_elements=0
MSL program=compute-262144 profile=mac off_bytes=1447979 on_bytes=6335 off_lines=124 on_lines=138 off_zero_elements=131072 on_zero_elements=0
FLAG programs=11 profiles=2 same=22 of 22: the flag alone changes no program without local memory
SPIRV_VAL modules=76 failed=0
BASE modules=38 same=38 differ=0 missing=0 extra=0
PASS: 0 failures
the base with this PR's test files: FAIL source: vk_shader.cpp: a function not found (substring not found)

Flags and settings

One new switch, for turning the loop off; on by default on MoltenVK. The diff against main also shows the switches of the PRs it
stacks on, theirs to list: RX_VK_GM_LOOP (#696), RX_VK_SPV_PROFILE, RX_VK_SPV_PROFILE_MIN (#695),
HOS_KEEP_FREED_PAGES, HOS_ZERO_ALL_PAGES, RX_NVN_WATCH_AFTER, RX_NVN_WATCH_ALL, RX_NVN_WATCH_TEXELS_SRC (#480),
RX_VK_LIBRARY (#438), RX_VK_DLSS_ACTORS, RX_VK_GPU_DUMP, RX_VK_MESH_IND_COUNT_WORD, RX_VK_NO_UPSCALE_STUB (#658),
RX_VK_TEXEL_CODECS (#649), RX_VK_SET_FOR_TABLES (#643) and RX_VK_SET_FOR_LENGTHS (#644).

Flag What it is Before this PR After this PR Effect Helps / doesn't help / untested Recommendation
RX_VK_LOCAL_ZERO_LOOP=0 local memory at computed offsets zeroed by its null initializer again, not by the loop no switch (the initializer everywhere) unset: the loop on MoltenVK; 0: the initializer Metal source size and compile time; in the program cache's stamp helps: compile time (LM-02, MoltenVK); other drivers: no effect; untested: the game's own programs unset (the loop on)

Testing

Tested: on macOS 26.7.1, Apple M4 Max, at bb02ac38, this PR's head before the rebase onto e090cc48 (main 6c9edffe,
#696's stack, this PR), game-free, every run under the shared check slot with a fresh HOME and TMPDIR:

  • tests/test_local_zero.py --base the base, with spirv-cross, arm64, and --arch x86_64: exit 0, as in Measurements. On
    the base with this PR's test files: fails; test_amd_subgroup_profiles.py there too (no zero_local_memory_by_loop).
  • tests/test_local_zero.py --keep at this head writes the 96 modules and Metal files of LocalMemBuffer's modules-01 byte for
    byte, among them on-compute-4096-renderer.spv, the module LM-02's loop was matched against block for block, so LM-02's
    result holds for this head's loop.
  • tests/test_seg_shader.py --base-tree the base, arm64 and x86_64: exit 0, 139 of 139 identical.
  • tests/test_gm_loop.py --base #696's base; tests/test_subgroup_vertex.py --base-tree the base, arm64 and x86_64;
    test_msl_depth, test_vendor_fixes, test_amd_subgroup_profiles, test_program_stamp_knobs (19 switches),
    test_program_cache_format: exit 0.
  • Objects: emit_spirv.cpp, spirv_emit_context.cpp and vk_shader.cpp compiled whole with main 6c9edffe's renderer flags
    for arm64 and x86_64: 6 of 6, exit 0 (scratch tree with #476, #492 and #490).
  • The wave's merge (below, in the run_all box): link check 0 of 620 declared names without an external definition, 66 of 66
    objects, 1,216 name/arch checks, closure 565 symbols a route, 0 undefined.
  • History at the topic's commit (LocalMemBuffer's report): LM-02 on arm64 (MoltenVK 1.4.2); the topic's full suite with only its
    base's own failure. Every changed line here is the topic's, line for line, but one fixture line (Reviewer notes).
  • Logs: reports/PubWave-files/PubShader3/ (proof/lmb-bb02ac38-*.log, keep/lmb-bb02ac38-arm64,
    compile/lmb-local-mem-buffer-bb02ac38/, equal-lines.txt, stamps.txt, offapple-lmb-be4a1387-bb02ac38.txt,
    merge-tree.txt, wave/).
  • After the rebase onto main e090cc48, at 8f6597c0 (was bb02ac38): Sweep2's own checks of this head were still
    running at writing (reports/Sweep2-files/lanes/697.txt); python -I -B tests/run_all.py on wave Sh3's merge
    57c9c4ab, which carries this head: 578/31/18 (passed/failed/skipped) against main e090cc48's 544/32/17 on this Mac,
    no new failure (reports/2026-11-14-rebase-sweep-2.md).

Not tested: the game at this head, and no GPU run at this head (LM-02 is history; its loop was matched against this head's
module, byte-identical to the topic's). Windows and Linux: not built there (5 changed files differ from the base's off Apple; the
loop's code is compiled everywhere, reached only with the flag, off there); its test request covers it.

Must be tested:

  • macOS, Apple silicon (MoltenVK 1.4.2): start the game to the title from a cold cache, expecting every program prepared again
    once (lzlp in the stamp) and the precompile's programs with large local memory taking less time than at the base (by hand:
    the owner, the log's vk: and [mvk-error] lines)
  • Windows and Linux, any GPU: build this PR (head 8f6597c0, it carries #696 with the PRs it stacks on) and main e090cc48
    the same way and start each to the title, and run python -I -B tests/run_all.py on Linux, expecting the build to compile this
    PR's changed files, the shaders prepared once more (a new translator stamp), and every other line to match main's (by hand:
    test request T-XXXX)

Risks

What could break: on MoltenVK, a program with local memory at computed offsets starts with the zeroing loop: a fault would
leave a word non-zero where the program reads it first (the CPU runs start every uninitialized word as a pattern and check every
output word against the model, and the check of the check sees the pattern without zeroing), or cost the loop's run time at the
program's start (a word a step; the initializer is also a run-time copy). Other drivers: nothing changes; only the stamp.

Turning it off: RX_VK_LOCAL_ZERO_LOOP=0 (the initializer, as before; in the program cache's stamp).

Compatibility: every platform's shaders are prepared again once (the back end is in the translator's stamp:
5ad12149fc77e534 → 0bbc4339d659e93e); on MoltenVK the translator stamp also gains "lzlp". kProgramCacheFormat stays 3.
Nothing changes in saves, longshot_options.ini or the vk ABI (MKWVK_API_VERSION).

Reviewer notes

  • Review be4a1387..bb02ac38, one commit, -x from macos/local-mem-buffer (56092389). Two conflicts, both in fixtures:
    test_amd_subgroup_profiles.py takes only this change's block, and test_vendor_fixes.py keeps its own text: its Apple
    drivers' fixture, where the topic gave the cut-out make_profile a local_zero_loop_on stand-in, is #698's and is
    not on this line. So every changed line is the topic's line for line in 11 files (macos/integrated 0e8829bd), and that one
    stand-in line is left to where the two meet. Read DefineLocalMemory's condition, then ZeroLocalMemory, then
    test_local_zero.py's model.
  • Depends on #696 (this branch is its head be4a1387), and through it #694 and #695, for the stamp's
    order and the fixtures' stand-ins; the change itself needs none of them. Until they merge this PR's diff shows their commits.
  • Conflicts with sibling PRs: #693 (profile.h, vk_shader.cpp, test_amd_subgroup_profiles.py): keep both, stamp
    order "msl2", "mslb", "sgv1", "gmlp", "lzlp" (as macos/integrated). #698 (vk_shader.cpp's Apple block,
    test_amd_subgroup_profiles.py; its test_vendor_fixes.py fixture then needs this PR's local_zero_loop_on stand-in): keep
    both; whichever lands second adds the cut-out fixtures' stand-ins (the macOS integration's 02b10861 plus this one line; the
    wave's merge carries them as its one fixture commit). #681 and #652 (and #666, #667): docs/development.md switch rows, keep
    both. git merge-tree with #692, #677-#680, #686: clean. The other conflicts with #650/#652 are the base's.
  • KosmicKrisp is left out on purpose: it has no SPIRV-Cross, and its NIR keeps an initializer whole.
  • Every platform: the back end's files and vk_shader.cpp are compiled everywhere and the stamp changes, so every platform
    prepares its shaders once; the loop is on only on MoltenVK. Not split for that reason.
  • Code comments cite reports/2026-11-14-*.md (the port's design notes), which are not in this repository.

Issues

See #420

Checklist

Everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description) of the status line's type.
  • Why gives the reason plainly and links the issues that fit (the tracker searched).
  • Measurements has a verdict, or one line saying why there is nothing to measure.
  • Testing names what was tested, what wasn't, and what must still be tested, where and how. (Test requests not open yet.)
  • Rebased on current main (main e090cc48; head 8f6597c0, was bb02ac38: this PR's own commits replayed one for one, old → new: bb02ac38 → 8f6597c0; the commit names elsewhere in this description are from before the rebase).
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named). (On the wave's merge 77333ce0: #693 on #692, this PR with #696, #694 and #695, #698 with #654, and one fixture commit for the cut-out make_profile stubs: 565 passed, 31 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the 31 failures are main's own and nothing that passes on main fails; test_collect_wide now passes; 33 new tests pass, test_local_zero among them; test_darwin_audio_service, Darwin-only, is skipped by the runner; reports/PubWave-files/PubShader3/wave/full-77333ce0/. After the rebase onto main e090cc48 (head 8f6597c0): wave Sh3's merge 57c9c4ab, 578/31/18 against main e090cc48's 544/32/17, no new failure.)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

When it applies

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value.
  • A game-free test added for the fix, failing on main and passing here. (test_local_zero fails on this PR's base with
    this PR's test files; on main too, which has neither the flag nor the fixture.)
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • Docs updated.
**Type:** perf · **Risk:** low · **Tested on:** macOS 26.7.1, Apple M4 Max (MoltenVK 1.4.2), game-free at this head; GPU probe LM-02 on arm64 (history) · **Needs:** Windows, Linux, macOS game run ## Why On a Mac, Metal compiles a program with a lot of local memory (the guest's LDL/STL, per thread) slowly: the probe LM-01 took 493 ms to make a pipeline with 4 KiB of it, 1,869 ms with 16 KiB and 7,518 ms with 64 KiB. The translator zeroes local memory with an `OpConstantNull` initializer, and SPIRV-Cross, MoltenVK's translator to Metal, writes that initializer out element by element: 366,619 bytes of Metal source for 64 KiB a thread, 1.4 MB for 256 KiB. This PR zeroes local memory that the program indexes at computed offsets with a loop at the start of `main` instead, on MoltenVK only. It is part of the macOS port. See #420. ## What changed On Apple, for MoltenVK only, `Profile::zero_local_memory_by_loop` declares local memory that the program indexes at a computed word without its null initializer, and `main` opens with a block of its own that stores 0 to every word in a loop, then branches to the program's first block: every word is 0 before the program reads one, as with the initializer. Local memory indexed only at constant words keeps its initializer (Metal can keep that array in registers). `RX_VK_LOCAL_ZERO_LOOP=0` turns it off. The program cache's stamp gets a word only when it is on; every other driver's SPIR-V, and every program without computed local offsets, is byte for byte as before. - back end, `gpu/vk/eden/shader_recompiler/profile.h`: `Profile::zero_local_memory_by_loop`; `backend/spirv/spirv_emit_context.{h,cpp}` `DefineLocalMemory`: no initializer when the flag is on and some `LoadLocal`/`WriteLocal` offset is not an immediate (`local_memory_zero_loop`, `local_memory_words`); `emit_spirv.cpp` `DefineMain`: `ZeroLocalMemory` before the program (a Function counter, a structured loop, then the program's first block). `EDEN_NX2_LOCAL_ZERO=0` still zeroes neither way. - renderer, `gpu/vk/vk_shader.cpp`: inside `#if defined(__APPLE__)`, `local_zero_loop_wanted(driver, env)` and `local_zero_loop_on()`; `make_profile` sets the flag in its Apple block; `translator_stamp` adds `"lzlp"` only when it is on, after #696's `"gmlp"`. - docs: `docs/development.md`, a row for `RX_VK_LOCAL_ZERO_LOOP`. - tests: - `tests/test_local_zero.py` (new) with `tests/local_zero_ir.cpp` (new): LDL/STL as the frontend translates them (6 B to 256 KiB; computed and constant offsets; 8- to 128-bit; a load past a 6-byte size) in compute, vertex and fragment stages and the mesh fallback's, off and on, in the renderer's and the Mac's profiles: spirv-val, the module shapes, every CPU run against a model of the window (uninitialized Private words started as a pattern), the check of the check (`EDEN_NX2_LOCAL_ZERO=0`: every read before a write sees the pattern), the flag alone changing no program without local memory, the switch for 10 driver/env cases, and with `--base` the off forms byte for byte against an older tree. - `tests/spirv_run.h`: Private and Output words without an initializer start as `Machine::undefined_word` (0 by default). - `tests/test_amd_subgroup_profiles.py`, `tests/test_subgroup_vertex.py`: a `local_zero_loop_on` stand-in where `make_profile` is compiled cut out; the first checks the flag in every stage's profile. ## Measurements **Verdict:** faster on the GPU (history: LM-02 on arm64, pipeline creation 15-16 ms at every size against LM-01's 493 / 1,869 / 7,518 ms at 4 / 16 / 64 KiB, every output word exact, Metal's scratch for 64 KiB 6.1 GB against 8.3); game-free at this head the Metal source of a program with 64 KiB of local memory at computed offsets falls from 366,619 to 6,322 bytes and stays that size whatever the array's, with every CPU run the model's. Hardware: macOS 26.7.1, Apple M4 Max, 64 GB, Apple clang 21 (game-free at this head; GPU results are the probe's history) Scene: none (synthetic local-memory programs in the frontend's shapes, emitted, validated, run on the CPU interpreter and converted by SPIRV-Cross) Method: `tests/test_local_zero.py` at this head with `--base` a `git archive` of this PR's base and the Vulkan SDK's `spirv-cross` (1.4.363.1), arm64 and `--arch x86_64` (Rosetta); with this PR's test files on the base, which must fail. On the GPU: probe LM-02 (LocalMemBuffer's report), one admission on arm64, on LM-01's program in the loop form, checked block for block against the back end's module at the topic's commit `56092389`. Exact checks: one run per route. How to reproduce: `python -I -B tests/test_local_zero.py --headers <SPIRV-Headers/include> --spirv-val spirv-val [--spirv-cross <spirv-cross>] [--arch x86_64] [--base <git archive of the base>]` Baseline: the "before" column is this PR's base, #696 (`be4a1387`), whose stamp words this PR extends (main has none of them); its modules are the test's off form (38 of 38 byte-identical). | | Before: base (#696) @ be4a1387 | After: this PR @ bb02ac38 | |---|---|---| | Metal source (SPIRV-Cross 1.4.363.1), compute, local memory at computed offsets: 4 KiB / 16 KiB / 64 KiB / 256 KiB, bytes | 28,682 / 96,276 / 366,619 / 1,447,979 | 6,307 / 6,320 / 6,322 / 6,335 | | the same, vertex 1 KiB / fragment 16 KiB / compute 6 B | 11,637 / 96,025 / 4,684 | 6,157 / 6,069 / 4,796 | | CPU runs against the model of the window | (initializer) | every run `differ=0`, arm64 and x86_64 | | modules without the flag or with constant offsets (`--base`) | 38 | 38 of 38 byte-identical; the flag alone changes 0 of 22 programs without local memory | | `test_local_zero` (this PR's test) | fails with this PR's test files (`vk_shader.cpp`: `local_zero_loop_wanted` not found; no loop forms) | passes, arm64 and x86_64 | | LM-02 on the GPU, arm64 (history), pipeline creation | LM-01 (initializer): 493 ms (4 KiB), 1,869 ms (16 KiB), 7,518 ms (64 KiB) | 15-16 ms at every size; every word PASS | ``` this PR @ bb02ac38, arm64 (x86_64: the same MODULE, CPURUN, NOZERO, FLAG and BASE lines): MSL program=compute-65536 profile=mac off_bytes=366619 on_bytes=6322 off_lines=124 on_lines=138 off_zero_elements=32768 on_zero_elements=0 MSL program=compute-262144 profile=mac off_bytes=1447979 on_bytes=6335 off_lines=124 on_lines=138 off_zero_elements=131072 on_zero_elements=0 FLAG programs=11 profiles=2 same=22 of 22: the flag alone changes no program without local memory SPIRV_VAL modules=76 failed=0 BASE modules=38 same=38 differ=0 missing=0 extra=0 PASS: 0 failures the base with this PR's test files: FAIL source: vk_shader.cpp: a function not found (substring not found) ``` ### Flags and settings One new switch, for turning the loop off; on by default on MoltenVK. The diff against main also shows the switches of the PRs it stacks on, theirs to list: `RX_VK_GM_LOOP` (#696), `RX_VK_SPV_PROFILE`, `RX_VK_SPV_PROFILE_MIN` (#695), `HOS_KEEP_FREED_PAGES`, `HOS_ZERO_ALL_PAGES`, `RX_NVN_WATCH_AFTER`, `RX_NVN_WATCH_ALL`, `RX_NVN_WATCH_TEXELS_SRC` (#480), `RX_VK_LIBRARY` (#438), `RX_VK_DLSS_ACTORS`, `RX_VK_GPU_DUMP`, `RX_VK_MESH_IND_COUNT_WORD`, `RX_VK_NO_UPSCALE_STUB` (#658), `RX_VK_TEXEL_CODECS` (#649), `RX_VK_SET_FOR_TABLES` (#643) and `RX_VK_SET_FOR_LENGTHS` (#644). | Flag | What it is | Before this PR | After this PR | Effect | Helps / doesn't help / untested | Recommendation | |---|---|---|---|---|---|---| | `RX_VK_LOCAL_ZERO_LOOP=0` | local memory at computed offsets zeroed by its null initializer again, not by the loop | no switch (the initializer everywhere) | unset: the loop on MoltenVK; `0`: the initializer | Metal source size and compile time; in the program cache's stamp | helps: compile time (LM-02, MoltenVK); other drivers: no effect; untested: the game's own programs | unset (the loop on) | ## Testing **Tested:** on macOS 26.7.1, Apple M4 Max, at `bb02ac38`, this PR's head before the rebase onto e090cc48 (main 6c9edffe, #696's stack, this PR), game-free, every run under the shared check slot with a fresh HOME and TMPDIR: - `tests/test_local_zero.py --base` the base, with `spirv-cross`, arm64, and `--arch x86_64`: exit 0, as in Measurements. On the base with this PR's test files: fails; `test_amd_subgroup_profiles.py` there too (no `zero_local_memory_by_loop`). - `tests/test_local_zero.py --keep` at this head writes the 96 modules and Metal files of LocalMemBuffer's `modules-01` byte for byte, among them `on-compute-4096-renderer.spv`, the module LM-02's loop was matched against block for block, so LM-02's result holds for this head's loop. - `tests/test_seg_shader.py --base-tree` the base, arm64 and x86_64: exit 0, 139 of 139 identical. - `tests/test_gm_loop.py --base` #696's base; `tests/test_subgroup_vertex.py --base-tree` the base, arm64 and x86_64; `test_msl_depth`, `test_vendor_fixes`, `test_amd_subgroup_profiles`, `test_program_stamp_knobs` (19 switches), `test_program_cache_format`: exit 0. - Objects: `emit_spirv.cpp`, `spirv_emit_context.cpp` and `vk_shader.cpp` compiled whole with main 6c9edffe's renderer flags for arm64 and x86_64: 6 of 6, exit 0 (scratch tree with #476, #492 and #490). - The wave's merge (below, in the run_all box): link check 0 of 620 declared names without an external definition, 66 of 66 objects, 1,216 name/arch checks, closure 565 symbols a route, 0 undefined. - History at the topic's commit (LocalMemBuffer's report): LM-02 on arm64 (MoltenVK 1.4.2); the topic's full suite with only its base's own failure. Every changed line here is the topic's, line for line, but one fixture line (Reviewer notes). - Logs: `reports/PubWave-files/PubShader3/` (`proof/lmb-bb02ac38-*.log`, `keep/lmb-bb02ac38-arm64`, `compile/lmb-local-mem-buffer-bb02ac38/`, `equal-lines.txt`, `stamps.txt`, `offapple-lmb-be4a1387-bb02ac38.txt`, `merge-tree.txt`, `wave/`). - **After the rebase onto main e090cc48**, at `8f6597c0` (was `bb02ac38`): Sweep2's own checks of this head were still running at writing (`reports/Sweep2-files/lanes/697.txt`); `python -I -B tests/run_all.py` on wave Sh3's merge `57c9c4ab`, which carries this head: 578/31/18 (passed/failed/skipped) against main e090cc48's 544/32/17 on this Mac, no new failure (`reports/2026-11-14-rebase-sweep-2.md`). **Not tested:** the game at this head, and no GPU run at this head (LM-02 is history; its loop was matched against this head's module, byte-identical to the topic's). Windows and Linux: not built there (5 changed files differ from the base's off Apple; the loop's code is compiled everywhere, reached only with the flag, off there); its test request covers it. **Must be tested:** - macOS, Apple silicon (MoltenVK 1.4.2): start the game to the title from a cold cache, expecting every program prepared again once (`lzlp` in the stamp) and the precompile's programs with large local memory taking less time than at the base (by hand: the owner, the log's `vk:` and `[mvk-error]` lines) - Windows and Linux, any GPU: build this PR (head 8f6597c0, it carries #696 with the PRs it stacks on) and main e090cc48 the same way and start each to the title, and run `python -I -B tests/run_all.py` on Linux, expecting the build to compile this PR's changed files, the shaders prepared once more (a new translator stamp), and every other line to match main's (by hand: test request T-XXXX) ## Risks **What could break:** on MoltenVK, a program with local memory at computed offsets starts with the zeroing loop: a fault would leave a word non-zero where the program reads it first (the CPU runs start every uninitialized word as a pattern and check every output word against the model, and the check of the check sees the pattern without zeroing), or cost the loop's run time at the program's start (a word a step; the initializer is also a run-time copy). Other drivers: nothing changes; only the stamp. **Turning it off:** `RX_VK_LOCAL_ZERO_LOOP=0` (the initializer, as before; in the program cache's stamp). **Compatibility:** every platform's shaders are prepared again once (the back end is in the translator's stamp: `5ad12149fc77e534` → `0bbc4339d659e93e`); on MoltenVK the translator stamp also gains `"lzlp"`. `kProgramCacheFormat` stays 3. Nothing changes in saves, `longshot_options.ini` or the vk ABI (`MKWVK_API_VERSION`). ## Reviewer notes - Review `be4a1387..bb02ac38`, one commit, `-x` from `macos/local-mem-buffer` (`56092389`). Two conflicts, both in fixtures: `test_amd_subgroup_profiles.py` takes only this change's block, and `test_vendor_fixes.py` keeps its own text: its Apple drivers' fixture, where the topic gave the cut-out `make_profile` a `local_zero_loop_on` stand-in, is #698's and is not on this line. So every changed line is the topic's line for line in 11 files (`macos/integrated` `0e8829bd`), and that one stand-in line is left to where the two meet. Read `DefineLocalMemory`'s condition, then `ZeroLocalMemory`, then `test_local_zero.py`'s model. - Depends on #696 (this branch is its head `be4a1387`), and through it #694 and #695, for the stamp's order and the fixtures' stand-ins; the change itself needs none of them. Until they merge this PR's diff shows their commits. - Conflicts with sibling PRs: #693 (`profile.h`, `vk_shader.cpp`, `test_amd_subgroup_profiles.py`): keep both, stamp order `"msl2"`, `"mslb"`, `"sgv1"`, `"gmlp"`, `"lzlp"` (as `macos/integrated`). #698 (`vk_shader.cpp`'s Apple block, `test_amd_subgroup_profiles.py`; its `test_vendor_fixes.py` fixture then needs this PR's `local_zero_loop_on` stand-in): keep both; whichever lands second adds the cut-out fixtures' stand-ins (the macOS integration's `02b10861` plus this one line; the wave's merge carries them as its one fixture commit). #681 and #652 (and #666, #667): `docs/development.md` switch rows, keep both. `git merge-tree` with #692, #677-#680, #686: clean. The other conflicts with #650/#652 are the base's. - KosmicKrisp is left out on purpose: it has no SPIRV-Cross, and its NIR keeps an initializer whole. - Every platform: the back end's files and `vk_shader.cpp` are compiled everywhere and the stamp changes, so every platform prepares its shaders once; the loop is on only on MoltenVK. Not split for that reason. - Code comments cite `reports/2026-11-14-*.md` (the port's design notes), which are not in this repository. ## Issues See #420 ## Checklist ### Everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`) of the status line's type. - [x] Why gives the reason plainly and links the issues that fit (the tracker searched). - [x] Measurements has a verdict, or one line saying why there is nothing to measure. - [x] Testing names what was tested, what wasn't, and what must still be tested, where and how. (Test requests not open yet.) - [x] Rebased on current `main` (main e090cc48; head `8f6597c0`, was `bb02ac38`: this PR's own commits replayed one for one, old → new: `bb02ac38` → `8f6597c0`; the commit names elsewhere in this description are from before the rebase). - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). (On the wave's merge `77333ce0`: #693 on #692, this PR with #696, #694 and #695, #698 with #654, and one fixture commit for the cut-out `make_profile` stubs: 565 passed, 31 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the 31 failures are main's own and nothing that passes on main fails; `test_collect_wide` now passes; 33 new tests pass, `test_local_zero` among them; `test_darwin_audio_service`, Darwin-only, is skipped by the runner; `reports/PubWave-files/PubShader3/wave/full-77333ce0/`. After the rebase onto main e090cc48 (head `8f6597c0`): wave Sh3's merge `57c9c4ab`, 578/31/18 against main e090cc48's 544/32/17, no new failure.) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### When it applies - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value. - [x] A game-free test added for the fix, failing on `main` and passing here. (`test_local_zero` fails on this PR's base with this PR's test files; on `main` too, which has neither the flag nor the fixture.) - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] Docs updated.
Every scan down vk_memory.cpp's import table by start (pool_lookup's
covering import and merge plan, bg_drain, prefetch_chunks,
import_in_background, memory_note_gpu_write and memory_guest_imported)
stopped once an import ended more than 1 GiB below its address, on the
belief that no import is larger than that. pool_lookup's own merge escape
leaves an import above 1 GiB over the smaller ones inside it ("left
overlapping"), and the device allows imports up to max_import_size: a scan
stopped at a small import inside a large one, missed the large one, and
the same bytes were imported again, two memories over the same bytes.

import_reach.h counts the live imports by span class; its bound is above
every live span, within twice the largest. imported_add and imported_take,
now the table's only ways in and out, keep the count. A scan stops only
where no import further down can reach its address, so it also stops
sooner than before when the imports are small. The covering-import checks
in pool_lookup and import_in_background look past the last import at or
below the range for the same reason.

Tests: test_import_reach.py runs the actual scans with a 4 GiB import over
a 16 MiB chunk, left by the merge escape (imports are recorded ranges: no
payload, no Vulkan, no game). Its lookup, query, note, drain, background
and prefetch cases fail on main 5e072fc8 and pass here, and a source check
keeps every change to the table in the two helpers. test_import_windows
runs pool_lookup with the helpers; test_mirror_gpu_only names the new
publication point.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(vk): the readback refresh fixture runs memory_note_gpu_write with the import table's helpers
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 7s
PR description / Description follows the template (pull_request_target) Successful in 4s
693acd941b
test_parallel_rb_refresh cuts memory_note_gpu_write out of vk_memory.cpp,
whose scan now stops at the live imports' reach: the fixture includes
import_reach.h, cuts live_reach, imported_add and imported_take beside it,
and puts its pool in the table through imported_add. It failed to build
at 11e9ee44 ("use of undeclared identifier 'import_reach'").

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Reserve the fixed guest window with a non-overwriting Mach mapping. Reclaim
complete owned host pages with fresh anonymous mappings and zero only the
partial-page edges, preserving adjacent 4 KiB guest pages on 16 KiB hosts.
Use the actual host granule for non-guest allocation/protection and report
macOS discard failures instead of returning success.

Add game-free source-linked arm64/Rosetta tests covering collisions, repeated
free/recommit, unaligned ranges, failure paths and resident-memory reduction.
Keep Linux and Windows paths unchanged. Build-list integration is separate.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep the POSIX layer's outward 4 KiB MEM_DECOMMIT and inward 4 KiB MEM_RESET
contracts above the byte-exact Darwin helper. Check arithmetic before rounding,
and keep separate real-memory assertions for helper and wrapper ranges.

Default the Darwin regression test to the native architecture. Both CPU routes
remain explicitly selectable for acceptance without requiring Rosetta or arm64
execution from every ordinary macOS suite run.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's #324 (a4c77174) made hos_commit in host/mkw_glue.c return whether
the range was committed, so its definition now starts "int hos_commit(".
The Darwin guest-memory test cut the function by its old "void" head and
stopped with a ValueError on main's source, on this branch and on main alike,
before compiling anything. Cut it by the new head.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Decode Darwin signal contexts and access faults for arm64 and x86_64,
copy supported context edits back on signal return, and prevent recursive
callback dispatch when a fault handler itself faults. Keep the non-Apple
bridge text and the x64-shaped CONTEXT layout unchanged.

Add source-linked, game-free owned-memory fault tests for both targets,
including page repair, PC redirection, prior handlers and an inaccessible
handler stack. Document the native PC/SP contract and remaining limits.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Do not make the ordinary Apple Silicon suite depend on Rosetta. Default
to the running Python process's architecture and retain explicit repeated
--arch options for dual-route qualification. Document that command and
count only files when reporting crash-report directory changes.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(runtime): classify the fault bridge's fatal _exit in main's exit inventory
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
861d9aba6f
Main's crash records (#75) added an exhaustive inventory of host process
exits to test_crash_record.py, which scans every host/**/*.c on every
platform. darwin_fault.c's nested-fault _exit(128 + sig) is fatal, so it
is listed as one and exempt from the clean-end mark, like hos_thread.c's
ExitProcess(3). Without it the inventory fails on Windows and Linux too.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Select a local Vulkan loader or MoltenVK with RX_VK_LIBRARY and keep SDL and
renderer startup on that same library. Capture SDL's instance extensions on
the window thread, enable advertised portability extensions, and name missing
admission requirements and VkResults without changing Windows or Linux paths.

Keep the non-Apple host API at 21 for existing prebuilt renderers; append the
two macOS callbacks only in Apple's version 22. Cover pure decisions, legacy
platform expectations, ABI layout, and the SDL2/SDL3 bridge without a GPU.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Remove the SDL source-string wiring and source-index ordering assertions while
keeping the sixteen compiled behavioral cases and surface adapter compile.
Replace the comment-sensitive API header hash with fixed baseline field names,
byte offsets, versions and sizes, retaining typed Apple callback checks and
both platforms' actual admission decisions and negative controls.

Production code is unchanged. Prior full-suite and device receipts remain
bound to 818e33b1; only the affected game-free tests were rerun.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fixture cuts LoadLibraryA and create_device out of main's source. Since
#34, LoadLibraryA keeps dlerror()'s text in load_error, and since #191
native_fadd_wanted also takes the device ID and driver version, so the cut
code no longer compiled against the fixture's stand-ins on this base. Give
it load_error and a dlerror stand-in, and the four-argument stand-in. No
expectation changed.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(vk): pin the admission ABI at main's version 22 and macOS 23
All checks were successful
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 10s
PR description / Description follows the template (pull_request_target) Successful in 4s
0f2262a378
Main's custom output size (#51) took MKWVK_API_VERSION 22 on every
platform and appended take_text at offset 352. The macOS loader
callbacks now follow it: the rebased header says 23 on Apple and 22
elsewhere. The fixed contract follows: non-Apple version 22, size 360
and 45 named offsets (take_text added); Apple version 23, size 376,
vulkan_library and vulkan_extensions at 360 and 368.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(runtime): follow main's HLE call counts in the Darwin guest-memory fixture
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 11s
PR description / Description follows the template (pull_request_target) Successful in 5s
cd383fcbbf
Main's HOS_HLE_STATS (#153) made HLE() a counting wrapper: a static
HleCount named by its first field, hos_hle_stats and hos_hle_counted. The
fixture's cut of hos_mem.c now gets those two (off: each body runs
directly) and main's -Wno-missing-field-initializers, as main's other
C fixtures pass. The guest-memory checks are unchanged.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This PR needs the macOS fault-context PR (#436); its commits are kept as they are.
The one conflict, host/posix/win32_posix.c's Apple include block, keeps both
darwin_mem.h (the guest-memory PR's) and darwin_fault.h (#436's).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep address-region and page frees owned until discard or in-place zeroing
finishes. Route guest mutations from each image to one executable-owned
registry, and pin complete imported backing through duplicate and overlapping
GPU memory lifetimes without querying the renderer under ownership locks.

Use real Mach query ranges and protections, preserve byte-exact 4 KiB guest
ownership and 64 KiB commit semantics, and handle all native watch subpages
with checked fault recovery and conservative dirty coverage. Replace racy
bulk dirty-byte reads with same-granule atomics.

Add game-free native arm64/Rosetta fixtures for two-image dispatch, actual
faults, aliases, failures, and deterministic free/reallocate/import races.
Carry prior RSS evidence rather than repeat unchanged experiments.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep a cancelled-writable watch state so an already pending diagnostic fault
is acknowledged without rearming or changing permissions. Check the real
mapping before acknowledgement; intentional read-only/no-access stays closed.

Freeze affected watch pages once, preflight the complete native range against
Mach maximum permissions, and issue one protection operation. Preserve prior
watch states when either preflight or the native operation fails.

Exercise the real Win32 vectored dispatcher, signal-handler retention and the
next genuine watch fault. Cover permission failures both before preflight and
after a deterministic barrier, with real Mach maximum-protection changes and
no manufactured VM results, on native arm64 and Rosetta.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The two commits before this one also changed Windows and Linux: the
dirty-page scan read its marks with byte atomics, the renderer's zeroed
guest memory was marked written, an empty range consumed no mark,
allocation failures in the scan and in the address-region bookkeeping
were fatal, hos_region_alloc refused a bad size or alignment,
hos_mark_written clamped its length, and VirtualQuery checked its
pointer and set last_error. Nobody has timed the scan there, so each of
these is now inside #if defined(__APPLE__), with main's code in the
#else. unifdef -U__APPLE__ of every host/ file this branch changes
equals main's byte for byte, and so do their preprocessed Linux and
Windows token streams. Issue #432 is the version for every platform.

test_collect_wide's fixture compiles the scan the platform builds: byte
atomics, and an empty range consuming nothing, on macOS; main's word
loads elsewhere.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
docs(macos): describe the guest window's one owner, import pins and native-page watches
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 5s
afa7490c0f
The guest-memory section still described the groundwork alone: it said
the write watch and VirtualQuery were separate work and that the
region allocator's unlock-before-discard race remained. Describe the
executable's single owner and how each image reaches it, frees that
keep their range until the bytes are gone, import pins, the real
VirtualQuery and VirtualProtect, the native-page watch and the atomic
dirty scan, that all of it is macOS only (issue #432), and the test's
two narrower modes.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Carries the vk-admission pull request's commits, which this one needs
(MkwVkHost's Apple callbacks, darwin_vulkan.h), on top of guest-16k.
host/posix/win32_posix.c: its Apple include block keeps darwin_mem.h,
darwin_fault.h and darwin_vulkan.h.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Append Apple23 full-backing import lifetime callbacks while preserving the
non-Apple21 layout. Keep buffer views move-only, release buffers before the
last memory owner unpins backing, and preserve completion-gated retirement.
Unpublished failures unwind; published roots retain process lifetime rather
than calling Vulkan from unsafe static teardown.

Check and unwind post-device startup allocations. Resolve the shared audio
shim through the executable-owned SDL service on Darwin without unresolved
weak imports or linker relaxation. Retain the existing loader/SDL behavior.

Add game-free owner, ABI, source-topology and strict-link consumers, including
both native arm64 and x86_64 routes. This slice does not claim to fix separate
host-import alias visibility or implement the final alias admission planner.

Assisted-by: GPT-6 Astra
Co-Authored-By: GPT-6 Astra <[email protected]>
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
044635ea gave vk_memory.cpp's Pool an Apple-only DarwinImportView member and
import_pool an Apple route (darwin_import_pool) right after its guards. It
moved test_import_buffer_binding and test_texel_buffer_ownership onto the
Windows/Linux layout (#if defined(__APPLE__) -> #if 0: the Apple owner has
its own darwin_import_lifetime_test), but three more fixtures cut the same
code and stopped compiling on macOS. Each passes at 58c5c5aa (044635ea's
parent) and fails at 044635ea:

- test_direct_buffer_writes and test_parallel_rb_refresh: their cut of
  struct Pool named DarwinImportView. They now cut Pool's Windows/Linux
  layout, the same one they compiled before.
- nx2/test_import_windows: its cut of import_pool's guards, which ends at
  the import, took in the unterminated Apple #if. It now ends at the Apple
  route, which must stay one #if just before the import; the fixture's own
  continuation models the Windows/Linux import as before.

No assertion, case or check is removed or loosened. test_parallel_rb_refresh
and test_import_windows also need the renderer-core extractions (next).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
044635ea added vma_startup_rollback (vk_vma.cpp, run after a failed
replay_start), which calls vmaDestroyAllocator. vk_vma.cpp includes
vk_work.h last so that RX_VK_RECORDER=defer/thread drains before its
destructions, and every such boundary call in a vk_work.h file must be in
RX_REC_BOUNDARIES with a drain-first macro. This one was not, and
test_recorder_sync caught it ("vk_vma.cpp calls ['vmaDestroyAllocator'],
which vk_work.h does not drain before"): it passes at 58c5c5aa (044635ea's
parent) and fails at 044635ea.

Add it to the list and to vk_work.h, next to vmaDestroyBuffer and
vmaDestroyImage. With the recorder off (the default) boundary() is a
relaxed load and a branch. Rollback runs before any image or buffer exists,
so nothing should be held there today, but the allocator is no longer
destroyed while a recorder could still hold packets. The test is unchanged
(sources: 42 boundaries drained, was 41).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
tests/test_darwin_renderer.py cuts vk_memory.cpp's code into
darwin_import_lifetime_test.cpp. Two of main's changes since this
branch's first base broke that cut on the rebase, before any check ran:

- #324 reworded the comment after imports_give_back's drain of the
  evicted pools ("(longshot) Off by default on Windows ..." is now
  "(longshot) Default by OS and vendor ..."). The cut ends at that
  comment, so it now looks for the new words. The cut code is the same
  drain loop as before.
- #250 (sparse mirrors) made retire_pool erase a retired pool from
  sparse_wanted. The fixture's stand-ins gain an empty sparse_wanted,
  beside mirror_wanted, as main declares it (std::vector<Pool*>).

No assertion, case or expected output changes.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
107fda29 moves the new pool into the import table
(imported.emplace(p.lo, std::move(p))) instead of copying it.
tests/test_mirror_gpu_only.py ends its cut of pool_lookup's merge at
"imported.emplace(p.lo, p)", so it stopped with "substring not found"
(full-suite receipt full-91f9a58d: the only check that passes on main
and fails here). It now ends at the moved form. The check is the same:
the merged pool must take in the gpu_only and gpu_written marks of the
pools it absorbs before it goes into the table.

On the macOS integration branch this was hidden: renderer-core's
53b8914d rewrote the same line for its imported_add. When that pull
request rebases onto this one, its line replaces this one.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
fix(macos): the import pin callbacks are macOS host API version 24
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 5s
0f54963d80
Main's custom output size (#51) took MKWVK_API_VERSION 22 everywhere, so
the macOS loader callbacks (#438) became version 23 after take_text, and
this branch's guest_import_pin and guest_import_unpin, appended after
them, become version 24. The Apple table is now 392 bytes with the four
callbacks at 360, 368, 376 and 384; the admission contract and the
renderer startup fixture pin exactly that. Windows and Linux stay at 22.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A guest span larger than one device allocation (the cap on MoltenVK/M4 Max is 41,747,087,360 bytes) is bound as
pieces: one descriptor per allocation segment, at the descriptor's own binding (reports/2026-11-14-full-range-design.md
sections 4.1-4.4). This is the shader half; the draw side (descriptors, layout, gathers) comes later.

- runtime_info.h: SegEntry {Storage|Table|TexBuf|ImgBuf, desc, pieces} and RuntimeInfo::segmented.
- Back end: a segmented storage buffer is an array of pieces at its binding (a table count * pieces, entry k's piece j
  at k * pieces + j); its loads, stores and atomics call DontInline helpers made after main, which find the piece by
  the pieces' OpArrayLength in order, with constant indices. The global memory fallbacks sum the pieces' lengths for a
  segmented descriptor's bound end and branch to its piece (single, native candidate, table written out and in the
  loop). Texel and image buffers are arrays too, their fetches, reads, writes and atomics through helpers that pick
  the piece by OpImageQuerySize, and a texel buffer's size query sums the pieces. ImageQuery was declared for every
  module already. With RuntimeInfo::segmented empty nothing new is made: the SPIR-V is byte-identical.
- vk_shader: SegSig (entries, hash) on Program, seg_sig and seg_variant; program_segmented finds or translates the
  variant of a plain program with plain's texture questions and answers and checks its descriptors are plain's;
  program_pick, mesh_program_locked, program_find_plain, loaded_variants and portable_variant keep the plain and
  segmented variants apart; every stage's RuntimeInfo::segmented comes from Program::seg.
- vk_progcache: a segmented variant is never written to the program cache nor listed among the loaded programs.
- tests/test_seg_shader.py + tests/seg_shader_ir.cpp: synthetic IR programs through the real back end, plain and
  segmented; structure checks, spirv-val (Vulkan 1.3, uniform buffer standard layout), and with --base-tree a byte
  comparison of the plain modules against an older tree. Five source-reading checks follow the new Program field and
  the atomics' new branch.

The translator stamp changes with this (eden/, vk_shader.cpp, sizeof(Program)): every program cache is rebuilt once.
Land it in the same release as the vk_api.h change that already does so.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit b7928876dab846485f667c1c934d34d25ecef035)
A connected guest span that the views of one operation need can be larger than
one device allocation (41,747,087,360 bytes on MoltenVK 1.4.2, M4 Max). It is
then imported as disjoint, adjacent segments of at most the cap.
gpu/vk/span_segments.h (rules only, no Vulkan) chooses the cuts: multiples of
the import granularity, the fewest segments, the least cost of the views a cut
splits (gathers by their bytes, then uniform, texel and storage buffers), a
cost for moving a previous cut, and the lowest addresses on ties. It maps any
guest range onto the segments, and splits a texel view at cuts and at
maxTexelBufferElements, reporting 12-byte seams. Invalid input gets an
explicit refusal. Nothing uses it yet: vk_memory.cpp takes it later.

tests/test_span_segments.py checks it against brute force at caps of 48 KiB,
64 KiB, 96 KiB and 1 MiB with 16 KiB granularity, plus the 48 GiB example, the
512 GiB guest window and the top of the u64 range (--arch arm64 / x86_64 on
macOS).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 11e53b66ffbdddd0cfeaa7a043361a337299d855)
feat(vk): let the segment planner add segments that pay for themselves
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 5s
6fd02124bc
With the fewest segments, a large gathered view can be cut on every draw
although one more segment would keep it whole: a 32 GiB index extent at
[8, 40) GiB in a 48 GiB domain at the 41,747,087,360-byte cap is split by
every two-segment cut list. plan() now weighs any number of segments, each
one beyond the fewest at Weights::segment (2^32 bytes-equivalent, above a
move), and returns the least cost, then the fewest segments, then the lowest
cuts. Every cut of that plan is a mark (the first multiple of g at or above a
demand's end, or a previous cut) or ceil(next cut - cap), so the search runs
over those candidates and is exact over [k_min, k_min + min(marks,
C0 / segment)]; a least plan over 4096 segments is refused. Plan::extra
reports the added segments.

The test brute-forces the same range of segment counts, checks a DP over
every count, and adds the 32 GiB extent (three segments, nothing gathered).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 189d0636cb5e90e0d98de397d0b9676a5f8354c2)
fix(macos): storage buffer tables get descriptor sets on MoltenVK
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 4s
77a91fc936
MoltenVK 1.4.2 binds a push descriptor set's resources one by one, never
through a Metal argument buffer (MVKDescriptorSet.mm pickArgumentBufferMode
returns Off for every push layout, and the pipeline names that set to
SPIRV-Cross as discrete). The SPIR-V back end declares a storage buffer
table (count > 1: Info::global_tables, the task and mesh programs'
c0+0x880 table) as one fixed-size array per typed view, all at one binding
(DefineSsbos; any global memory access uses the U32, U32x2 and U32x4
views), and SPIRV-Cross 6c09849f aliases such arrays only inside an
argument buffer. So every pushed program with a table failed: 256
"Attempting to alias same binding with a descriptor array which is not
implemented through argument buffers" errors and as many failed compute
pipelines in the first Mac start (mac-arm64-a5f2d6f9-01).

vk_vendor.h gets a fix, set_for_tables, on by default only for
VK_DRIVER_ID_MOLTENVK, with RX_VK_SET_FOR_TABLES to force it either way.
ensure_layout's push decision moves into layout_pushes(): with the fix on,
a set that has an array of storage buffers is not pushed but allocated
from the recording's pool, the path programs over the push limit already
take, where MoltenVK's sets are Metal 3 argument buffers. Every other set
is still pushed, the bindings are the same either way, and the SPIR-V,
the translator stamp and the program cache are unchanged. The vendor
fixes' log line at device creation says whether it is on, and a MoltenVK
run with MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS=0 is warned that these
programs cannot translate.

tests/test_set_for_tables.py compiles vk_draw.cpp's layout code (kPushSize
to the end of ensure_layout) on made-up programs and devices: on MoltenVK
a set with a table (compute, task, fragment) is a set, a set without one
(texture arrays and a constant buffer of count 2 included) is pushed;
ten other drivers push as before; the switch works both ways. It fails
on the base with 7 of 79 cases. tests/test_vendor_fixes.py adds a MoltenVK
device and the switch, and checks that the fix stays out of the
translation's profile and stamp. Windows and Linux: same layouts as
before; the log line gains one field.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 0c2fea2a303a5158d12ebc87bcd28467486486ef)
fix(macos): programs reading storage buffer lengths get descriptor sets on MoltenVK
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 4s
e90ba9a66c
MoltenVK 1.4.2 writes the buffer sizes SPIRV-Cross reads for OpArrayLength
in a discretely bound set only in bindDescriptorSets
(MVKCommandEncoderState.mm), never in pushDescriptorSet, and it binds every
push set discretely. So every pushed storage buffer's length read 0
whatever its range, while an ordinary set's (a Metal 3 argument buffer
with its own sizes) was exact (GPU probe,
reports/2026-11-14-null-range-probe.md). The back end measures storage
buffers in its global memory search (DefineGlobalMemoryFunctions:
native_global_bounds' candidates and every single storage buffer), so in a
pushed program no buffer ever held a global address: global stores and
atomics were dropped, the atomics returned 0, and loads the page table
does not serve read 0.

vk_vendor.h gets set_for_lengths, on by default only for
VK_DRIVER_ID_MOLTENVK, with RX_VK_SET_FOR_LENGTHS to force it either way.
The new gpu/vk/spirv_lengths.h (no Vulkan, as fallback_size_policy.h)
walks a module's words for OpArrayLength.
Each Stage gets array_length, noted beside its phis wherever the program
cache handles its SPIR-V: program_cache_save after translation (before the
program is published), a whole record read, and complete_from for a
compute program read without its code. The words are not kept anywhere a
layout could scan them later, and a light compute program's layout is made
before its code is read, so until then (array_length -1) a stage counts as
reading a length where it uses global memory: outside segmented variants,
which the cache never keeps, that is the only place the back end measures
a storage buffer. layout_pushes(p, bindings, total) then refuses a push
for any program with a length in any stage. Every other program, every
other driver, the SPIR-V, the translator stamp and the program cache are
unchanged.

The vendor fixes' log line says whether it is on, and at close one line
says how many programs it gave a set (and how many of those were decided
before their code was read).

tests/test_set_for_lengths.py builds real back end modules
(tests/set_for_lengths_ir.cpp: compute and vertex global memory with a
single storage buffer, 18 OpArrayLength each; global memory with nothing to
search; direct storage in compute, vertex and fragment stages, none), then
compiles vk_draw.cpp's layout code with their descriptors and notes:
138 layouts on MoltenVK and ten other drivers, with the switch unset, 0
and 1, stages not read yet, and the bindings equal pushed or not; plus the
renderer's scan on the same modules and on words that are not a module,
and the count kept for the log. test_vendor_fixes.py covers the new fix
on 17 devices, both ways of the switch, and its absence from the
translation's profile and stamp. The program cache fixtures
(program_cache_one_variant_test.cpp, program_cache_cleanup_test.cpp), which
compile vk_progcache.cpp's functions, include spirv_lengths.h beside
fallback_size_policy.h.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit bf5ca961c91479da13e8923387012ef6dc7fb2c7)
fix(macos): stages with more than 8 read_write textures get descriptor sets on MoltenVK
All checks were successful
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
fc5a86dcba
On MoltenVK 1.4.2 a pushed set is bound outside an argument buffer
(MVKDescriptorSet.mm pickArgumentBufferMode: push layouts are Off), and
there Metal's compiler takes at most 8 read_write textures in one function:
TexelDescProbe TD5 (40 storage images and 9 storage texel buffers in one
compute stage) failed pipeline creation pushed with "number of textures
with read_write access exceeds maximum supported (8)", and worked in an
ordinary set (Metal 3 argument buffers). The device reports
maxPerStageDescriptorStorageImages 256 (MoltenVK sets
maxPerStageStorageTextureCount to its argument buffers' texture count), so
nothing in Vulkan's limits stops it.

Which descriptors count: SPIRV-Cross 6c09849f declares a storage image
(sampled 2), storage texel buffers included as texture_buffer since
MoltenVK sets texture_buffer_native, access::read_write unless the
variable is decorated NonWritable or NonReadable (spirv_msl.cpp
image_type_glsl), and it never adds those decorations itself. The eden back
end decorates none (DefineImages, DefineImageBuffers), although Info knows
is_read and is_written. So every storage image and storage texel buffer
counts, by descriptor; sampled images and uniform texel buffers do not.
The limit is per Metal function, that is per stage.

layout_pushes now refuses a push under the existing set_for_tables fix
when any stage's storage images and storage texel buffers, counted by the
bindings' descriptorCount, exceed 8. It is the same cause and remedy as a
storage buffer table (a shape a pushed set cannot take on MoltenVK, where
the pipeline fails; an ordinary set takes it), and RX_VK_SET_FOR_TABLES
covers both, so the vendor fixes' log line is unchanged. ensure_layout
records where each stage's bindings begin, so the mesh fallback's task
and mesh stages, both compute, are counted apart. The programs moved for
it are counted and logged at close (draw_layouts_report). Every other
driver, the SPIR-V, the translator stamp and the program cache are
unchanged.

tests/test_set_for_tables.py gains 55 layouts: 8 and 9 storage images,
5+3 and 5+4 images and texel buffers, 9 texel buffers, an image array of
8 and of 9, graphics stages of 8+8 (pushed) and 0+9, the fallback's task
and mesh stages of 5+5 (pushed) and 4+9, and 41 read-only textures beside
8 storage images (pushed), on MoltenVK; the 9s on the ten other drivers
(pushed); the switch both ways; and the count kept for the close. It
fails on 7b82e199. test_vendor_fixes.py follows the new call and checks
the count reaches the close's log.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 162aca1b3cce50adde9250f14b209a4e5b4c1331)
fix(vk): MslArrayPtr: no pointer into a storage buffer array is passed to a function
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 4s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
9d48901abf
On MoltenVK a segmented storage program with a compare-exchange loop (an
increment, decrement or float add the device does not have) failed pipeline
creation: NullProbe revision 02's NS2, "no matching function for call",
&ssbo0_4[0u] given to a device _66 * parameter. SPIRV-Cross's MSL (6c09849f,
MoltenVK 1.4.2) passes a storage buffer pointer to a function by value, and
takes the address of the argument's expression: for an element of a buffer
array, which MSL holds as an array of pointers, that is the address of the
pointer (reports/2026-11-14-msl-arrayptr.md).

A descriptor bound as pieces now has its loops written out in the piece,
inside the segmented helper (CasLoopHere, CasStoreHere: CasLoop's loop,
attempt for attempt, its variables made first in the helper), instead of
calling the CasLoop function or write_storage_cas_loop_func with a pointer
into the pieces' array. Every other program is as before byte for byte.

Tests: test_seg_shader.py checks that no module passes a pointer into a
buffer descriptor array to a function. The two checks that cut the back
end's source follow the new loop and parameter.

Ported from macos/msl-arrayptr, where it was committed on NullShader, to the
pieces alone: here every storage access that reaches a segmented helper is
in pieces, so the loops are always written out. Left for the NullShader
change: the bound-whole branch that still calls the function with its
block, the guarded helper's cas_loop, and the tests that need NullShader or
tests/spirv_run.h (the CPU runs of the loops and of a storage buffer table,
--changed-segmented and the *.cpurun comparison).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit bd396010887afab9520181009db1b56a4cc425e3)
feat(vk): texel codecs in the SPIR-V back end and program variants
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 4s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
c16eac09d9
A texel buffer whose format the GPU gives no texel buffer view (MoltenVK 1.4.2 on Apple GPUs: R32G32B32_SFLOAT, the
R8G8B8 formats and the 16-bit packed ones) is read and written through an R8, R16 or R32 UINT view of the same bytes,
with the format's decode and encode in the shader (reports/2026-11-14-texel-formats-design.md section 4).

- Back end: RuntimeInfo::texel_codecs (TexelEntry: a texture or image buffer descriptor and its Shader::TexelCodec),
  honoured with Profile::support_texel_codecs only. Carrier images, per-codec decode and encode functions of integer
  operations alone (normalized components through tables of the exact floats, writes rounded to nearest even from the
  float's bits), whole-texel bounds (reads past the last texel 0, writes dropped, the size the elements over k), an
  integer format's own view read as U32 or S32 texels, and composition with the segmented piece arrays (the helpers
  take each descriptor's own image type). Typed storage accesses and atomics keep the view of their declared format:
  a codec there is refused. With no codec the SPIR-V is byte for byte as before.
- Renderer: gpu/vk/texel_codec.h, the plan shared by the variants and (next) the views; texel_codecs_on (macOS, from
  the format query, RX_VK_TEXEL_CODECS); each texel buffer's codec a variant answer (desc | Program::kTexelAnswer, so
  the program cache and the pipeline state list carry it as they are), checked by still_valid; "txcd" in the
  translator stamp when on; program cache format 2.
- Tests: tests/seg_shader_ir.cpp and tests/spirv_run.h emit every codec plain and in pieces, validate it, run its
  decode and encode on the CPU against reference conversions (every input of the 8- and 16-bit formats, random and
  boundary sets for 32-bit) and run its entry point over texel buffers in memory; the GPU probe's codec module.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 783eb1898b91d505ff22a31f337256c5047d0177)
fix(macos): vk_memory.cpp compiles on macOS: #324's evicted_pools declaration is not Apple's
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 9s
Third-party parts / As their notes say (pull_request) Successful in 7s
PR description / Description follows the template (pull_request_target) Successful in 5s
7bf1589934
Main's #324 (a4c77174) declares `extern std::deque<…> evicted_pools;` above
memory_guest_imported, which reads it on Windows and Linux. This branch
defines evicted_pools on macOS as a process-owned reference
(`auto& evicted_pools = *new std::deque<…>;`), so after the rebase onto
6c9edffe vk_memory.cpp did not compile for macOS ("redefinition of
'evicted_pools' with a different type"). The game-free fixtures cut the
functions out and never compile the whole file, so they did not see it.

The declaration is now for Windows and Linux only: on macOS
memory_guest_imported answers through darwin_guest_imported, which comes after
the definition. Windows and Linux compile the same text as before.

First made on mac/renderer-core, where this branch is merged.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 444e05c119a261b36c7c762abf9d26194b0ee417)
# Conflicts:
#	gpu/vk/vk_memory.cpp
#	tests/test_mirror_gpu_only.py
#	tests/test_parallel_rb_refresh.py
#431 (fix/import-lookback) makes imported_add and imported_take the import
table's only ways in and out. #491 (mac/renderer-correctness) gives the
macOS Pool an import view (DarwinImportView) that owns the imported memory
and cannot be copied, defines the table on macOS as a process-owned map,
and releases imports through destroy_import. Merged, they do not build or
test on macOS:

- imported_add took a const Pool& and copied it. It now takes Pool&& and
  moves it into the table (the macOS stack's form, 53b8914d). Its callers
  already move (the merge kept #491's moves); the fixtures that call it with
  a local pool move it, or a copy where they read the pool afterwards
  (nx2/test_import_windows.py). Windows and Linux insert the same Pool at
  the same key: a move in place of a copy.
- test_import_reach.py cut the Pool and the scans with their Apple guards,
  so on macOS the fixture met DarwinImportView. It now resolves the Apple
  guards to the other side (test_vk_darwin_admission.py's select, as
  test_darwin_renderer.py does; the macOS route is that test's), cuts
  destroy_import where the source has it, and its source check does not
  count macOS's definition of the table as a change to it.
- test_darwin_renderer.py's Darwin fixture cuts live_reach, imported_add and
  imported_take (darwin_import_lifetime_test.cpp includes import_reach.h),
  and its queue-slot order check finds imported_take where the source has it.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
RendererSliceReview findings on 044635ea, forwarded by the parent.

darwin_import_commit refused a whole import when any page in its range was
committed but NOACCESS. Darwin's VirtualQuery reports the demand-paged guest
window committed with each page's own protection, so an explicit NOACCESS or
a diagnostic watch page dropped every binding in that range to zeros, where
the pre-pin path tried the import and let the driver's VkResult decide. The
CPU protection of a committed page is CPU-side state: such pages are now left
exactly as they are and the import proceeds; only an unmapped page or a failed
query refuses. Whether MoltenVK/Metal imports a NOACCESS host range is left to
its own result; a GPU arm for the parent-released probe settles it.

waveOutOpen on macOS returned MMSYSERR_ERROR without a word when the
executable provides no hos_posix_audio_open. The lookup now names the missing
service once, on stderr; nothing else changes.

Tests: darwin_import_lifetime_test.cpp imports a range with a committed
NOACCESS page (no commit, no reprotection); test_darwin_audio_service.py
counts the line: once with the provider missing, never with it present. Both
fail on an archived 73f90266.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 9e3832639a33e22dc8672e80a64354eb729c4c48)
Two host imports of the same bytes read stale data within one submission on MoltenVK. Under alias
isolation (set in memory_init on Apple only) every operation's guest buffers are admitted before it
binds the first, and no import is replaced while recorded work may still reach it:

- Seam: memory_admit / replay_admit on the replay thread (replay_mu, no parallel worker): draw()
  (mesh records, then its buffers once the pipeline is known), dispatch() (the upscale stand-in, then
  its buffers), the replay's signal, buffer copy, fill and uniform update, the textures of every
  copy and clear, and copy_b2t's buffer side once its texture is resolved.
- Resolver: one side-effect-free description of what each binding path binds, from the binders' own
  helpers (uniform_bind_size, storage_binding, texel_buffer_extent, buffer_extent, the *_bytes
  helpers), including the GPU unswizzle's source (texture_unswizzle_source), the temporal
  upscaler's actor motion (temporal_actor_demands) and the diagnostic readers' copies
  (RX_VK_DRAW_STATS, RX_VK_DUMP). tests/test_resolver_coverage.py holds every gpu_buffer_bind /
  gpu_buffer call site of the renderer to its resolver demand and its seam.
- Covering reuse: a range a live import holds needs nothing; on Apple a range inside a live import's
  backing becomes a new view on that memory. Backing extents count for alias conflicts.
- Handoff: end the pass, flush the uniform updates before this place, device-to-host barrier,
  submit, wait, then import (old memories retired and kept to completion, page-table changes
  deferred), host-to-device barrier.
- Cohorts: different operations whose union passes the cap get one memory each, with a handoff.
- Under batch admission the dry pass's view-making marks are skipped (its command buffer is then
  recorded in order); RX_VK_POOL_END_IMPORTED is ignored under alias isolation (it imported to
  answer a resolver's question).

Off alias isolation (Windows, Linux) nothing of this runs and every binding path is unchanged.

Tests: test_alias_admission, test_admission_handoff, test_resolver_coverage (new); the Apple
spare-backing reuse and backing-overlap cases in darwin_import_lifetime_test; extractions of
test_import_chunk_default, test_fallback_depth_hold, test_vulkan_copy_span(_gpu_compile),
test_draw_side_effects and test_vk_occlusion_queries follow the moved code, every assertion kept
(the last two also check the admission seam). test_import_reach.py's main() call is restored: the
53b8914d copy ran nothing.

Published on main 6c9edffe with #491 and #431 below it (mac/renderer-core):
- actor_reads binds main's index bytes (count << itype) for every index
  type, as main's actor_motion does. The macOS stack's copy bound nothing
  for an index type outside 0..2, which would change Windows and Linux too;
  such a draw does not reach actor_motion (bones_used refuses it when the
  draw is noted) unless its model key matches a draw of another index type,
  so that guard is left out of this macOS PR.
- vk_temporal.cpp: #59 (actor motion pairs copies by place) copies a pose
  read at its draw from the stage; the copy from the game's memory, its
  else branch, binds through actor_reads.
- vk_memory.cpp imports_give_back keeps #324's retire grace beside alias_ref.
- test_import_reach.py keeps #431's source check and prefetch case and takes
  the admission excerpts and --arch. Its main() call needed no restoring:
  #431's copy has it.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit ae1d7812c12f3e64941da0c34bb3541eb695f6db)
A connected guest span one operation needs that is larger than max_import_size (41,747,087,360 bytes
on MoltenVK 1.4.2, M4 Max) was left unplanned (admit_over_cap): its bindings came back empty. It is
now a segmented domain (reports/2026-11-14-full-range-design.md 2.1, 2.3, 5.3-5.5), in place of
that branch:

- memory_admit plans it as disjoint, adjacent segments where span_segments::plan cuts them (the
  operation's views and classes as demands, the domain's previous cuts kept unless moving one pays,
  priced extra segments), each an ordinary Pool of at most the cap: one import, its own page-table
  entries, mirror, write-backs and retirement. Only segments holding a demand are imported; every
  live import over their bytes is retired, after a handoff when recorded work may reach it.
- A view across a cut is never one memory: pool_lookup refuses it (counted, logged), not owed to an
  admission. gpu_span_bind (vk_internal.h SpanRef) gives its pieces, one per segment, for the
  consumers' failure branches; gpu_span_pieces and gpu_span_layout describe them binding nothing.
- Domains change only at an admission, never while parallel workers run (debug-checked with the
  design's invariants, seg_invariants); a batch past the cap is admitted as segments instead of
  being recorded in order. memory_admit loses its batch flag and AdmissionStats its splits.
- span_unused_piece: the hook the NullDesc design fills (MoltenVK has no nullDescriptor: an unused
  piece slot of a storage table cannot be null).
- import_planned retires a taken-in import only while it is still over the plan's bytes (two
  segment plans of one admission may name the same old import).

Off alias isolation nothing of this runs.

Tests: test_segmented_domains (the 48 GiB A/B case at the real cap, the priced extra segment,
injected small caps, growth with the cuts kept, a moved cut after a handoff, frozen under workers,
a refused domain reported, nothing off alias isolation; fails on the planner commit and on the
archived base); test_resolver_coverage lists gpu_span_bind's bindings and holds future callers to
a failure branch.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 9fb9b3b4a60af49e54031d9c7707cdaf71fa5117)
The renderer-core merge (f216d287) moved code three fixtures cut out of the
sources. ae1d7812 updated the extractions it listed; these three it missed
(its named set held no nx2/ tests, and the other two already failed on its
base for the reason the previous commit fixes). The code moved; what it does
did not change, so each fixture follows it and keeps every assertion and case:

- nx2/test_cbuf_table_extent: ae1d7812 moved uniform_range's `used` lines,
  unchanged, into uniform_used(), which the resolver shares, and
  uniform_range calls it. The test searched for those lines after
  uniform_range( (ValueError). It now cuts uniform_used's body, from the
  same `uint32_t used = index < Shader::Info::MAX_CBUFS` line, and
  uniform_range's own `used` up to the driver banks, and runs both as
  uniform_range does.
- tests/test_parallel_rb_refresh: ae1d7812 moved build_descriptors'
  bare-entry guess and binding range into storage_binding() (b.addr, b.size,
  b.bare, ...). The fixture now cuts storage_binding's guess and range and
  build_descriptors' binding from them, through memory_note_gpu_write, the
  lines it cut before. 53b8914d made memory_note_gpu_write's scan stop by
  import_reach.h's live_reach, so the fixture includes the header, cuts
  live_reach and imported_add, and adds its pool through imported_add.
- nx2/test_import_windows: pool_lookup and the code above it now read
  53b8914d's import_reach (live_reach, imported_add, imported_take) and
  ae1d7812's admission state and alias isolation globals. The fixture cuts
  them (DemandClass and AdmissionStats from vk_internal.h), with isolation
  off as on Windows and Linux and alias_conflict false (as
  tests/import_reach_test.cpp does). Its retire_pool spy and its two seeded
  imports use imported_take/imported_add, the table's only ways in and out.

The three print what they printed at 58c5c5aa.

Published on main 6c9edffe (mac/renderer-core): #431 below this branch
already cuts live_reach, imported_add and imported_take into
nx2/test_import_windows and test_parallel_rb_refresh (its rb_table.inc), so
those two keep #431's cuts and add only what follows ae1d7812: the alias
isolation globals, DemandClass and AdmissionStats, and alias_conflict's
stub; storage_binding's cut.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 64a84e4dbc10017f44cd16fc0fbd7440135ee498)
Two findings from the renderer-core review (reports/2026-11-14-renderer-core-review.md), both in vk_replay.cpp's
parallel-batch admission. Neither path is reachable off Apple: admit_fallback and batch_admit are set only where
memory_admission_on() is true.

P2.5: a parallel replay's batch its dry pass could not know is recorded on the replay thread in order (Mode::Worker,
admit_fallback), each operation admitted as the serial replay admits it, except NOP_UPDATE_UB, whose worker branch broke out
before op_admit. Its destination is bound at the recording's end (flush_ub_updates -> gpu_write); one that needed a growth over
reached memory was refused by the guard and written on the CPU out of GPU order. The worker branch now admits it under
admit_fallback, before the update counts as passed, so a handoff there writes only the updates before it.

P3.1: a batch admitted at once whose admission hands off called admission_handoff() with rmode Serial, which wrote every listed
uniform update into the recording that ends, including the ones the batch's own dry pass had listed (from ub0). admission_handoff
takes the end of the updates to write (ub_end, default all: unchanged for every other caller), and the batch passes ub0.

Tests: tests/test_batch_admission.py (new) runs the NOP_UPDATE_UB case, op_admit, replay_admit, admission_handoff and the batch's
handoff statement cut out of the source; on bacb7c59 both of its cases fail for those reasons. tests/test_admission_handoff.py
cuts admission_handoff out by its name only (its signature gained the parameter).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 39542f89f4443e14b70fb1e46bdaa3e0e330f2ea)
Findings P1 and P2.2 of the renderer-core review (reports/2026-11-14-renderer-core-review.md). Both are in vk_memory.cpp's
admission planner, which runs only under alias isolation (memory_admit returns at once elsewhere: macOS only).

P1, the page-table holes. A plan retires every live import it overlaps (retire_pool zeroes the import's whole range), but two plans
did not cover what they retired:
- the cohort (plan_cohort) retired the imports it overlaps and does not need without growing over them;
- a segment (seg_plan) retired an import that reaches into a segment no demand needs.
Shaders that follow the guest page table then miss on those bytes. The fix, by the rule that no byte reachable before an admission
is left without memory after it (design 5.5):
- admit_rest: the bytes of each retired import that no plan holds are planned with the first plan that retires it, imported
  exactly, before that plan's import (one that fails leaves everything as it was, as mirrors_trim does), and published after it;
  they keep the import's marks (gpu_only, gpu_written, trims, last_value) and count in the handoff's conflict test;
- seg_plan: a segment that replaces live imports makes every segment over those imports needed, to a fixpoint;
- the SegMem span takes in every live import over it, so a segment retires only imports its domain holds whole;
- the cohort's threshold is the device's import cap (max_import_size) under the admission, not kMergeMax: a merge up to the cap is
  one import (pool_lookup keeps kMergeMax: plan_import's merge_max defaults to it);
- admit_apply: a plan that is not made keeps the imports it would retire, and no later plan of the admission retires them.

P2.2, covered demands across a domain's boundary or cut. A range a live import already holds was left out of the domain: across the
domain's edge, the segment's plan retired its import and the guard then refused its binding; across a cut, one live import bound
it whole while gpu_span_pieces reported its pieces. The span's live-import take-in covers the first. For the second, a covered
range that straddles a cut seeds its domain's span (its segments are planned, the crossing import retired with the handoff the
conflict test asks for).

Tests: tests/test_admission_coverage.py with tests/admission_coverage_test.cpp (new; the admission cut out of the source, recorded
imports and page table, injected caps), cases merge-table, cohort-table, segment-table, domain-edge, covered-straddle. Each fails on
bacb7c59 for the reviewed reason (reports/CoreFix-files/repro-base-bacb7c59) and passes here on arm64 and x86_64.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit f29396c79600467b11fbb935a529a4c2511cf533)
Findings P2.1 and P3.3 of the renderer-core review (reports/2026-11-14-renderer-core-review.md). The code is Apple-only
(#if defined(__APPLE__) in vk_memory.cpp's admission planner).

P2.1: covering_backing returns the owner's backing for a demand across the end of the owner's view, and the view plan was
[max(want_lo, b->lo), min(want_hi, b->hi)): (a) at the owner's own key, imported_add kept the owner, the new view was destroyed,
memory_admit returned Ready and the guard refused the binding at every admission; (b) at another key, the view overlapped the
owner (eviction of either zeroed page-table entries the other served; seg_invariants broke inside a domain). A failed
darwin_import_view looped the same way. Now:
- view_plan grows the view over every live view on the same backing it overlaps; those are retired when it is made (the same
  memory: alias_conflict excludes it, no handoff; retired views are kept until their work is done) and their gpu_only and
  gpu_written marks carried. A live import of another memory over it means no view: the range is planned as pool_lookup imports it.
- admit_apply makes the view first, then retires what it takes in, then inserts it, and checks the insertion.
- A view that fails is logged, its backing is remembered (weak_ptr) as one that takes no view, and its range is imported on its own
  (the binding, or the guard's debt and the next admission's handoff), never as a view on it again.
P3.3: admit_st.reused counts only a view that went into the table.

Tests: tests/darwin_import_lifetime_test.cpp (the real Apple import code, test_darwin_renderer.py, both arches) gains spare-cross
(a range across a one-chunk owner's end), spare-reuse-count, spare-overlap (a two-chunk owner) and spare-view-failure (the driver
refuses a second buffer on a memory). Each fails on bacb7c59 for the reviewed reason (reports/CoreFix-files/repro-base-bacb7c59)
and passes here; the fixture takes --case NAME to run one alone (the driver's --process-exit is unchanged).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 0c1d2aea03e17a3a10b0c1aa6ef69461d8d405d9)
Finding P2.4 of the renderer-core review (reports/2026-11-14-renderer-core-review.md). Under alias isolation (macOS) bg_drain
dropped a background import only when it overlapped a live import or one sat at its start; it never asked alias_conflict, so an
import over bytes a memory retired, evicted or queued dead in the current handoff epoch still held (alias_ref) was published:
two memories over the same bytes in one epoch, the stale read alias isolation prevents. It could also publish a chunk across a
segmented domain's cut. Reachable only with the opt-in background imports (RX_VK_PREFETCH_CHUNKS, RX_VK_ASYNC_IMPORT,
RX_VK_BG_IMPORT).

Under alias isolation bg_drain now also drops (counted in st_prefetch_dropped) an import when alias_conflict holds over its
memory's bytes (on Apple its whole backing), or when it is over a segmented domain (seg_overlaps). The new branch runs only when
alias_isolation is set: off Apple the condition is unchanged. alias_conflict's and seg_overlaps' declarations move up beside the
alias isolation globals, and seg_overlaps' definition into SegMem's state block (above pool_lookup), so bg_drain (and every
fixture that cuts it out with them) sees both.

Tests: tests/admission_coverage_test.cpp gains bg-epoch (an import given back after work reached it, a background import of its
bytes), bg-domain (a background chunk across a cut after the segments were given back) and bg-elsewhere (off alias isolation,
published as before); tests/darwin_import_lifetime_test.cpp gains apple-bg-epoch (Apple's alias_conflict, real import code). The
first two and apple-bg-epoch fail on bacb7c59 for the reviewed reason (reports/CoreFix-files/repro-base-bacb7c59); all pass here
on arm64 and x86_64.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 755ac7e8757fdd30d4809ae1142e51232fae2a9e)
Finding N1 of the core-fix review (reports/2026-11-14-core-fix-review.md). f29396c7 let the admission merge imports up to the
device's import cap (plan_import(..., max_import_size), and the plans' merge at max_import_size). mirror_make refuses a VRAM mirror
for any pool over 1 GiB, and mirrors are on by default for a discrete, non-GCN GPU (an Intel Mac with an RDNA Radeon, which alias
isolation also covers) and with RX_VK_MIRROR=1: such a merged pool lost its mirror, every binding in it reading host memory.

The cap was never needed for correctness once admit_rest keeps a cohort's leftover bytes mapped, so the admission's threshold is
kMergeMax again (cohort plus leftovers past it). plan_import loses the merge_max parameter that only the admission used; it, its
comment and pool_lookup's calls are as on bacb7c59. No correctness reason to keep the cap was found: a cohort and its leftovers
import the same bytes a merge would, each piece no larger than the import it came from.

Off Apple: unchanged (admit_plan runs only under alias isolation; plan_import is textually bacb7c59's again).

Tests: tests/admission_coverage_test.cpp gains merge-mirror (the seam of two live 768 MiB imports at the real cap: no import the
admission makes is larger than mirror_make's limit, which the driver reads from mirror_make itself, and every byte stays mapped).
It fails on 755ac7e8 for that reason (reports/CoreFix-files/repro2-base-755ac7e8) and passes here on arm64 and x86_64;
merge-table (the same seam: every byte mapped, one handoff) passes on both.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 9ccc7c19864991be11d4437211eecf3e430b2a7d)
Finding N2 of the core-fix review (reports/2026-11-14-core-fix-review.md). f29396c7's closure in seg_plan marked every segment over
an import a needed segment retires as needed, and each such segment was then imported whole (up to the device's cap, 41.7 GB of
guest address space on an M4 Max) only to keep that import's few bytes in it mapped; and when such a forced segment's import failed
after the needed one's, the crossing import was already retired.

The closure is gone: seg_plan plans only the segments the operation's ranges need. The retired imports' bytes in segments no range
needs are leftovers, as a cohort's are: admit_rest now takes segment plans too and cuts every piece that lies in a domain planned in
this admission at that domain's cuts, so each piece lies in one segment, crosses no cut and overlaps nothing (invariant 1 holds),
and costs no more than the import it came from. They are made before the segment's own import and published after it retires the
import (admit_apply, unchanged), so a failure leaves everything as it was.

Off Apple: unreachable (seg_plan and admit_rest run only from the admission, under alias isolation).

Tests: tests/admission_coverage_test.cpp gains segment-rest (a chunk across the cut into a 100 MiB segment nobody needs, retired by
the segment the operation needs: its 4 MiB there stay one import clipped at the cut, nothing else of that segment is imported, every
byte stays mapped, no import crosses a cut). It fails on 755ac7e8 for that reason (reports/CoreFix-files/repro2-base-755ac7e8) and
passes here on arm64 and x86_64; segment-table, domain-edge and covered-straddle (the other segment cases) pass on both.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 596f0ff4063da99505e7d5df789580fb4cde89f7)
Open item 1 of the core-fix review (reports/2026-11-14-core-fix-review.md), more reachable after N1 (9ccc7c19): with mirrors on
(a discrete, non-GCN GPU such as an Intel Mac's RDNA Radeon, or RX_VK_MIRROR=1), an import an admission retires whose bytes go to
several new imports (a cohort and that import's leftovers, or two segments over an import across their cut) handed what the GPU
wrote into its mirror and nowhere else to the first of them whose mirror was made only: mirror_make ends a retired pool's wait unless
it is trimmed, so the others' mirrors were filled from guest memory without it.

admit_apply now does what mirrors_trim does for a trim's parts:
- it counts, per import the plans retire, the new pools its bytes go to (the plans over it and its leftovers);
- a retired import with more than one, whose mirror was on (inherit_pending, set by retire_pool), is marked trimmed and counted in
  trims_waiting once the plan that retires it is made, so each new pool takes its share when its mirror is made, and trims_settle
  lets it go once every live pool over its range has its own (or after kIdleEpochs, as before);
- a leftover of an import whose mirror was on wants a mirror at once (MIRROR_WANTED, in mirror_wanted), as a trim's parts do.
trims_waiting is declared ahead of the admission region (its definition, with mirrors_trim, is unchanged).

Mirrors off (Apple Silicon's default): no pool is ever MIRROR_ON, so retire_pool sets no inherit_pending, nothing is marked and no
mirror is wanted: unchanged (the counting runs, without effect). Off Apple: unreachable (admit_apply runs only under alias isolation).

Tests: tests/admission_coverage_test.cpp gains mirror-cohort (the seam's cohort retires a mirrored import: the cohort and the leftover
both inherit, the leftover wants a mirror, trims_settle, cut out of the source, lets the old one go) and mirror-segments (two needed
segments over a mirrored import across their cut); mirror_made models mirror_make's inheritance, whose two tests the driver checks
are in mirror_make verbatim. Both fail on 596f0ff4 for the reviewed reason (reports/CoreFix-files/repro3-base-596f0ff4) and pass here
on arm64 and x86_64; mirror-off (mirrors off: nothing marked, nothing wanted) passes on both. alias_admission_test.cpp,
segmented_domains_test.cpp and darwin_import_lifetime_test.cpp get a trims_waiting stub only (no assertion changed).

Published on main 6c9edffe (mac/renderer-core): darwin_import_lifetime_test's
trims_waiting stub sits beside #491's sparse_wanted stub (main's #250).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 6aa7c2af0fb4e9d0dc57d6eeb8018aaac07ed234)
Main's #250 (sparse mirrors) makes retire_pool drop a retired pool from
sparse_wanted, the pools a binding found a sparse block with no memory in.
test_alias_admission, test_segmented_domains and test_admission_coverage cut
retire_pool out of vk_memory.cpp with the admission, so their fixtures did
not compile on main 6c9edffe ("use of undeclared identifier
'sparse_wanted'"). Each gets the stub darwin_import_lifetime_test already has
(#491). No assertion changes.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main's #59 (actor motion pairs a model's copies by place) takes a skinned
draw's pose as the draw read it (temporal_note_skinned_draw, ub_read), through
a host-visible stage, and copies the bones and origin from the game's memory
only for a draw whose pose was not read, or with no stage. actor_reads and
temporal_actor_demands (alias isolation's resolver for the upscaler's actor
motion) still demanded the bones and origin of every skinned draw: a
superset, so admissions on macOS could import or hand off for bytes nothing
binds.

pose_read_at_draw(s, k, staged) is now the one test, actor_motion's (with its
stage) and the resolver's (with the stage actor_motion takes next): the
resolver demands a draw's bones and origin only where actor_motion copies
them, and its vertices and indices always. With no stage made yet the
resolver demands the bones and origin too, since actor_motion's making of the
stage may fail. test_resolver_coverage holds both to that test.

Off alias isolation (Windows, Linux) the resolver does not run, and
actor_motion's test is the same expression as before.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(vk): test_split_ub_flush follows flush_ub_updates' end parameter
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
11d09b9abb
Main's #189 check reads flush_ub_updates by its signature. The admission (6e765c8d) gives it an end, the first update not
written, for a handoff in a dry-passed command buffer, defaulting to SIZE_MAX, so the check's anchor was not found and it
stopped with a ValueError. It now reads the new signature and still requires what it guarded: the writes start at
ub_first_of_recording and, with the default end every caller in maybe_split and finish_recording uses, reach the last
update (end = std::min(end, ub_updates.size())).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This PR needs the texel codec PR (#P21: texel_codec::Plan, which
texel_view_pieces takes) and the renderer-core PR (#P19, the base:
SegMem, gpu_span_bind, SpanLayout). Both are kept as they are; the
merge had no conflicts.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
feat(vk): the segmented consumers' frozen contract (C0)
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
ccc6b071c1
The interfaces the four segmented-consumer lanes (SegBind, SegXfer,
TexelDesc, NullShader; reports/2026-11-14-seg-consumer-plan.md section 3)
code against, frozen before they fork. Inert: no caller of anything new,
no SPIR-V, layout, push range or stream change, sizeof(Program) unchanged.

- vk_internal.h / vk_memory.cpp, SegMem's consumer interface:
  gpu_span_any() (a relaxed count of the live domains, stored in
  seg_commit; domains are kept for the run), gpu_span (gpu_buffer's
  flavour of gpu_span_bind: import buffers, no written marks, no mirror),
  SpanLayout::bind_stamp (gpu_bind_stamp(): a segment can be imported
  again with its cuts kept, so cut_gen alone never keys piece buffers),
  span_unused_piece's semantics ({null_storage, 0, 1}), and the SegArena
  API (SegArenaRef, seg_arena_take, seg_gather, seg_scatter; stub bodies
  after stream_alloc that give no room and log, SegBind's to fill).
- Review fixes in the same interface (renderer-core review P2-3, P3.2):
  every piece of gpu_span_bind carries its buffer's device address (the
  import's or the mirror's; 0 only with bufferDeviceAddress off), and its
  one-piece path stores the translated guest address, not a virtual one.
- vk_texture.h/.cpp: texel_view_pieces / texel_piece_views, stubs that
  return 1 and false (TexelDesc's to fill).
- vk_shader.h: Program::presence_words, in what was padding.
- Back end: PRESENCE_PUSH_OFFSET (88), Profile::emulate_null_descriptors,
  Bindings::presence / presence_read with the bit numbering rule.
- tests: test_resolver_coverage lists gpu_span_bind / gpu_span sites too
  (scoped to gpu/vk: gpu/render has an rx::gpu_span of its own), checks
  that gpu_span follows its own gpu_buffer, and splits SITES into
  SITES_BASE plus one block per lane. test_segmented_domains executes the
  contract (the count 0, 1, 2 -> 1 on a merge, 0; gpu_span's pieces;
  addresses with the fixture's gpu_buffer_bind returning none as the real
  one does; the bind stamp; a virtual address's guest). test_seg_shader
  accepts a base tree of this back end's generation and compares every
  module.
- docs/segmented-binding.md: the gate (every new behaviour Apple-only,
  other platforms byte-identical) and one heading per lane.

Conflict on this branch (not with main): tests/test_seg_shader.py's
docstring, against the array_pointer_calls paragraph #P20's
msl-arrayptr port added; both kept, this paragraph first.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 95fa06d4e6526601bd2e1bc88c2f8f5720855350)
The segmented binding PR needs the MoltenVK descriptor-set fixes (#P17
storage buffer tables, #P26 storage buffer lengths; #P28 read_write
textures, which it lands after): ensure_layout's layout_pushes decides
the push, and seg-bind's counts go through it. Their commits are kept as
they are; the merge had no conflicts.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The renderer's half of plan section 6 (reports/2026-11-14-seg-consumer-plan.md), on B2 08cccafb:

- vk_texture.cpp texel_buffer_view(t, v, storage, plan): the plan's view (the format's own, an alias, a typed
  access's declared format, or an R8/R16/R32 UINT carrier), whole-texel ranges at most maxTexelBufferElements of
  the view's elements, plan.zero as a cached 16-byte zero view; key (format, offset, range) with the backing as
  before, every view through one cache helper (texel_view_locked).
- texel_view_pieces / texel_piece_views (C0.4): cut at a segmented domain's cuts (span_segments::texel_pieces, in
  view elements) and, on Apple, at runs of maxTexelBufferElements; each piece view on its segment's buffer (gpu_span
  after gpu_buffer); a stale count, an unbound piece, a seam or a written seam refused loudly (the element absent).
- gpu/vk/null_dummies.h (new, pure): the stand-ins' tables by view type x class x multisample, storage format, and
  the texel zero view's format from the variant's texel answer. vk_texture.cpp null_dummies_make makes them once
  (1x1 zero images, views, four samplers, texel zero views; vkGetPhysicalDeviceImageFormatProperties first; one
  clear submit), only on Apple without nullDescriptor (make_dummies).
- vk_draw.cpp: texel_views with texel_plan / texel_plan_native, P piece views for a segmented variant's entry, the
  zero view of the declared format for an absent element; sampled and storage-image stand-ins, null samplers, the
  default sampler for a real view whose sampler failed; DescWriter presence bits (C0.8 numbering), the graphics
  words kept per thread, push_presence at offset 88.
- Non-Apple, and Apple with nullDescriptor: writes byte-identical to B2 (checked against the B2 cut).

Tests: test_texel_buffer_ownership (extended), test_texel_views_plan, test_null_dummies, test_null_descriptors,
test_null_layout_push (new), SITES_TEXEL; docs: texel-buffer-formats.md (new), texel-buffer-ownership.md,
segmented-binding.md TexelDesc.

Conflict on main: tests/test_texel_buffer_ownership.py's eviction cut
ends at imports_give_back's "(longshot) Default by OS and vendor"
comment, as main's #324 reworded it ("Off by default" before).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 36fe0a1359b31095fcb8d40d3c2f502a105d6b1b)
A failed case printed only a CalledProcessError; it now prints the fixture's own FAILED line and the case.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit cb8dea7c94ef0b0c002c9102d6c78c48bc778b7e)
On a segmented domain (macOS), vk_draw.cpp now binds storage descriptors across a segment's cut as piece arrays in the
segmented variant program_segmented makes, gathers uniform buffers and fixed-function inputs into a per-recording arena at
the pre-pass point, and fills C0's arena stubs. On Apple, unbound storage descriptors take a zero-length range of a new
64 KiB null_storage buffer (span_unused_piece set), and unbound uniform buffers take 64 KiB of zero_buffer, which loses
its STORAGE usage. Other systems keep every write, layout and push range as before.

- ensure_layout and the descriptor counts take piece counts from Program::seg; push range 88 + 4 * presence_words
- build_descriptors: single, table (k*P + j, prefix rule), bare/wide/shrink per piece; a refused array writes nothing
- uniform_range: gather queued (SegBuild), null_ubo on Apple
- draw()/dispatch(): seg_sig_of + program_segmented after the seam, always waited for; same_desc never reuses a
  gathered or stale piece set; vertex/index/indirect/mesh-args/dispatch-args gathers; index extent demanded (O9)
- pipeline_note and the compute notes skip segmented variants
- tests: test_seg_bind (new), resolver coverage SITES_BIND, and the fixtures and anchors the change moved

The next commits add test_seg_gathers, test_seg_notes, the desc_counts, push_state and vertex_bind
extensions and the docs section. (Reworded from the topic's "wip:" commit; the content is the topic's.)

Conflicts on main 6c9edffe: #252's compute-variant path in dispatch()
(program_cache_complete_ask, on_worker, made_how; compute_optimize at
the bind) meets this commit's rename of the pre-seam program to
`plain`. #252's lines are kept here as main has them, naming `p`; the
notes take SegBind's guard and #252's optimize and bind follow it.
They follow the rename in this PR's last commit, which vk_draw.cpp
needs to compile; tests/test_program_cache_one_variant.py the same.
tests/test_set_for_tables.py (#P28's read_write cases): CASES counts
both sides' cases.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit f40b77f71a47ee130cbe0df476d5e59f00fcef03)
draw()'s pre-pass point cleared only the gathers an earlier build had queued when the draw reused its descriptors. The build's
lost flag stayed: a build or signature that was refused (seg_lost: no arena room, a piece array that could not be written, a
descriptor in more than 255 pieces) left its own draw out, and then every later draw that reused the previous set while a domain
existed was left out too, with no log line of its own. The whole SegBuild is now reset there (its gathered/pieces/stamp fields are
read only at the bind of a fresh build). Apple-only in effect: off alias isolation no build is ever refused.

tests/test_seg_gathers.py's "stale" case fails without it.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit a38cf86cc766d3254539965265c9bf3d56123b87)
Plan 4.3 items 2, 3, 6 and 7 (reports/2026-11-14-seg-consumer-plan.md):
- tests/test_seg_gathers.py + tests/seg_gathers_test.cpp: the source's own cuts (RecordingRes and the arena with
  draw_begin_recording's reset and give-back, SegBuild, the size helpers, the bound-state caches, and draw()'s mesh-args
  rewrite, pre-pass gathers, vertex streams and indirect/indexed binds; dispatch()'s grid gather and binding) over a command
  recorder and SegProbe's domain. 76 checks: the order end_pass, barrier, copies, barrier, reader; vertex sized and WHOLE
  (clipped to the domain); direct indices; the indexed-indirect extent; records across a cut; native mesh records; the
  mesh-args rewrite per piece; a queued uniform gather; the 12-byte grid; count words in place and a straddling one left out;
  aligned arena offsets; arena made, grown, reset, given back, and full (a loud skip); nothing kept in kept_vb, vertex_set or
  last_ib; no stale refusal after a reusing draw. --base compares the common path's recorded commands with B2's cut; --json
  writes seg-gathers.json for SegBindProbe. Against B2 (--source) it builds and fails.
- tests/test_seg_notes.py: pipeline_note and dispatch()'s note lines cut out: a segmented variant makes no state_record,
  PipeNote or compute note; a plain one is noted as B2's cut is (--base).
- test_desc_counts.py, test_push_state.py, test_vertex_bind.py: --source, and SegBind's source rules (piece counts with
  seg_layout_count; the push range kPushSize + 4 * presence_words and uses_push; gathered streams and indices never reach
  vertex_bind_reuse, kept_vb, vertex_set or last_ib). Each fails against B2.
- docs/segmented-binding.md: the SegBind (L1) section.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit a0eccd31253378f8beb5963da4d4a6e5f340e670)
The segmented single storage array's case (plan 4.3 item 5) had no negative: the check always cut its own tree. With --source
<B2 archive> it fails as required ("moltenvk.segmented_single want=set got=push").

Conflict with #P28 (read_write textures; not with main): the docstring
keeps #P28's paragraph with this usage line, and main() cuts from
--source's vk_draw.cpp with #P28's read_write count (as in the
macOS integration, Integration 17).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 5824f6828e21cf810dcceaa6be425a9224b15994)
SegBind renamed draw()'s program for the bound state to `plain` (after the seam `p` is it or its segmented variant), so the
source rule "topo = pipeline_topology(p, topo);" no longer matched and the check failed in the full suite (a new failure
against B2's full run). The rule now reads `plain`; what it checks is unchanged.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 2e1eb52102ae7a889af7a0aa770f448833d36ebf)
ensure_layout, which the fixture compiles from vk_draw.cpp (kPushSize
to its end), now counts each descriptor through SegBind's
seg_layout_count and grows the push range by Program::presence_words.
test_set_for_lengths (mvk-sizes) was written after SegBind's base, so
its fixture had neither, and it no longer built after the merge of
SegBind into macos/integrated (a34be2da). It gets the same stubs
SegBind gave test_set_for_tables' fixture: presence_words = 0, and a
seg_layout_count that keeps every count (its modules are plain
variants). No assertion changed.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 071c628f14ec46d0c55b1e5ef1a2941040a362aa)
A guest range across a SegMem segment's cut is never one memory: gpu_buffer_bind
and gpu_buffer return it empty. Each SegXfer consumer's failure branch now binds
it piece by piece (gpu_span_bind, or gpu_span for the unswizzle's gpu_buffer
reader) after its operation's admission, and records the work per piece or
through the recording's SegArena (C0's declarations; SegBind fills the bodies):

- vk_replay.cpp: gpu_write as one vkCmdUpdateBuffer per piece (unaligned stays
  on the CPU); NOP_COPY_B2B as regions between the sides' pieces, one
  vkCmdCopyBuffer per buffer pair; NOP_CLEAR_BUF as one fill per piece.
- vk_xfer.cpp: B2T gathers the span into the arena and copies from it; T2B
  gathers in first (row padding keeps the guest's bytes), copies the image into
  the arena and scatters every byte back; bufferOffset a multiple of the texel
  block; stamp and texture_gpu_wrote over the span.
- vk_texture.cpp: the GPU unswizzle gathers its source into the arena in the
  upload command buffer, ahead of the dispatches, at the storage alignment.
- vk_temporal.cpp: the actor's bones and origin copied per piece, its vertices
  and indices gathered and read through the arena's device address.
- vk_draw.cpp: the frame dump, StatsQuery copies and mesh-indirect convert per
  piece; the mesh fallback's 8-byte indirect input gathered, by address.

No arena room is refused loudly (error log, counter), never silently. Every
path tests gpu_span_any() first: with no domain (all platforms but macOS under
alias isolation) every consumer records exactly what it did before, which the
new fixtures check against B2's recorded commands. The arithmetic is in the
new pure header gpu/vk/span_xfer.h.

Tests: tests/test_seg_xfer.py (+ seg_xfer_test.cpp), copy_b2t in
tests/test_vulkan_copy_span.py (+ copy_b2t_span_test.cpp), the gpu compile
fixture given SegMem's types, and SITES_XFER in test_resolver_coverage.py.
Docs: docs/segmented-binding.md § SegXfer.

Conflict on main 6c9edffe: vk_temporal.cpp actor_motion, #59 (actor
motion pairs copies by place): a pose read at its draw is copied from
the stage (placed), and only otherwise from the game's memory. The
piece-by-piece copy of a pose across a segment's cut goes in that
else branch, where the bones and origin are bound; the kept pose
takes #59's placed and place.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit db3c9bf0c7c9181001522991d2447321362b5724)
Semantic rework for main 6c9edffe, after the picks. SegBind renames the
program for the bound state to `plain` before the admission seam (after
it, `p` is that program or its segmented variant). Main's #252 added
the compute-variant path on those lines (program_cache_complete_ask,
on_worker, made_how), kept as main has it in the seg-bind pick, which
left vk_draw.cpp not compiling ("use of undeclared identifier 'p'", 7
lines). Those lines now name `plain`: the CAS and the ask, the job, the
cached pipeline, and the census and give-up after the 50 ms wait.

Behaviour is #252's: the plain program is the one read without its
code and made whole there. A segmented variant is translated, not read
from the cache, and compiled synchronously after the seam through
cached_compute, which takes #252's Intel gate itself (Windows Intel
only; segmented variants exist on macOS only). At the bind, #252's owed
optimized build and compute_optimized stay on `p`: a variant is never
built quick, so it binds its own pipeline.

The checks that read these lines follow:
test_program_cache_one_variant (its first-CAS, cached-pipeline and
complete anchors; its code_words check now looks for both names),
test_intel_compiler_gate (the ask's anchor), and test_seg_notes, whose
cut of the compute notes now ends where #252 queues the owed optimized
build (at the bind on a tree without it; its between() helper went with
the old cut). No check is weakened.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Fixture follow-up for main 6c9edffe. tests/test_seg_gathers.py cuts
draw_begin_recording into its fixture, and main's #162 (the recorder
thread's own slot resets, RX_VK_RECORDER_OWN) marks the descriptor pool
reset there with a rec::OwnSlot scope, so the fixture did not build
("use of undeclared identifier 'rec'"). It gets the stub the other
fixtures that cut that function have (draw_stream_visibility_test,
recording_admission_test): a scope that does nothing. No check changed.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Semantic rework for main 6c9edffe. Main's #59 (actor motion pairs
copies by place) takes a pose read at its draw through a stage, and
copies the bones and origin from the game's memory only otherwise;
#P19's rework (pose_read_at_draw, shared by actor_motion and
temporal_actor_demands) makes the resolver demand them only there. The
SegXfer pick puts the per-piece pose copy in that else branch.

- tests/seg_xfer_test.cpp: the fixture's State takes #59's members
  (Skin::pose and kNoPose, pose_bytes, poses_time, Pose::placed and
  place, pose_stages, pose_stage_at, poses_read, poses_copied) and the
  <cmath> and <unordered_set> actor_motion now needs; its skins keep
  kNoPose, so its cases still take the copy from the game's memory,
  across a cut as before.
- tests/test_seg_xfer.py cuts pose_read_at_draw into the fixture too
  (when the tree has it).
- tests/test_resolver_coverage.py: SITES_XFER's four actor_motion sites
  expect the resolver's loop as SITES_BASE's do since #P19's rework
  (from pose_read_at_draw's 2 or 0), and the bones' site the same
  decision in both functions.

28 of 28 SegXfer cases and 74 binding sites in 19 functions pass again;
no expectation was dropped.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(vk): SegXfer's gpu_write checks follow #329's ordered inline writes
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
b8579f1a70
Fixture follow-up for main 6c9edffe. Main's #329 (order overlapping
inline buffer updates) puts a full barrier before each vkCmdUpdateBuffer
in gpu_write, and tests that with test_inline_write_order, which cuts
gpu_write alone.

- tests/test_inline_write_order.py: SegXfer's span path in gpu_write
  calls gpu_span_any, gpu_span_bind and gpu_write_pieces; the fixture
  stubs them as they are off macOS and with no domain (gpu_span_any()
  false), so it tests #329's order as before. Its three variants keep
  their results (the barrier and the work count still caught).
- tests/test_seg_xfer.py: replay.gpu_write.common, recorded on the
  topic's base (08cccafb) without #329's barriers, is recorded again:
  the common path at this branch equals its base's (--base), on arm64
  and x86_64. The other eight common digests are unchanged.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With Profile::emulate_null_descriptors (make_profile: Apple, no
nullDescriptor) the back end keeps what a null descriptor gives while the
renderer binds real dummies. Off, every module is byte for byte B2's
(57 of 57, tests/test_seg_shader.py --base-tree).

- Storage guard: direct loads guarded inline by the U32 view's
  OpArrayLength; stores, the CAS store loop and every storage atomic
  through DontInline null_ssbo helpers; the miss log and block probe too.
- Segmented helpers measure every piece by the U32 view, the last one
  too; image buffer helpers bound their last piece and take a presence
  parameter.
- Presence bits: DefinePresence numbers every element, sizes the stage's
  member at offset 88 (ResolutionInfo or RenderAreaInfo, S32 lengths so
  no ArrayStride lands on a Function or Workgroup array); size and LOD
  queries, texel fetches, image reads, writes and atomics read them.
- vk_shader.cpp: the profile bit, the "nnul" stamp word, and
  Program::presence_words; vk_progcache.cpp: format 3 keeps it.
- Tests: spirv_run.h runs storage by the byte with bound ranges, atomics
  and push constants; seg_shader_ir.cpp checks the guards, presence
  members and reads, runs them on the CPU and writes the presence map;
  stamp, cache format and record head checks extended.

The next two commits document a gap image and image buffer arrays
already had and fix two source-cutting checks this one broke.
(Reworded from the topic's "wip:" commit; the content is the topic's.)

Conflicts with #P20's MslArrayPtr port (in this PR's base), not with
main: spirv_emit_context.cpp/.h and emit_spirv_memory.cpp, where
NullShader's guarded helpers meet the compare-exchange loops written out
in a piece (CasLoopHere, CasStoreHere). Here every helper writes its loop
out, the guarded one NullShader makes for a descriptor bound whole too
(.cas_loop = cas_loop on both of SegmentedStorageCall's returns;
WriteStorageByCasLoop takes StorageThroughHelper with #P20's body): valid,
and no pointer into a buffer array reaches a function. This PR's last
commit restores MslArrayPtr's bit-on half as macos/msl-arrayptr has it (a
descriptor bound whole calls the loop function with its block).
tests/test_program_cache_format.py: main's translator_files (#370) beside
--source; tests/test_seg_shader.py: NullShader's module check, then the
array-pointer check.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 0862a81f0f1b72edfbe01e2002433cd4e1783168)
DefineImages and DefineImageBuffers declare one image whatever the
descriptor's count, while Image() indexes the variable when the count is
above 1, so such a module fails spirv-val ("OpAccessChain reached
non-composite type"); B2 has it already, on every platform. The null_img*
and null_imgbuf* helpers take their element the same way and share it.
Found while extending the fixture; declaring the arrays changes every
platform's SPIR-V and is left to the owner.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 83e7e18b4fee477f1e6901871ad37efe8f23d5bf)
The full suite at the wip head failed two checks that B2 passes:
- test_vendor_translation: EmitImageFetch's path with fetch_bounds off must
  stay the translator's old one word for word (NVIDIA's path). The presence
  select had been added to that tail; a texel buffer's presence bit without
  FetchInBounds now takes its own branch before the fetch_bounds block, and
  the old tail is back as it was. Same instructions in the same order, so
  every module is byte for byte as before, with the bit on or off.
- test_native_float_atomic_add: its fixture's EmitContext stand-in lacked
  StorageThroughHelper, which the atomics now call. Added, as the real one
  with the profile bit off (the fixture's profile never sets it).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 478640436f4a5fb1cd81f3659fde7e86043deda4)
fix(vk): MslArrayPtr's NullShader half: a descriptor bound whole calls its loop function, and the loops' CPU runs
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 13s
CI / Workflow lint (actionlint) (pull_request) Successful in 21s
Third-party parts / As their notes say (pull_request) Successful in 30s
PR description / Description follows the template (pull_request_target) Successful in 4s
d7f7c82602
The rest of macos/msl-arrayptr's commit (bd396010), which #P20's port
left for this PR: #P20 writes the compare-exchange loops out in the
pieces; this adds what needs NullShader or tests/spirv_run.h.

- SegmentedCasLoop (emit_spirv_atomic.cpp) and WriteStorageByCasLoop
  (emit_spirv_memory.cpp): only a descriptor bound as pieces writes its
  loop out (CasLoopHere, CasStoreHere). NullShader's guarded helper for a
  descriptor bound whole calls the CasLoop function or
  write_storage_cas_loop_func with its block, as on macos/msl-arrayptr:
  that block is no element of a buffer array, so SPIRV-Cross's MSL can
  pass it. Until this commit that helper wrote its loop out as well.
- SegmentedStorageCall: the guarded helper makes the loop's variables
  only for pieces (.cas_loop = cas_loop && pieces != 0); the header's
  comment and SegHelper::cas_loop's place as there.
- tests/seg_shader_ir.cpp: the MslArrayPtr section (build_cas_run, the
  storage buffer table run, the msl-* modules and their CPU runs), called
  under NULL_SHADER; tests/spirv_run.h: uniform buffers, 64-bit integer
  scalars, GLSL.std.450 PackHalf2x16, UnpackHalf2x16, NMin and NMax;
  tests/test_seg_shader.py: --changed-segmented, segmented_module(), the
  *.cpurun comparison, the msl- prefix left out of an older generation's
  set, and array_pointer_calls where macos/msl-arrayptr has it.

With this commit the SPIR-V back end and those three test files are byte
for byte macos/integrated 7cd720da's (NullShader and bd396010 merged).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit bd396010887afab9520181009db1b56a4cc425e3)
SPIRV-Cross (MoltenVK 1.4.2's pin 6c09849f) prints an OpImageRead's result,
and a pure read helper's call, where the value is first used, and nothing
orders it against the same invocation's later writes and atomics on that
image: in MSL the read ran after them and returned the later value. TexOrder's
TO-01 probe proved it on the GPU for LS's plain output, with and without the
NullShader bit, on both CPU routes (reports/2026-11-14-tex-order.md).

- Profile::materialize_image_reads, set by make_profile on Apple for
  MoltenVK only, with its translator stamp word ("rord") only when on.
- emit_spirv_image.cpp InPlace: with the flag, every storage image and image
  buffer read's result (the plain read, the segmented read helper's call, a
  carrier codec's element reads) is stored to a Private variable at the read
  and loaded back; SPIRV-Cross prints a store where it stands. Private, since
  a Function variable must lead its function's first block; in the entry
  point's interface from SPIR-V 1.4. Off: no new ID, every module byte for
  byte as before.
- tests/seg_shader_ir.cpp: order-* modules with the flag; the read-in-place
  rule (every read and read helper call stored at once), their flag-off twins
  breaking it, CPU runs equal to the modules without the flag, and the flag
  alone changing nothing without image reads. test_seg_shader.py: order-
  modules required; a base tree without the flag builds them with
  TEX_ORDER_BASE and they are not compared; --source against it fails.
- tests/spirv_run.h runs Private variables (fresh per invocation) and
  OpMemoryBarrier (a no-op one invocation at a time).
- docs/segmented-binding.md: a TexOrder section.

Conflicts on mac/null-shader (#P33), with MslArrayPtr's tests that #P33's
last commit carries, not with main; resolved as macos/integrated did
(Integration 22, b08694d8): tests/seg_shader_ir.cpp keeps both sections
and both calls (msl_arrayptr, then tex_read_order); tests/spirv_run.h
keeps both storage classes (Uniform, then Private); tests/test_seg_shader.py
keeps both docstring paragraphs and presence checks, leaves 'msl-' and
'order-' out of an older generation's set, and compares with a base in one
loop (an order- module skipped for a base without the flag, identical
counts as the same, a different segmented one listed under
--changed-segmented, anything else fails).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit a9aad5bcd6f4cce3d9fcff8b231da9628e7da4b3)
The check cuts make_profile out of vk_shader.cpp into a fixture with its own
stand-ins for what it reads; make_profile now reads VK_DRIVER_ID_MOLTENVK on
Apple (Profile::materialize_image_reads), which the stand-in lacked, so the
fixture did not compile (full suite at a9aad5bc). The real enum's value, as
the stand-in has VK_DRIVER_ID_AMD_PROPRIETARY's.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 88a18d33fec89732c69d89c401303134eb7c7937)
fix(vk): TexOrder: an image memory barrier before each image write and atomic on MoltenVK
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 15s
CI / Workflow lint (actionlint) (pull_request) Successful in 18s
Third-party parts / As their notes say (pull_request) Successful in 29s
PR description / Description follows the template (pull_request_target) Successful in 4s
242ff050a0
TO-02 (arm64, reports/2026-11-14-tex-order.md §8) ran the stored reads: in MSL
every read now stands before the atomic or write that follows it, but a
texture_buffer read stored before a native atomic_fetch_add on its texel still
returned the atomic's result (5 of 34 cases, whole image buffers). Metal orders
a thread's own accesses to a read_write texture only through a fence, and
SPIRV-Cross prints fence() only before reads. TO-02's kernels with a
DeviceMemoryBarrier between the read and the atomic all matched.

- emit_spirv_image.cpp OrderImageWrite: with Profile::materialize_image_reads,
  OpMemoryBarrier(Device, AcquireRelease|ImageMemory) right before each storage
  image and image buffer write (plain, segmented and guarded helper calls, a
  carrier codec's write function call) and, in emit_spirv_image_atomic.cpp,
  each atomic. SPIRV-Cross (MSL 3.2+) prints atomic_thread_fence(
  mem_flags::mem_texture, memory_order_seq_cst, thread_scope_device): texture
  memory only. Plain reads cost nothing. Off: nothing, no new ID.
- tests/seg_shader_ir.cpp writes_unfenced: every image write, image atomic and
  write or atomic helper call, outside such helpers, has an ImageMemory barrier
  before it in its block; the order-* modules keep it (36 of 36), their flag-off
  twins break it, and the rule fails against 88a18d33.
- docs/segmented-binding.md, profile.h: the barrier.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 5440c34eefa70ebb95c7f7f039e19fa1efb563cb)
Metal's compiler refuses source nesting more than 256 brackets, and a
Mac start lost 4 variants of a compute program to it
(program_source:2537:1025: bracket nesting level exceeded maximum of
256). SPIRV-Cross 6c09849f, MoltenVK 1.4.2's translator, prints a value
used once inside its user's expression; it stops after 64 inherited
dependencies, but a pure call's result inherits none, so a chain
through calls (the back end's helpers among them) nests without bound.

Profile::bound_forwarded_depth, set by make_profile on MoltenVK only
and given a word in the translator stamp only then, runs
BoundForwardedDepth (backend/spirv/msl_depth.cpp) on the finished
module: a model of SPIRV-Cross's forwarding (a value read by two
instructions is a temporary, a phi, parameter or variable a name)
gives each value a depth, and a scalar or vector value whose user would
nest deeper than 32 instructions is stored to a Function variable of
its own and loaded back under its old id. No emit function changes;
with the flag off the SPIR-V is byte for byte as before.

tests/test_msl_depth.py builds tests/msl_depth_ir.cpp (four deep
single-use chains: texel fetches bound whole and as pieces, integer
arithmetic, calls of a pure function; each off and on, run on the CPU
against the reference) and tests/seg_shader_ir.cpp with --msl-depth,
validates every module and checks each one's depth from SPIR-V's
grammar against the bound. Against a7711f3c it fails.

Conflicts on mac/tex-read-order (#P35), resolved as macos/integrated
did (Integration 23, 6b2119a5): vk_shader.cpp translator_stamp keeps
the three MoltenVK words in the order "nnul", "rord", "msld", each in
its own block; tests/seg_shader_ir.cpp's argv[2] is --msl-depth by its
exact text, or else NullShader's presence programs (both forms in the
usage line). gpu/vk/VULKAN.md: main's pipeline cache line (#321) kept,
this change's paragraph after it (the worker-cache sentences before it
on macos/msl-depth are #P22's, not in this PR).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 6dcd6a9e68fbe9d6e878e754c383631053007beb)
test(vk): the subgroup profile fixture knows MoltenVK's driver id
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 14s
CI / Workflow lint (actionlint) (pull_request) Successful in 23s
Third-party parts / As their notes say (pull_request) Successful in 34s
PR description / Description follows the template (pull_request_target) Successful in 5s
1c21f295a9
make_profile now reads dev.driver_id against VK_DRIVER_ID_MOLTENVK for
Profile::bound_forwarded_depth, and test_amd_subgroup_profiles compiles
make_profile against its own stub constants: it gains the constant (14,
as the Vulkan headers have it), a MoltenVK case, and checks that the
bound is on for MoltenVK only and carried into the pinned and graphics
profiles.

Conflict on mac/tex-read-order (#P35), whose TexOrder commit added the
same constant: one stand-in, its comment naming both readers (as
macos/integrated 6b2119a5).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 6985b6385334a342efca1ab915e7e14fdc0b4318)
Metal has SIMD-group functions and lane ids in kernel and fragment functions only (MSL 4.1 section 6.10), MoltenVK 1.4.2 lists
fragment, compute and tessellation control in subgroupSupportedStages, and SPIRV-Cross 6c09849f refuses
SubgroupLocalInvocationId in a vertex function ("Subgroup builtins are not available in this type of function."). Run #16 lost
graphics pipelines to it in the precompile.

On MoltenVK only, make_profile clears Profile::supported_subgroup_stages for the stages Metal runs as vertex functions (VertexA,
VertexB, tessellation evaluation, geometry) that the device does not list, and the translator stamp gets a word for it then. The
back end's path for such stages is each invocation as lane 0 of a 32-lane warp whose other lanes are not active: the lane masks
of lane 0 (SR_GTMASK 0xfffffffe and SR_GEMASK 0xffffffff, not 0 and 1), SHFL.UP/DOWN/BFLY's bounds from lane 0 (not always out
of bounds), and no subgroup barrier. Every other driver keeps every stage: its SPIR-V is byte for byte as before.

tests/test_subgroup_vertex.py builds make_profile against stub devices and the back end with tests/subgroup_vertex_ir.cpp: 19
warp operations in vertex programs and 5 in tessellation evaluation ones, each emitted native and for MoltenVK, run on the CPU
(tests/spirv_run.h now runs vertex inputs, outputs and a subgroup of one active lane) against the guest's lane 0 alone (PTX
ISA's shfl, vote and lane masks), every SHFL b, clamp and segmentation mask; no subgroup construct may be left in a MoltenVK
module of a vertex-side stage. reports/2026-11-14-subgroup-vertex.md.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit a878cef81d0f98c9517e79d51c29cbb7ae503ba6)
--base-tree compares only the base tree's native modules; its fixture's own checks cannot hold there (MoltenVK is every stage)
and are no longer printed as FAIL lines. A MoltenVK module that still reads another invocation is reported once, as the
subgroup operations it runs, not again as an undefined read the model does not expect.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit c1d4768368a62bc3c8dd329d74371a3a12c6eb69)
test(vk): test_subgroup_vertex: SPIRV-Cross drops a subgroup barrier in a vertex function
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 2s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 16s
3b7e95a8d1
SPIRV-Cross 6c09849f's emit_barrier prints no barrier outside compute, task, mesh and tessellation control functions
(spirv_msl.cpp 11000-11008), so a vertex module whose only subgroup construct is OpControlBarrier at subgroup scope is
converted and made, not refused by Metal. Only the printed verdict changes; the checks do not.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit b53d8b0f9d48d17b9828a3042be01ddb75cedb49)
Metal compiles of the biggest compute modules take 17-20 s each (some minutes,
and crashed MTLCompilerService). With RX_VK_SPV_PROFILE=1, each module of a
program translated in the run that is over RX_VK_SPV_PROFILE_MIN SPIR-V words
(100000) gets one log line, at most 200 a run (numbered; the cap's line once),
with the slow compile line's kind, key, variant and stage:
- the entry point with every call written out (SPIRV-Cross 6c09849f prints
  every function static inline always_inline), split by origin: the entry
  point, NullShader's null_* helpers, the seg_* helpers, the texel_* codec
  functions and the rest, with the heaviest callee;
- what each Apple-only feature adds (nnul, rord, msld, txcd, seg), in SPIR-V
  words and written-out words: the same IR emitted again without it
  (spv_profiled::EmitSPIRV), against a second emission with everything on;
- the largest function, the entry point and the instructions by kind.

gpu/vk/spirv_profile.h is plain code over the words and templates over the
translator's types. Unset, every stage is emitted by the one EmitSPIRV call it
had, nothing is walked or printed, and the switch is not in translator_stamp's
list. vk_shader.cpp changes, so LONGSHOT_SHADER_STAMP changes with this commit.

tests/test_spv_profile.py: tests/seg_shader_ir.cpp's synthetic programs through
the real back end, counts checked against an independent walk, each feature's
re-emitted measure equal to fresh emissions with and without it, the off path
byte for byte, the line's width and the cap. Fails on 0155d2f2.

Picked onto main 6c9edffe's line (mac/msl-depth, which has the features this
measures): one conflict, in docs/development.md's switch table, where this
change's base had the precompile hand-off's three rows (RX_VK_PRECOMPILE_STALL_S,
RX_VK_COMPILER_SERVICE, RX_VK_SLOW_COMPILE_S) before its own. Only the two
RX_VK_SPV_PROFILE rows are this change's and only they are added, after
RX_VK_PRECOMPILE; this branch's end commit words them for a tree without that
hand-off.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit a8a725320750d70e418b2508f844ccbac11d336e)
The measures are kept until vk_shader.cpp spv_profile_lines prints the line
(the store's comment still named vk_progcache.cpp, an earlier place). Two long
lines wrapped. No code change.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 1a50b01ba4628e938a80eb3fdd76f9cc954139fa)
docs(vk): RX_VK_SPV_PROFILE's text names the slow compile line as the precompile hand-off's
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 15s
d0a38783bc
RX_VK_SPV_PROFILE was made on a tree with the precompile hand-off, whose
slow compile line (vk_draw.cpp compile_watch_end, compute_tag,
graphics_tag; RX_VK_SLOW_COMPILE_S) the profile's line copies the kind,
key, variant and stage of. Main 6c9edffe has neither the hand-off nor that
line. The switch table's row now says "for Metal's slow compiles" and "the
program's kind, key, variant and stage" (it read "for the slow compiles
above", a row this tree has not), and the three comments that name the
line's functions say they are the hand-off's, true with it and without.
No code change.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Carries the RX_VK_SPV_PROFILE pull request's commits (mac/spv-size
d0a38783, on mac/msl-depth like this branch's start): GmLoop's fixture,
tests/gm_loop_ir.cpp, counts each module's written-out words with
gpu/vk/spirv_profile.h's count, which that pull request adds.
mac/subgroup-vertex (this branch's start, for the make_profile stub
tests/test_subgroup_vertex.py gets here) and mac/spv-size merge without a
conflict.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
SPIRV-Cross's MSL and Mesa's NIR inline every function, so each global access
of the culling programs carried its fallback with every table entry written
out: compute modules of 100k-560k SPIR-V words that Metal's compiler service
compiled for minutes or failed in. Profile::search_tables_in_loops (set by
make_profile on Apple only, for MoltenVK and KosmicKrisp with
shaderStorageBufferArrayDynamicIndexing; RX_VK_GM_LOOP=0 off) loops each table
in every fallback, the 8/16-bit stores' and float adds' too, which Intel's
global_table_loop leaves written out. The program cache's stamp gets a "gmlp"
word only when it is on; every other GPU's SPIR-V is unchanged.

tests/test_gm_loop.py builds seg_shader_ir.cpp's table programs and big
synthetic ones in the written-out, Intel, flag and both forms: spirv-val, loop
counts, CPU runs equal, the flag alone changing no program without a table,
and (--base) the off forms byte for byte against an older tree. spirv_run.h
runs OpAtomicFAddEXT.

Picked onto main 6c9edffe's line (mac/subgroup-vertex with mac/spv-size
merged): one conflict, in docs/development.md's switch table, where main
has rows this change's base had not (RX_VK_NO_INTEL_SERIAL,
RX_VK_COMPUTE_QUICK_FIRST, the two HOS_VIDEO_ rows, main's
RX_VK_DRIVER_FAULTS wording). Main's rows are kept and only the
RX_VK_GM_LOOP row is this change's ("Intel's Mesa driver", as
vk_coopmat.cpp's table_loop_wanted says on main too).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 94ea1c19bb6c6f7e542d4bc86a798ef8b8bda10e)
GL-01's route measures each pipeline's creation; a written-out compile that
runs past the route's time or ends in a compiler-service failure now leaves
the loop forms measured.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit f55ed6698a0247d5a0fb4c2732fdb2eb7a794da9)
test(vk): stub table_loops_on where make_profile is compiled cut out
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 2s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 17s
be4a138755
test_amd_subgroup_profiles.py and test_subgroup_vertex.py compile
make_profile's body with stubs; on macOS its Apple block now asks
table_loops_on (GmLoop).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 81254a3084540a5e3d28c2f5b5cd2f0823f773bd)
feat(vk): MoltenVK zeroes local memory at computed offsets by a loop, not a written-out initializer
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 16s
CI / Workflow lint (actionlint) (pull_request) Successful in 24s
Third-party parts / As their notes say (pull_request) Successful in 31s
PR description / Description follows the template (pull_request_target) Successful in 17s
bb02ac38e5
LS gives a program's local memory (LDL/STL) one Private array of the
declared size with an OpConstantNull initializer. SPIRV-Cross, MoltenVK's
translator, writes that initializer out element by element, twice: 28 KB
of Metal source for 4 KiB a thread, 367 KB for 64 KiB, 1.4 MB for
256 KiB (the SDK's spirv-cross on the new fixture's modules). LM-01
created a trivial 64 KiB pipeline in 7.5 s.

Profile::zero_local_memory_by_loop, set by make_profile on MoltenVK only
(RX_VK_LOCAL_ZERO_LOOP=0 off), drops the initializer of local memory the
program indexes at a computed word, and DefineMain opens main with a
block of its own that zeroes every word in a loop, then branches to the
program's first block. Nothing reads local memory before that, so every
word is 0 where the program starts, as with the initializer. The same
Metal source is now 6.3 KB from 64 B to 256 KiB. Local memory indexed only at
constant words keeps its initializer, which Metal can scalarize with the
array; EDEN_NX2_LOCAL_ZERO=0 still zeroes neither way. The program
cache's stamp gets an "lzlp" word only when the flag is on; every other
GPU's SPIR-V is unchanged.

tests/test_local_zero.py builds tests/local_zero_ir.cpp: LDL/STL as the
frontend translates them (6 B to 256 KiB; computed and constant offsets;
8/16/32/64/128-bit; a load past a 6-byte size) in compute, vertex and
fragment stages and the mesh fallback's stages, off and on, in the
renderer and Mac profiles. Every module passes spirv-val. Every CPU run
(spirv_run.h, now with a pattern in Private words without an initializer)
equals a model of the window. The flag alone changes no program without
local memory, and none indexed at constant words. With
EDEN_NX2_LOCAL_ZERO=0 every read before a write sees the pattern (the
check of the check). --base compares the off forms with d6a89208 byte for
byte. Three fixtures that cut make_profile out get the switch's stand-in;
test_amd_subgroup_profiles checks the flag in every stage's profile.

Picked onto main 6c9edffe's line (mac/gm-loop), two conflicts, both in
fixtures this line has differently: tests/test_amd_subgroup_profiles.py
takes only this change's block (the TexOrder/KosmicKrisp asserts beside
it on macos/integrated are KosmicKrisp's follow-up, not on this line), and
tests/test_vendor_fixes.py keeps its own text: its Apple drivers' fixture,
where this change gave make_profile's cut a local_zero_loop_on stand-in,
is KosmicKrisp's follow-up's and is not on this line. So two fixtures
that cut make_profile out get the stand-in here, not three; the third
needs it where the two meet.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 560923891130567562a99cce6fb3b804ef0f268b)
mooki force-pushed mac/local-mem-buffer from bb02ac38e5
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 16s
CI / Workflow lint (actionlint) (pull_request) Successful in 24s
Third-party parts / As their notes say (pull_request) Successful in 31s
PR description / Description follows the template (pull_request_target) Successful in 17s
to 8f6597c005
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 5s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 5s
Website / Website on Cloudflare (pull_request_target) Has been skipped
2026-11-15 11:54:20 +00:00
Compare
mooki force-pushed mac/local-mem-buffer from 8f6597c005
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 5s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 5s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 5s
Website / Website on Cloudflare (pull_request_target) Has been skipped
to a526c34c9f
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 6s
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 11s
2026-11-15 15:05:00 +00:00
Compare
Author
Member

Folded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)

Folded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)
mooki closed this pull request 2026-11-15 18:08:14 +00:00
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 6s
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 7s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 11s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!697
No description provided.