WIP: fix(macos): Metal's shader cache folder asked at the host's start, before SDL's window #786

Closed
mooki wants to merge 111 commits from mooki/Longshot:mac/windowed-metal-cache into main
Member

Type: fix · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (MoltenVK main 5189f8de with the argbuf-slots patch), game-free on arm64 and x86_64 under Rosetta; GPU probe WC-01 on arm64 · Needs: macOS, Apple silicon (a windowed start with the switch set); Windows and Linux, any GPU (build and start)

Why

RX_VK_METAL_CACHE_DIR (macOS only, off by default, #653) asks Metal to keep its shader cache in a folder the user names
instead of its own per-user folder. Metal fixes that folder the first time anything in the process uses it, and in a windowed
start the renderer asked only after SDL had started its video, loaded the Vulkan library and made the window: by then the
folder was fixed, and the switch was lost (vk: RX_VK_METAL_CACHE_DIR NOT applied: …). Players are not affected: nothing
sets the switch for them, and Metal's own folder, which is writable outside a sandbox, caches a player's windowed start as it
does a windowless one. It matters to whoever sets the switch, as the Mac's test runner does: its sandbox denies Metal's
default folder, so the first windowed run (#35) cached nothing anywhere and its precompile took 1,464 s against 391 s for the
windowless run before it (#32). This PR asks Metal for the folder in main(), right after longshot_env.txt and the Vulkan
driver's choice, before SDL, the Vulkan library or any other thread, on macOS only. It is part of the macOS port. See #420.

How to reproduce

Seen on: macOS 26.7.1, Apple M4 Max, MoltenVK main 5189f8de with the argbuf-slots patch; main does not start on macOS
yet: seen on the macOS port's integration build 3f872b0c (run #35), which carries the open macOS PRs
Settings and switches: RX_VK_METAL_CACHE_DIR=<an existing folder> (the runner sets it), windowed
Steps: start the game in its window with the switch set, and read the Metal cache line after the device is made.
Expected: vk: Metal shader cache: <the folder> (MTLGetShaderCachePath, as RX_VK_METAL_CACHE_DIR asked), and the next
start warm from it.
Actual: run #35: vk: RX_VK_METAL_CACHE_DIR NOT applied: Metal's shader cache is /var/folders/<…>/C//com.apple.metal, not …/cache/metal (Metal chose its folder before the switch was applied); nothing cached (reports/2026-11-15-windowed-metal-cache.md
§1-2).

What changed

Only where and when the folder is asked for changes; the logic and its log lines are the same. Unset or empty, nothing is
loaded, called or logged, as before. Windows and Linux compile the same text as before.

  • host, host/hos_main.c main(): hos_darwin_metal_cache_ask() inside the macOS block, right after
    hos_darwin_vulkan_choose_driver(): after longshot_env.txt (which may set the switch), before hos_display_init (SDL),
    the game's thread, the window and anything that loads the Vulkan library. --prepare-shaders and headless starts pass the same
    point.
  • host, host/posix/darwin_metal_cache.h (new; gpu/vk/metal_cache.h moved with git mv and written in C): the checks, the
    dlopen/dlsym lookups, the CFString handling and every log string as they were; the path a 1,024-byte array (at most
    1,023 bytes, as before).
  • host, host/posix/darwin_vulkan.{c,h}: hos_darwin_metal_cache_ask() keeps what it asked; hos_darwin_metal_cache_check()
    logs the result on the renderer's thread, which starts after the ask.
  • ABI, gpu/vk/vk_api.h, host/rx_backend.c: a new macOS MkwVkHost slot, metal_cache_check, appended after #686's
    guest_backing_generation: Apple host API version 26 (#686 took 25), offset 400, size 408 (5c37ec10).
  • renderer, gpu/vk/vk_main.cpp: load_global no longer asks; mkwvk_init calls host->metal_cache_check() where
    metal_cache::check was, after create_device and before metal_archive::begin. metal_archive.h's comment names the new
    header.
  • docs: docs/development.md, the RX_VK_METAL_CACHE_DIR row says where the game asks and why.
  • tests: tests/test_metal_cache_dir.py compiles the host's real ask and check (cut from darwin_vulkan.c) and the header
    (also as C11 with -Werror) against made-up Metal and CoreFoundation calls, and checks main()'s order, the renderer's check
    after create_device, the macOS-only slot (version 26) and its wiring; tests/display_startup_failure_test.cpp runs the
    real main() and requires exactly one ask, after longshot_env.txt and the driver's choice, before any SDL load or init,
    window, thread or offscreen start; the ABI fixtures (test_vk_darwin_admission.py, darwin_renderer_startup_test.cpp) take
    the API 26 layout and call the wired slot; test_darwin_renderer.py and test_metal_archive.py follow the moved check.

Measurements

Verdict: fixed where measured. Probe WC-01 replayed a windowed start's own steps (SDL's video, MoltenVK and SDL's Vulkan
library, the window, its events, then the device and two pipelines) in a sandbox shaped like the runner's, with this PR's
real host ask and check: asked first, as main() now asks, Metal took the folder and wrote its cache there; asked where the
renderer asked in #35, after the window, it did not. Asked after SDL's video start or after the Vulkan library it still took
it: SDL's window creation is the step that fixes Metal's folder. Run #35's os_log agrees (the renderer's thread was the first
to open Metal's cache files, after the main thread's window). For whoever sets the switch, a windowed start should then be as
warm as a windowless one: the start's time is in the table.

Baseline: main does not start on macOS. Both "before" columns are the integration build 3f872b0c (I38), arm64, without this
change: #35 windowed, #32 windowless (the switch applied there), each with the runner's sandbox and the switch set.

Hardware: macOS 26.7.1 (25G241), Apple M4 Max (40-core GPU), 16-core CPU, 64 GB
Scene: a start from the field-dawn save (runs #32, #35): the preparation, the precompile, the title
Method: the Metal cache line of the game log, and the run's Metal and libCoreFSCache os_log lines counted by text (log show
over #35's pid, 01:06:43-01:36:00; #32's as the RosettaCache lane counted them); one run each. WC-01: six routes, one process
each, the real hos_darwin_metal_cache_ask()/_check() and darwin_metal_cache.h (blob-identical at this head) made at each
route's point, MTLGetShaderCachePath read after the device and the route's folder listed; released once on arm64 (05:02 CST)
How to reproduce: a windowed start with RX_VK_METAL_CACHE_DIR set, reading vk: Metal shader cache / NOT applied; WC-01's
build and run commands (reports/2026-11-15-windowed-metal-cache.md §4)

Before: integration @ 3f872b0c (run #35, windowed) Before: integration @ 3f872b0c (run #32, windowless) After: integration @ 2b4b7b8e with this PR (run #36, windowed)
the switch's folder not applied: Metal's default folder, denied by the sandbox applied applied, asked for before SDL (log line 2, ahead of sdl: SDL3 at line 3)
pipelines found in Metal's cache 30 s into the precompile 1,379 30,977 37,668
precompile 1,464 s 391 s 346 s (62.9 s of it translating afresh: the stamp changed)
start to hand-off 24 m 28 s 9 m 30 s 6 m 53 s
WC-01 route (ask made …) applied Metal's folder files written there
windowless (first; no SDL: #32's order) yes functions and libraries cache, 5 files
fixed (first, then SDL and the window: this PR's order) yes 5 files
after-init (after SDL's video start) yes 5 files
after-vklib (after MoltenVK's and SDL's Vulkan library load) yes 5 files
after-window (after SDL_CreateWindow) no: /var/folders/<…>//com.apple.metal none
today (after the window and 1 s of events: #35's order) no: /var/folders/<…>//com.apple.metal none
#35 L274: vk: RX_VK_METAL_CACHE_DIR NOT applied: Metal's shader cache is /var/folders/<redacted>/C//com.apple.metal, not …/cache/metal (Metal chose its folder before the switch was applied)
WC-01 reports/WindowedCache-files/probe/runs-01/result.json (sha256 52524f95b04516267adf66ac07be6006b0a0da284a4cd8c7f3741a7d34748975): fix_proven true, first_touch_after "after-window", crash_reports_new [], every route exit 0 with 2 pipelines made

Reports: reports/2026-11-15-windowed-metal-cache.md (the order of a windowed start, what a player's start caches, the fix,
WC-01), reports/2026-11-15-mac-startup-35-windowed.md, reports/2026-11-14-mac-startup-32.md.

Flags and settings

None new. One switch is asked earlier: RX_VK_METAL_CACHE_DIR, off by default. The diff against main also shows the switches
of the PRs it stacks on (#667's, #686's and #654's stacks), theirs to list: HOS_KEEP_FREED_PAGES, HOS_ZERO_ALL_PAGES,
LONGSHOT_DARWIN_ARCHIVE_RECEIPT, LONGSHOT_DARWIN_ARCHIVE_RECEIPT_SHA256, LONGSHOT_FFMPEG, LONGSHOT_FSR,
LONGSHOT_KOSMICKRISP, LONGSHOT_MOLTENVK, LONGSHOT_NGX_SDK, LONGSHOT_SDL3, LONGSHOT_VULKAN_LOADER, RX_ARCH,
RX_NVN_WATCH_AFTER, RX_NVN_WATCH_ALL, RX_NVN_WATCH_TEXELS_SRC, RX_VK_ALONE_TRIES, RX_VK_COMPILER_SERVICE,
RX_VK_COMPILE_QOS, RX_VK_DLSS_ACTORS, RX_VK_DRIVER, RX_VK_GIANT_COMPILES, RX_VK_GIANT_HOLD_PRESENTS,
RX_VK_GIANT_HOLD_S, RX_VK_GIANT_WORDS, RX_VK_GPU_DUMP, RX_VK_LIBRARY, RX_VK_MESH_IND_COUNT_WORD,
RX_VK_METAL_ARCHIVE_DIR, RX_VK_NO_UPSCALE_STUB, RX_VK_ONLY, RX_VK_SKIP_LOG, RX_VK_SKIP_PROG, RX_VK_SLOW_COMPILE_S,
RX_VK_WORKER_CACHES.

Flag What it is Before this PR After this PR Effect Helps / doesn't help / untested Recommendation
RX_VK_METAL_CACHE_DIR=<folder> the folder Metal keeps its shader cache in (macOS only) unset; set, asked in the renderer's load_global: lost in a windowed start unset; set, asked in main() before SDL: applied in windowed and windowless starts a windowed start that sets it uses its folder helps: the runner's windowed starts (its sandbox denies Metal's default folder); doesn't matter for players (unset, Metal's own folder is warm); untested in the game off by default, as it ships

Testing

Tested: on macOS 26.7.1, Apple M4 Max:

  • At this head (main e090cc48, #667 with #686 and #654 merged, this PR), every run under the shared check slot with a fresh
    HOME and TMPDIR (reports/PubWave-files/PubLate5/StartTrio/proof/wmc-*):
    • arm64 and x86_64 (Rosetta), exit 0 on each: test_metal_cache_dir (ok: source: asked in main() after longshot_env.txt and the driver's choice, before SDL's display and any thread, macOS only; …), test_display_startup_failure (11 cases, 0 failed), test_vk_darwin_admission (Apple version 26, size 408, typed callbacks at 360/368/376/384/392/400),
      test_darwin_renderer (Apple26 typed host callback wiring, both arches in one run), test_metal_archive.
    • On this PR's base 425fba24 with this PR's test files, exit 1 on each: host/posix/darwin_metal_cache.h is missing;
      11 cases, 11 failed (Metal's shader cache folder not asked once at startup); FAIL: Apple host API version is 26;
      no member named 'metal_cache_check' in 'MkwVkHost'; test_metal_archive's source check not finding
      host->metal_cache_check();.
    • Main's test_crash_record source_contract() alone (hos_main.c changes): passed. The bases' own checks on the merged
      tree: test_darwin_guest_memory (both), test_vk_darwin_sdl, test_vendor_fixes (arm64, x86_64), test_thread_stack (both),
      test_loading_stall, test_import_hold (arm64): exit 0.
    • Compiled whole for arm64 and x86_64: gpu/vk/vk_main.cpp and the renderer files the base's merges combine (vk_draw.cpp,
      vk_memory.cpp, vk_replay.cpp, vk_texture.cpp) with the tree's own renderer flags, 10 of 10; host/hos_main.c,
      host/posix/darwin_vulkan.c and host/rx_backend.c with the macOS host flags (#490, #479, #476, #492 merged in a scratch
      tree only), 6 of 6; and host/posix/win32_posix.c (no change of this PR's, but new to all three bases: the merges combine
      #686's and #654's), 2 of 2 with #490's side of its include hunk, as for #491 and #658 before (a scratch merge that keeps the
      branch's side drops #490's Mach includes) (…/StartTrio/compile/windowed-metal-cache-own/, …-host/).
  • The topic as written (macos/windowed-metal-cache c77008a8, API 25, on the integration branch 79e2e006): the two order
    checks failed on 79e2e006 (darwin_metal_cache.h is missing; 11 of 11 startup cases not asked once at startup) and on 4
    mutations (the ask after hos_display_init, the check before create_device, the string released before
    MTLSetShaderCachePath, the slot unwired); 8 of 8 whole-file compiles (hos_main.c, rx_backend.c, darwin_vulkan.c,
    vk_main.cpp, arm64 and x86_64) with 0 warnings; the object link check 0 failures; full suite 585 passed, 1 failed, 16
    skipped, by name the base's (test_menu_i18n) (reports/WindowedCache-files/).
  • Off Apple (unifdef -U__APPLE__, base against head): every changed file's text is the base's (metal_archive.h in a comment);
    metal_cache.h (Apple-only) is gone and darwin_metal_cache.h is new, neither included off Apple
    (reports/PubWave-files/PubLate5/StartTrio/offapple-windowed-metal-cache.txt).
  • GPU probe WC-01 (frozen packet, review eda365d7…, build 4f53e8b5…; released once on arm64 by the parent, 05:02 CST):
    reports/WindowedCache-files/probe/runs-01/result.json (sha256
    52524f95b04516267adf66ac07be6006b0a0da284a4cd8c7f3741a7d34748975): fix_proven true (the windowless control and this PR's
    order applied, with Metal's cache files in their folders; #35's order not applied, its folder empty), first_touch_after
    after-window (SDL's window creation fixes the folder), no new crash report. Its host code is this head's: darwin_metal_cache.h
    and darwin_vulkan.c are blob-identical at c77008a8, which it was built from, and here.

Not tested: Rosetta in the game (WC-01 and run #36 are arm64); Windows and Linux: not built there (their text is the base's);
its test request covers them. A windowed game start on the integration carrying this PR, run #36, found Metal's cache warm, as
in the table. WC-01 has passed, so no probe holds this PR in WIP any more; the Testing box waits for the test requests.

Must be tested:

  • macOS, Apple silicon, on the rebased macOS stack: a windowed start with the runner (the switch set, a folder warm from a
    start of the same tag length), expecting the ask line right after vk: Vulkan driver:, then vk: Metal shader cache: …/cache/metal (MTLGetShaderCachePath, as RX_VK_METAL_CACHE_DIR asked) after the device, and a precompile like #32's (by hand: the parent's
    game run)
  • Windows and Linux, any GPU: build this PR (head 5c37ec10, it carries #667, #686, #654 and what they carry) and #686's head the
    same way and start each to the title, expecting every line to match #686's (by hand: test request T-XXXX)

Risks

What could break: on macOS only, and only with the switch set: Metal's shader cache path is set from main() before SDL
(the same calls and checks as before, earlier), and the renderer's check goes through a host slot. Unset or empty, nothing
runs. Players, who do not set it, see no change.

Turning it off: leave RX_VK_METAL_CACHE_DIR unset (the default), or revert the PR.

Compatibility: the Apple host ABI moves to version 26 (size 408): the macOS executable and its renderer library must be
built together, as with every ABI step (on a mismatch rx_backend.c does not take the Vulkan renderer). Windows and Linux keep
version 22, size 360. No shader-stamp file changes; nothing changes in saves, longshot_options.ini or the caches.

Reviewer notes

  • Review 425fba24..5c37ec10, two commits: the topic picked with -x from macos/windowed-metal-cache (c77008a8 → 9ca31cd8)
    and the API end commit (5c37ec10). Read main()'s macOS block and hos_darwin_metal_cache_ask first, then
    darwin_metal_cache.h against the metal_cache.h it replaces (the same logic in C).
  • ABI: the slot takes the next free macOS number, 26. The topic was written where the Apple table ended at version 24; on
    this stack #686 took 25 for the backing generation (offset 392, size 400). The pick keeps #686's numbers where they met
    (vk_api.h's define, test_vk_darwin_admission.py's offsets and line, the startup fixture's layout check); 5c37ec10 then
    makes the slot version 26 at offset 400, size 408, in vk_api.h, both ABI fixtures and test_metal_cache_dir.py's check (the
    pick alone fails the two ABI checks on the layout, as that commit says). The tree's other users of MKWVK_API_VERSION check
    at least a version (test_fps_stats, test_menu_text_field) and pass at 26; the docs name no Apple version. Any other PR that
    appends to the Apple host table renumbers after whichever lands first.
  • Depends on #667 (metal_cache.h and the archives' begin it moves around, and through it #653 and #666's stack), #686 (the Apple
    host layout this appends to, and #658 below it) and #654 (main()'s macOS block with the Vulkan driver's choice, which the ask
    follows and the startup test orders it against, darwin_vulkan.{c,h}; and #479, #453, #437, #438 below it). The branch is
    #667's head bf5be681 with #686's head a0027de2 merged (1c35980c: vk_draw.cpp's dispatch(), #658's stand-in admission
    with #667's stack's named skips, as the integration branch 79e2e006 has it), then #654's head 7872c6e7 (425fba24:
    docs/development.md, the RX_VK_DRIVER row before the Metal rows, as 79e2e006).
  • The pick's conflict: test_vk_darwin_admission.py only (#686's layout, above); every other file is the topic's change line for
    line.
  • Sibling conflicts (git merge-tree): none of this PR's own commits beyond its bases'. Inherited: gpu/vk/vk_draw.cpp's
    dispatch() with #784 and #669 (the base merges'), docs/development.md with #785 (rows: keep
    both), display_startup_failure_test.cpp, win32_posix.c, test_collect_wide.py with #490 (#654's and #686's).
  • Conflicts with main (35465d12): inherited from the bases (docs/development.md, vk_draw.cpp, test_pipeline_play_cache.py);
    none of this PR's own.
  • Code comments cite reports/2026-11-1*.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; WC-01 passed: no probe holds it WIP.)
  • Rebased on current main. (On main e090cc48 through #667 bf5be681, #686 a0027de2 and #654 7872c6e7; main has moved since (35465d12): a delta sweep follows.)
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named). (On wave S c08ae52d = #784 694edb10 with this head 5c37ec10 and #785 a4d31a59 merged: 586 passed, 31 failed, 18 skipped against main e090cc48's 544/32/17 on this Mac; the 31 failures are main's own and nothing that passes on main fails; test_collect_wide now passes; 41 new tests pass; reports/PubWave-files/PubLate5/wave/full-S-c08ae52d/.)
  • 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. (They fail on this PR's base with this PR's test files; main has no Metal cache switch.)
  • A check under tests/ that reads changed source updated in this PR, not weakened.
  • MKWVK_API_VERSION bumped (gpu/vk/vk_api.h's MkwVkHost or MkwVkRenderer changed). (Apple 25 → 26 for the new host slot; non-Apple unchanged at 22.)
  • Docs updated.
**Type:** fix · **Risk:** low · **Tested on:** macOS 26.7.1, Apple M4 Max (MoltenVK main 5189f8de with the argbuf-slots patch), game-free on arm64 and x86_64 under Rosetta; GPU probe WC-01 on arm64 · **Needs:** macOS, Apple silicon (a windowed start with the switch set); Windows and Linux, any GPU (build and start) ## Why `RX_VK_METAL_CACHE_DIR` (macOS only, off by default, #653) asks Metal to keep its shader cache in a folder the user names instead of its own per-user folder. Metal fixes that folder the first time anything in the process uses it, and in a windowed start the renderer asked only after SDL had started its video, loaded the Vulkan library and made the window: by then the folder was fixed, and the switch was lost (`vk: RX_VK_METAL_CACHE_DIR NOT applied: …`). **Players are not affected**: nothing sets the switch for them, and Metal's own folder, which is writable outside a sandbox, caches a player's windowed start as it does a windowless one. It matters to whoever sets the switch, as the Mac's test runner does: its sandbox denies Metal's default folder, so the first windowed run (#35) cached nothing anywhere and its precompile took 1,464 s against 391 s for the windowless run before it (#32). This PR asks Metal for the folder in `main()`, right after `longshot_env.txt` and the Vulkan driver's choice, before SDL, the Vulkan library or any other thread, on macOS only. It is part of the macOS port. See #420. ## How to reproduce **Seen on:** macOS 26.7.1, Apple M4 Max, MoltenVK main 5189f8de with the argbuf-slots patch; main does not start on macOS yet: seen on the macOS port's integration build 3f872b0c (run #35), which carries the open macOS PRs **Settings and switches:** `RX_VK_METAL_CACHE_DIR=<an existing folder>` (the runner sets it), windowed **Steps:** start the game in its window with the switch set, and read the Metal cache line after the device is made. **Expected:** `vk: Metal shader cache: <the folder> (MTLGetShaderCachePath, as RX_VK_METAL_CACHE_DIR asked)`, and the next start warm from it. **Actual:** run #35: `vk: RX_VK_METAL_CACHE_DIR NOT applied: Metal's shader cache is /var/folders/<…>/C//com.apple.metal, not …/cache/metal (Metal chose its folder before the switch was applied)`; nothing cached (`reports/2026-11-15-windowed-metal-cache.md` §1-2). ## What changed Only where and when the folder is asked for changes; the logic and its log lines are the same. Unset or empty, nothing is loaded, called or logged, as before. Windows and Linux compile the same text as before. - host, `host/hos_main.c` `main()`: `hos_darwin_metal_cache_ask()` inside the macOS block, right after `hos_darwin_vulkan_choose_driver()`: after `longshot_env.txt` (which may set the switch), before `hos_display_init` (SDL), the game's thread, the window and anything that loads the Vulkan library. `--prepare-shaders` and headless starts pass the same point. - host, `host/posix/darwin_metal_cache.h` (new; `gpu/vk/metal_cache.h` moved with `git mv` and written in C): the checks, the `dlopen`/`dlsym` lookups, the CFString handling and every log string as they were; the path a 1,024-byte array (at most 1,023 bytes, as before). - host, `host/posix/darwin_vulkan.{c,h}`: `hos_darwin_metal_cache_ask()` keeps what it asked; `hos_darwin_metal_cache_check()` logs the result on the renderer's thread, which starts after the ask. - ABI, `gpu/vk/vk_api.h`, `host/rx_backend.c`: a new macOS `MkwVkHost` slot, `metal_cache_check`, appended after #686's `guest_backing_generation`: Apple host API version **26** (#686 took 25), offset 400, size 408 (`5c37ec10`). - renderer, `gpu/vk/vk_main.cpp`: `load_global` no longer asks; `mkwvk_init` calls `host->metal_cache_check()` where `metal_cache::check` was, after `create_device` and before `metal_archive::begin`. `metal_archive.h`'s comment names the new header. - docs: `docs/development.md`, the `RX_VK_METAL_CACHE_DIR` row says where the game asks and why. - tests: `tests/test_metal_cache_dir.py` compiles the host's real ask and check (cut from `darwin_vulkan.c`) and the header (also as C11 with `-Werror`) against made-up Metal and CoreFoundation calls, and checks `main()`'s order, the renderer's check after `create_device`, the macOS-only slot (version 26) and its wiring; `tests/display_startup_failure_test.cpp` runs the real `main()` and requires exactly one ask, after `longshot_env.txt` and the driver's choice, before any SDL load or init, window, thread or offscreen start; the ABI fixtures (`test_vk_darwin_admission.py`, `darwin_renderer_startup_test.cpp`) take the API 26 layout and call the wired slot; `test_darwin_renderer.py` and `test_metal_archive.py` follow the moved check. ## Measurements **Verdict:** fixed where measured. Probe WC-01 replayed a windowed start's own steps (SDL's video, MoltenVK and SDL's Vulkan library, the window, its events, then the device and two pipelines) in a sandbox shaped like the runner's, with this PR's real host ask and check: asked first, as `main()` now asks, Metal took the folder and wrote its cache there; asked where the renderer asked in #35, after the window, it did not. Asked after SDL's video start or after the Vulkan library it still took it: SDL's window creation is the step that fixes Metal's folder. Run #35's os_log agrees (the renderer's thread was the first to open Metal's cache files, after the main thread's window). For whoever sets the switch, a windowed start should then be as warm as a windowless one: the start's time is in the table. Baseline: main does not start on macOS. Both "before" columns are the integration build 3f872b0c (I38), arm64, without this change: #35 windowed, #32 windowless (the switch applied there), each with the runner's sandbox and the switch set. Hardware: macOS 26.7.1 (25G241), Apple M4 Max (40-core GPU), 16-core CPU, 64 GB Scene: a start from the field-dawn save (runs #32, #35): the preparation, the precompile, the title Method: the Metal cache line of the game log, and the run's Metal and libCoreFSCache os_log lines counted by text (`log show` over #35's pid, 01:06:43-01:36:00; #32's as the RosettaCache lane counted them); one run each. WC-01: six routes, one process each, the real `hos_darwin_metal_cache_ask()`/`_check()` and `darwin_metal_cache.h` (blob-identical at this head) made at each route's point, `MTLGetShaderCachePath` read after the device and the route's folder listed; released once on arm64 (05:02 CST) How to reproduce: a windowed start with `RX_VK_METAL_CACHE_DIR` set, reading `vk: Metal shader cache` / `NOT applied`; WC-01's `build` and `run` commands (`reports/2026-11-15-windowed-metal-cache.md` §4) | | Before: integration @ 3f872b0c (run #35, windowed) | Before: integration @ 3f872b0c (run #32, windowless) | After: integration @ 2b4b7b8e with this PR (run #36, windowed) | |---|---|---|---| |the switch's folder|not applied: Metal's default folder, denied by the sandbox|applied|applied, asked for before SDL (log line 2, ahead of `sdl: SDL3` at line 3)| |pipelines found in Metal's cache 30 s into the precompile|1,379|30,977|37,668| |precompile|1,464 s|391 s|346 s (62.9 s of it translating afresh: the stamp changed)| |start to hand-off|24 m 28 s|9 m 30 s|6 m 53 s| | WC-01 route (ask made …) | applied | Metal's folder files written there | |---|---|---| | `windowless` (first; no SDL: #32's order) | yes | functions and libraries cache, 5 files | | `fixed` (first, then SDL and the window: this PR's order) | yes | 5 files | | `after-init` (after SDL's video start) | yes | 5 files | | `after-vklib` (after MoltenVK's and SDL's Vulkan library load) | yes | 5 files | | `after-window` (after `SDL_CreateWindow`) | no: `/var/folders/<…>//com.apple.metal` | none | | `today` (after the window and 1 s of events: #35's order) | no: `/var/folders/<…>//com.apple.metal` | none | ``` #35 L274: vk: RX_VK_METAL_CACHE_DIR NOT applied: Metal's shader cache is /var/folders/<redacted>/C//com.apple.metal, not …/cache/metal (Metal chose its folder before the switch was applied) WC-01 reports/WindowedCache-files/probe/runs-01/result.json (sha256 52524f95b04516267adf66ac07be6006b0a0da284a4cd8c7f3741a7d34748975): fix_proven true, first_touch_after "after-window", crash_reports_new [], every route exit 0 with 2 pipelines made ``` Reports: `reports/2026-11-15-windowed-metal-cache.md` (the order of a windowed start, what a player's start caches, the fix, WC-01), `reports/2026-11-15-mac-startup-35-windowed.md`, `reports/2026-11-14-mac-startup-32.md`. ### Flags and settings None new. One switch is asked earlier: `RX_VK_METAL_CACHE_DIR`, off by default. The diff against main also shows the switches of the PRs it stacks on (#667's, #686's and #654's stacks), theirs to list: `HOS_KEEP_FREED_PAGES`, `HOS_ZERO_ALL_PAGES`, `LONGSHOT_DARWIN_ARCHIVE_RECEIPT`, `LONGSHOT_DARWIN_ARCHIVE_RECEIPT_SHA256`, `LONGSHOT_FFMPEG`, `LONGSHOT_FSR`, `LONGSHOT_KOSMICKRISP`, `LONGSHOT_MOLTENVK`, `LONGSHOT_NGX_SDK`, `LONGSHOT_SDL3`, `LONGSHOT_VULKAN_LOADER`, `RX_ARCH`, `RX_NVN_WATCH_AFTER`, `RX_NVN_WATCH_ALL`, `RX_NVN_WATCH_TEXELS_SRC`, `RX_VK_ALONE_TRIES`, `RX_VK_COMPILER_SERVICE`, `RX_VK_COMPILE_QOS`, `RX_VK_DLSS_ACTORS`, `RX_VK_DRIVER`, `RX_VK_GIANT_COMPILES`, `RX_VK_GIANT_HOLD_PRESENTS`, `RX_VK_GIANT_HOLD_S`, `RX_VK_GIANT_WORDS`, `RX_VK_GPU_DUMP`, `RX_VK_LIBRARY`, `RX_VK_MESH_IND_COUNT_WORD`, `RX_VK_METAL_ARCHIVE_DIR`, `RX_VK_NO_UPSCALE_STUB`, `RX_VK_ONLY`, `RX_VK_SKIP_LOG`, `RX_VK_SKIP_PROG`, `RX_VK_SLOW_COMPILE_S`, `RX_VK_WORKER_CACHES`. | Flag | What it is | Before this PR | After this PR | Effect | Helps / doesn't help / untested | Recommendation | |---|---|---|---|---|---|---| | `RX_VK_METAL_CACHE_DIR=<folder>` | the folder Metal keeps its shader cache in (macOS only) | unset; set, asked in the renderer's `load_global`: lost in a windowed start | unset; set, asked in `main()` before SDL: applied in windowed and windowless starts | a windowed start that sets it uses its folder | helps: the runner's windowed starts (its sandbox denies Metal's default folder); doesn't matter for players (unset, Metal's own folder is warm); untested in the game | off by default, as it ships | ## Testing **Tested:** on macOS 26.7.1, Apple M4 Max: - At this head (main e090cc48, #667 with #686 and #654 merged, this PR), every run under the shared check slot with a fresh HOME and TMPDIR (`reports/PubWave-files/PubLate5/StartTrio/proof/wmc-*`): - arm64 and x86_64 (Rosetta), exit 0 on each: `test_metal_cache_dir` (`ok: source: asked in main() after longshot_env.txt and the driver's choice, before SDL's display and any thread, macOS only; …`), `test_display_startup_failure` (`11 cases, 0 failed`), `test_vk_darwin_admission` (`Apple version 26, size 408, typed callbacks at 360/368/376/384/392/400`), `test_darwin_renderer` (`Apple26 typed host callback wiring`, both arches in one run), `test_metal_archive`. - On this PR's base `425fba24` with this PR's test files, exit 1 on each: `host/posix/darwin_metal_cache.h is missing`; `11 cases, 11 failed` (`Metal's shader cache folder not asked once at startup`); `FAIL: Apple host API version is 26`; `no member named 'metal_cache_check' in 'MkwVkHost'`; `test_metal_archive`'s source check not finding `host->metal_cache_check();`. - Main's `test_crash_record` `source_contract()` alone (`hos_main.c` changes): passed. The bases' own checks on the merged tree: `test_darwin_guest_memory` (both), `test_vk_darwin_sdl`, `test_vendor_fixes` (arm64, x86_64), `test_thread_stack` (both), `test_loading_stall`, `test_import_hold` (arm64): exit 0. - Compiled whole for arm64 and x86_64: `gpu/vk/vk_main.cpp` and the renderer files the base's merges combine (`vk_draw.cpp`, `vk_memory.cpp`, `vk_replay.cpp`, `vk_texture.cpp`) with the tree's own renderer flags, 10 of 10; `host/hos_main.c`, `host/posix/darwin_vulkan.c` and `host/rx_backend.c` with the macOS host flags (#490, #479, #476, #492 merged in a scratch tree only), 6 of 6; and `host/posix/win32_posix.c` (no change of this PR's, but new to all three bases: the merges combine #686's and #654's), 2 of 2 with #490's side of its include hunk, as for #491 and #658 before (a scratch merge that keeps the branch's side drops #490's Mach includes) (`…/StartTrio/compile/windowed-metal-cache-own/`, `…-host/`). - The topic as written (`macos/windowed-metal-cache` `c77008a8`, API 25, on the integration branch `79e2e006`): the two order checks failed on `79e2e006` (`darwin_metal_cache.h is missing`; 11 of 11 startup cases `not asked once at startup`) and on 4 mutations (the ask after `hos_display_init`, the check before `create_device`, the string released before `MTLSetShaderCachePath`, the slot unwired); 8 of 8 whole-file compiles (`hos_main.c`, `rx_backend.c`, `darwin_vulkan.c`, `vk_main.cpp`, arm64 and x86_64) with 0 warnings; the object link check 0 failures; full suite 585 passed, 1 failed, 16 skipped, by name the base's (`test_menu_i18n`) (`reports/WindowedCache-files/`). - Off Apple (`unifdef -U__APPLE__`, base against head): every changed file's text is the base's (`metal_archive.h` in a comment); `metal_cache.h` (Apple-only) is gone and `darwin_metal_cache.h` is new, neither included off Apple (`reports/PubWave-files/PubLate5/StartTrio/offapple-windowed-metal-cache.txt`). - GPU probe WC-01 (frozen packet, review `eda365d7…`, build `4f53e8b5…`; released once on arm64 by the parent, 05:02 CST): `reports/WindowedCache-files/probe/runs-01/result.json` (sha256 `52524f95b04516267adf66ac07be6006b0a0da284a4cd8c7f3741a7d34748975`): `fix_proven` true (the windowless control and this PR's order applied, with Metal's cache files in their folders; #35's order not applied, its folder empty), `first_touch_after` `after-window` (SDL's window creation fixes the folder), no new crash report. Its host code is this head's: `darwin_metal_cache.h` and `darwin_vulkan.c` are blob-identical at `c77008a8`, which it was built from, and here. **Not tested:** Rosetta in the game (WC-01 and run #36 are arm64); Windows and Linux: not built there (their text is the base's); its test request covers them. A windowed game start on the integration carrying this PR, run #36, found Metal's cache warm, as in the table. WC-01 has passed, so no probe holds this PR in WIP any more; the Testing box waits for the test requests. **Must be tested:** - macOS, Apple silicon, on the rebased macOS stack: a windowed start with the runner (the switch set, a folder warm from a start of the same tag length), expecting the ask line right after `vk: Vulkan driver:`, then `vk: Metal shader cache: …/cache/metal (MTLGetShaderCachePath, as RX_VK_METAL_CACHE_DIR asked)` after the device, and a precompile like #32's (by hand: the parent's game run) - Windows and Linux, any GPU: build this PR (head `5c37ec10`, it carries #667, #686, #654 and what they carry) and #686's head the same way and start each to the title, expecting every line to match #686's (by hand: test request T-XXXX) ## Risks **What could break:** on macOS only, and only with the switch set: Metal's shader cache path is set from `main()` before SDL (the same calls and checks as before, earlier), and the renderer's check goes through a host slot. Unset or empty, nothing runs. Players, who do not set it, see no change. **Turning it off:** leave `RX_VK_METAL_CACHE_DIR` unset (the default), or revert the PR. **Compatibility:** the Apple host ABI moves to version 26 (size 408): the macOS executable and its renderer library must be built together, as with every ABI step (on a mismatch `rx_backend.c` does not take the Vulkan renderer). Windows and Linux keep version 22, size 360. No shader-stamp file changes; nothing changes in saves, `longshot_options.ini` or the caches. ## Reviewer notes - Review `425fba24..5c37ec10`, two commits: the topic picked with `-x` from `macos/windowed-metal-cache` (`c77008a8` → `9ca31cd8`) and the API end commit (`5c37ec10`). Read `main()`'s macOS block and `hos_darwin_metal_cache_ask` first, then `darwin_metal_cache.h` against the `metal_cache.h` it replaces (the same logic in C). - **ABI: the slot takes the next free macOS number, 26.** The topic was written where the Apple table ended at version 24; on this stack #686 took 25 for the backing generation (offset 392, size 400). The pick keeps #686's numbers where they met (`vk_api.h`'s define, `test_vk_darwin_admission.py`'s offsets and line, the startup fixture's layout check); `5c37ec10` then makes the slot version 26 at offset 400, size 408, in `vk_api.h`, both ABI fixtures and `test_metal_cache_dir.py`'s check (the pick alone fails the two ABI checks on the layout, as that commit says). The tree's other users of `MKWVK_API_VERSION` check at least a version (`test_fps_stats`, `test_menu_text_field`) and pass at 26; the docs name no Apple version. Any other PR that appends to the Apple host table renumbers after whichever lands first. - Depends on #667 (`metal_cache.h` and the archives' begin it moves around, and through it #653 and #666's stack), #686 (the Apple host layout this appends to, and #658 below it) and #654 (`main()`'s macOS block with the Vulkan driver's choice, which the ask follows and the startup test orders it against, `darwin_vulkan.{c,h}`; and #479, #453, #437, #438 below it). The branch is #667's head `bf5be681` with #686's head `a0027de2` merged (`1c35980c`: `vk_draw.cpp`'s `dispatch()`, #658's stand-in admission with #667's stack's named skips, as the integration branch `79e2e006` has it), then #654's head `7872c6e7` (`425fba24`: `docs/development.md`, the `RX_VK_DRIVER` row before the Metal rows, as `79e2e006`). - The pick's conflict: `test_vk_darwin_admission.py` only (#686's layout, above); every other file is the topic's change line for line. - Sibling conflicts (`git merge-tree`): none of this PR's own commits beyond its bases'. Inherited: `gpu/vk/vk_draw.cpp`'s `dispatch()` with #784 and #669 (the base merges'), `docs/development.md` with #785 (rows: keep both), `display_startup_failure_test.cpp`, `win32_posix.c`, `test_collect_wide.py` with #490 (#654's and #686's). - Conflicts with main (`35465d12`): inherited from the bases (`docs/development.md`, `vk_draw.cpp`, `test_pipeline_play_cache.py`); none of this PR's own. - Code comments cite `reports/2026-11-1*.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; WC-01 passed: no probe holds it WIP.) - [ ] Rebased on current `main`. (On main e090cc48 through #667 `bf5be681`, #686 `a0027de2` and #654 `7872c6e7`; main has moved since (`35465d12`): a delta sweep follows.) - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). (On wave S `c08ae52d` = #784 `694edb10` with this head `5c37ec10` and #785 `a4d31a59` merged: 586 passed, 31 failed, 18 skipped against main e090cc48's 544/32/17 on this Mac; the 31 failures are main's own and nothing that passes on main fails; `test_collect_wide` now passes; 41 new tests pass; `reports/PubWave-files/PubLate5/wave/full-S-c08ae52d/`.) - [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. (They fail on this PR's base with this PR's test files; main has no Metal cache switch.) - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. - [x] `MKWVK_API_VERSION` bumped (`gpu/vk/vk_api.h`'s `MkwVkHost` or `MkwVkRenderer` changed). (Apple 25 → 26 for the new host slot; non-Apple unchanged at 22.) - [x] Docs updated.
mooki added 111 commits 2026-11-15 11:56:13 +00:00
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]>
test(runtime): follow main's HLE call counts in the Darwin guest-memory fixture
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 20s
CI / Conventional Commit title (pull_request) Successful in 12s
CI / Workflow lint (actionlint) (pull_request) Successful in 9s
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 9s
ae36a96bb0
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]>
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
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 5s
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 9s
1b028c8419
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]>
Add macOS 14.4 public address waits, awake-uptime deadlines, relative
monitor waits, absolute timer sleeps, and real initialization for zeroed
SRW locks. Keep the Linux source behind each Apple branch byte-identical.

Exercise the extracted layer with real primitives and C/C++ header clients
on arm64 and x86_64. Document the existing broadcast address wakes, timer
restrictions and non-atomic polling, without claiming a complete host port.

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 pointer mailbox test uses the real POSIX SRW wrappers, which now call
into darwin_wait.c. Compile that production C dependency and link it into
the existing native-default fixture on macOS only. Keep the Linux and
Windows compiler invocation and every existing assertion unchanged.

The full-suite failure at 1da4f9b3 is retained. This repair does not resolve
the held cross-image address-wait contract decision or qualify the port.

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]>
Move the address sequence words into an executable-only provider and resolve
that canonical service once in each shim image. Fail closed when the host
provider is absent; retain full-width comparisons and broadcast ordering.

Extend the existing wait driver with real host/two-dylib behavior coverage,
including upper-half and cross-word changes, same-image controls, image-local
error state, broadcast wakes, and rejection of a dylib-only provider. Keep
the existing timing campaign intact and independently selectable.

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]>
docs(runtime): describe the executable-owned macOS address-wait state
Some checks failed
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
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s
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 10s
0bc4e92cee
The wait section still said each image's darwin_wait.c is self-contained.
Since the address sequence words moved into the executable, the renderer's
copy finds them through RTLD_MAIN_ONLY, a missing provider aborts, and the
build has to link darwin_wait_state.c into the host only. Say so, and name
the test's --shared-only mode.

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]>
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]>
prepare_run's first part (every shader translated, and each compute
variant compiled for the pipeline cache) ran on half the CPUs
everywhere: prepare_cpus gives half_cpus unless the Windows process heap
is the segment heap. Half exists for the NT heap's lock that an
in-process driver compiler queued on, and for whole-PC freezes when
those compilers ran on every CPU at High priority. Neither holds on
macOS: MoltenVK converts SPIR-V to MSL in a few ms and hands every
compile to Metal's compiler service (MTLCompilerService, a launchd
service at nice 0), so the workers mostly wait on it. The Mac's first
start logged "8 workers for the shader programs (16 CPUs usable; half
the CPUs)".

prepare_workers::count takes an `every` argument (default false): each
usable CPU gets a worker, at least half_cpus and 1, at most kMaxWorkers.
kEveryCpu is true where __APPLE__ is defined, and prepare_cpus passes
it; the preparation's log line says "every one: macOS". In a synthetic
MoltenVK 1.4.2 benchmark on an M4 Max (16 CPUs, macOS 26.7.1) 16 threads
compiled 56-61 % more compute and 35-39 % more graphics pipelines a
second than 8, but only with a pipeline cache per thread; with the one
shared cache LS uses today the rate is flat from 4 to 16 threads. The
next commit gives each worker its own cache.

tests/test_prepare_every_cpu.py compiles the real prepare_workers.h and
prepare_cpus() cut from vk_progcache.cpp: on this compiler's target
prepare_cpus() gives 16 of 16 usable CPUs on macOS (8 elsewhere); count
with and without `every`; prepare_cpus with kEveryCpu forced either way.
On the base it fails: prepare_cpus() gives 8 of 16 on macOS.

Windows and Linux: unchanged (kEveryCpu false; the existing
test_prepare_workers and test_prepare_cpus cases are untouched).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 3573e736e15078554543e4740c5e2d53c719fa89)
MoltenVK 1.4.2 holds one mutex per VkPipelineCache across the SPIR-V to
MSL conversion and Metal's library compile (MVKPipeline.mm
getShaderLibrary, _shaderCacheLock). Every worker of the start's
preparation compiled with the one start cache, pcache, so they compiled
one pipeline at a time: the Mac's first start (mac-arm64-a5f2d6f9-01,
M4 Max) logged strict pairs of Metal compiles every ~195 ms, 5.05
compute variants a second, an estimated 33 minutes for the compute
programs alone. A synthetic benchmark on that Mac (PrepSpeed, MoltenVK
1.4.2, macOS 26.7.1): one shared cache stays at 10.5 compute and 9.6
graphics pipelines a second from 4 to 16 threads; a cache per thread,
merged after, gives 31.5 / 29.7 at 8 threads and 49-51 / 40-41 at 16.

gpu/vk/worker_caches.h: a Set of empty caches, one per worker of a
phase, that a worker enters with Scope; create() asks the shared cache
first under VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT
and compiles what it lacks with the worker's own; merge_into() merges
them into the start cache with one vkMergePipelineCaches in slot order
(MoltenVK retains each Metal library, no compile). The caches are not
externally synchronized, so a shared slot stays correct.

vk_draw.cpp:
- create_cached: before play, a worker in a phase with a set goes
  through worker_caches::create (pcache asked first, so a saved file's
  or an earlier phase's pipelines are found as before); every use of
  pcache holds pcache_mu shared, and worker_caches_merge holds it alone
  (vkMergePipelineCaches's dstCache is externally synchronized:
  vk.xml externsync without INTERNALLY_SYNCHRONIZED_MERGE). A save's
  reads of pcache's data hold it shared too.
- prepare_run (the compute warm), precompile_run, precompile_warm (both
  passes) and precompile_at_create make a set under worker_caches_on,
  enter it in their workers and merge it right after the join; the
  precompile's stragglers left running are merged as far as they got.
- precompile_run, with the caches on, sorts the notes by program
  variant and hands a variant's notes to one worker in runs of up to
  32 (worker_caches::runs/claim), so its shader libraries are compiled
  once, not by every worker that met one of its notes at the same time.
- In play, with the caches on, a compile goes into a new cache of its
  own merged into the generation's at once under the generation's lock
  (made_alone), which the asks of the generation hold shared: the asks
  keep their order and find what any thread compiled.

vk_vendor.h: worker_caches, on for VK_DRIVER_ID_MOLTENVK only, and only
with pipelineCreationCacheControl; RX_VK_WORKER_CACHES forces it either
way. Elsewhere nothing measured says a driver's cache is locked across
its compiles, and the caches would only add merges. The pipelines built,
the persisted cache's format and its stamp are the same either way.

tests/test_worker_caches.py runs vk_draw.cpp's own create_cached,
play_cache_make, worker_caches_on and worker_caches_merge and the real
header against a made-up driver whose caches lock across a compile:
four workers compile at once (without a set they cannot); pcache asked
first, nothing compiled with it; ask-only callers never compiled; caches
not externally synchronized; one merge in slot order whatever order the
slots were taken in, with no ask of pcache beside it; play's compiles
merged into the generation at once; runs and claims. The four-workers
part runs against the base's create_cached and fails there (1 of 4
compiles met the others). tests/test_pipeline_play_cache.py gets an
"alone" case, and both play-cache and cache-save harnesses the new
stand-ins (pcache_mu, worker_caches_on).

Windows and Linux: the same code paths as before (no set, one shared
cache), plus an uncontended shared lock around pcache's uses.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 2174e33f14ccce8c1635565e734773a33bc32455)
tests/test_recorder_sync.py holds every header to no recorder boundary
call: vk_work.h, included last by each renderer .cpp, turns
vkDestroyPipelineCache into a drain of RX_VK_RECORDER's packets and then
the call, and a header's calls would be wrapped or not depending on what
included it. worker_caches.h called vkCreatePipelineCache,
vkDestroyPipelineCache and vkMergePipelineCaches itself, from vk_draw.cpp
before vk_work.h.

worker_caches::Set and made_alone now take a Calls struct with the
three operations; vk_draw.cpp defines worker_cache_calls after
vk_work.h, so a set's cache destruction goes through the recorder's
boundary like every other vkDestroy* in the file. Behaviour is
otherwise the same: the same caches, made, merged and destroyed in the
same order.

tests/test_worker_caches.py and tests/pipeline_play_cache_test.cpp take
worker_cache_calls from the source and the model respectively.
test_recorder_sync now fails only as on the base (vk_vma.cpp's
vmaDestroyAllocator), and the four tests that touch the caches pass.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit aa8ecba546465f1ce65ac872ecf21d232b65cef0)
MoltenVK 1.4.2 keeps MSL, not GPU code, in a VkPipelineCache, and
vkCreatePipelineCache with saved data compiles every stored library one
after another on the calling thread (MVKPipeline.mm readData).
pcache_init hands it all of mkw_vk_pipelines.bin, and play_files_merge
every play file, at every start. The PrepFast GPU check on the M4 Max,
with Metal's per-user cache blocked as in the runner, ran three later
starts of the same 64 pipelines (80 libraries):
- reading the one file: 5.7 s of serial compiles, 6.6 s in all;
- 16 shards read in parallel: 1.4 s;
- nothing read, pipelines compiled from SPIR-V with the worker caches:
  1.4 s.
At the game's scale the read is tens of minutes per start.

vk_vendor.h gets pcache_no_load, on for VK_DRIVER_ID_MOLTENVK only and
logged in the vendor fixes' line with worker_caches. It is the device's
default for RX_VK_PCACHE_LOAD: pcache_load_off reads the switch first
(any value, as before) and falls back to the fix, so
RX_VK_PCACHE_LOAD=1 reads the files on MoltenVK as everywhere else. With
the fix on and the switch unset or off, pcache_files_unused holds:
- pcache_init reads neither the main file nor the play files and says
  so in the log; the cache lives in memory for the run;
- nothing is saved (pcache_no_save, as RX_VK_PCACHE_LOAD=0 already
  did), and play_cache_save writes no play file and keeps the
  generation in memory: unread play files would pile up, since they
  are compacted only where they are read;
- the stamp is left alone (pcache_drop_stale returns as for any
  start that does not read the files);
- precompile_run drops RX_VK_LOAD=create's longshot_pipelines.bin.built
  marker at each start, as it does when the files were dropped: no
  cache a later start reads holds the notes, so the warm builds them
  into the in-memory cache again before loads at create ask for them.

Saving does not continue on MoltenVK because nothing reads the file or
its stamp there:
- the prepared record (prepared_for) has its own file and stamp;
- --prepare-shaders' saved cache is useful only to a start that reads
  it;
- pcache_saved_now serves graphics pipeline libraries, which MoltenVK
  does not have;
- a start with RX_VK_PCACHE_LOAD=1 makes, saves and stamps its own.
Every other driver is unchanged, RX_VK_PCACHE_LOAD=0 there included
(main file unread and unsaved, play files still merged and written).

tests/test_vendor_fixes.py checks the two cache fixes on its 16 devices
and on MoltenVK: on for MoltenVK alone, RX_VK_WORKER_CACHES forcing
the first, vendor_fixes_env leaving pcache_no_load to the files'
switch. It also checks the log line and pcache_load_off's order.
tests/test_pcache_stamp.py runs pcache_load_off, pcache_files_unused,
pcache_drop_stale and pcache_init, cut from the source, in eight starts:
other drivers unset, 0, empty and 1 as before, and MoltenVK unset, 0,
off and 1. test_pipeline_play_cache gets an "unused" case. All three
fail on aa8ecba5. tests/test_warm_bounded.py's source check of the
marker's removal takes the new condition.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 61bfbf1ae7c0966a9436ab0c556a9ee547bb3b5f)
macOS starts a std::thread with 512 KiB of stack, where Linux starts one
with 8 MiB and Windows with longshot.exe's 8 MiB. On macOS MoltenVK
converts each shader with SPIRV-Cross on the thread that creates the
pipeline, recursing once per block of a program's control flow, and a
prepare_run worker overran its 512 KiB there (CFG::walk_from under
vkCreateComputePipelines from seed_compute, start #6, 2026-11-14).

gpu/vk/thread_stack.h: thread_stack::thread is std::thread itself off
macOS; on macOS a std::thread-like thread (construction with decayed
arguments, joinable, join, detach, native_handle, moves) started by
pthread_create with thread_stack::kBytes (16 MiB) of stack. Every
thread the renderer starts goes through it: the preparation's pools
(prepare_run, state_list_prepare, warm_libraries, precompile_at_create,
precompile_run, predict_run, warm_bounded::run), the job workers and
the serial builder, the replay, parallel-replay, presenter and
frame-generation threads, the program cache's loaders and the rest.
mkwvk_init logs the size once per start on macOS.

tests/test_thread_stack.py: no file under gpu/vk starts a thread any
other way; each kind of thread runs a recursion 2 MiB deep through the
helper, and on macOS each overflows as a std::thread (caught, no crash
report). Seven source checks follow the new spelling.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 48b49054b1c64851b3e8c974e64dc0b757b2ced3)
The first thread whose stack overflows says so and ends the process;
the others wait in their handlers, so the message is not interleaved.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 62d0991e0cfb5402202f8f14e9eb799d5c20d262)
test: the checks that compile renderer code with its threads see thread_stack.h
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 16s
CI / Conventional Commit title (pull_request) Successful in 14s
CI / Workflow lint (actionlint) (pull_request) Successful in 19s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 18s
Third-party parts / As their notes say (pull_request) Successful in 15s
01ffd9a938
Eight checks cut renderer functions that start threads (or copy
vk_jobs.cpp and vk_recorder.h) into fixtures of their own; they now
build with gpu/vk/thread_stack.h beside them: test_capture_copy_helpers,
test_gpu_hang_watch, test_jobs_failure, test_jobs_wait_runs_queued,
test_pipeline_cache_save, test_prepare_stall, test_present_source_crop
and test_recorder_sync.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit fd46ff431f9f62b1125582a4a3434a8cc92c551e)
fix(macos): the worker caches follow main's fetch lock (#190) and Intel's gate (#252)
Some checks failed
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
PR description / Description follows the template (pull_request_target) Successful in 17s
CI / Conventional Commit title (pull_request) Successful in 15s
CI / Workflow lint (actionlint) (pull_request) Successful in 22s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 20s
Third-party parts / As their notes say (pull_request) Successful in 27s
eee107e2f1
Main's #190 (f41a5c59) put a fetch lock (PcacheFetch, PcacheUse) on the pipeline cache's save and compile path, the
very path where the worker caches add the lock their merges need, and #252 (57f4eaec) put Intel's compiler gate
(IntelDriverGate) around the compiles. The worker caches now sit on top of both:

- pcache_mu, the merge lock, is taken only where the worker caches are on (pcache_use_held: MoltenVK by default).
  Elsewhere create_cached, the save's fetch and pcache_saved_now take exactly #190's locks, as on main: no new lock
  on any other driver. A play generation's lock is likewise taken only where play's compiles are merged into it
  (g->alone).
- The worker caches are never on with the fetch lock (worker_caches_on: not with pcache_fetch_lock_on, Intel's
  Windows driver or RX_VK_PCACHE_FETCH_LOCK=1): a merge reads its caches' data as a fetch does, which that lock keeps
  apart from the builds there, and the gate lets one compile at a time into Intel's compiler there anyway.
- The order of the locks: Intel's gate, then pcache_mu, then the fetch lock. The save's fetch and pcache_saved_now
  take pcache_mu before PcacheFetch; a merge takes pcache_mu alone.
- prepare_run's compute warm keeps #252's gate around its compile and, with the caches, goes through create_cached
  under it (cached_compute would take the gate a second time).
- tests: test_worker_caches checks the gating (with the caches off a pipeline is made while pcache_mu or the
  generation's lock is held alone elsewhere; with them on it waits for pcache_mu), worker_caches_on against the
  fetch lock, the warm's gate and pcache_mu ahead of PcacheFetch in the save. test_pipeline_play_cache's route case
  checks that no lock of theirs is taken off MoltenVK; pipeline_play_cache_test.cpp and pipeline_cache_save_test.cpp
  get pcache_use_held stand-ins. test_pcache_stamp's load fixture follows #321's cache_path and #190's PcacheFetch in
  pcache_init, and play_cache_save's new parameters.
- docs: #321's cache/longshot_vk_pipelines.bin in the RX_VK_WORKER_CACHES row and in vk_vendor.h, and a row for
  RX_VK_PCACHE_LOAD, whose default this PR changes on MoltenVK.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The precompile hand-off starts its workers and its compile watcher through thread_stack::thread. One conflict file,
gpu/vk/vk_draw.cpp (4 blocks: the lines P31 had to resolve against main, the other way round): the include block keeps prepare_workers.h,
worker_caches.h and thread_stack.h; prepare_run's, precompile_at_create's and precompile_run's thread lines take
thread_stack::thread and keep the worker caches' merges beside them.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the Mac's start 12 every precompile worker was held by a Metal compile of minutes while 37,600 of
42,234 items were still to hand out, so RX_VK_PRECOMPILE_STALL_S could not apply (it waits until every
item is handed out) and the title never came. With the new vendor fix compiler_service (MoltenVK only,
RX_VK_COMPILER_SERVICE):
- the precompile hands the rest to the game once nothing has finished for the stall time, or once all
  that is left is compiles running past RX_VK_SLOW_COMPILE_S; a quarter of the workers build the rest
  behind the game, every item still built, each straggler named when it ends;
- its order: noted pipelines first, then fallbacks and computes by SPIR-V words, what an earlier run
  found slow (longshot_pipelines.bin.slow) last;
- a compile over the slow time (10 s) is named with key, variant, kind, stage and SPIR-V words while it
  runs and when it ends, and written to the slow list;
- a pipeline whose compile failed is compiled once more with a new pipeline cache (MoltenVK keeps a
  failed library in the cache it was compiled with), and said made or not made.
Every other driver: the classic worker, wait and log line as they were.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 77d4971cbdb6356d9064a44cdee923f4bde354f9)
Assisted-by: Claude Opus 5.5

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit f9a179c08d3f9e102729ec8fe5fe923336f581b8)
Assisted-by: Claude Opus 5.5

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 194a230dd959706797203501edad3b5a2331118a)
test_fallback_pipeline_state compiles build_graphics cut from the source; the noted pipelines of behind_worker and precompile_warm name what they build instead.

Assisted-by: Claude Opus 5.5

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 528784dc5c0c293da1b692f6fd71da070a940d8b)
fix(vk): the precompile hand-off on main's Intel gate and quick builds (#252), fetch lock (#190) and game folder (#321)
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 5s
CI / Conventional Commit title (pull_request) Successful in 3s
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 12s
02cb2bc35f
The hand-off was written on the macOS integration branch, before main's #190 (f41a5c59), #252 (57f4eaec) and #321
(76699aad), and before the worker caches followed them (this branch's base). What it needs on main's code:

- cached_graphics and cached_compute keep #252's gate and main's `return create_cached(guarded_*_held, ci, pl);`, and go
  through create_watched only where compiles are watched (compiles_watched: compiler_service, MoltenVK's, or
  RX_VK_SLOW_COMPILE_S set). Every other driver makes its pipelines exactly as on main, and main's own source checks
  (test_intel_compiler_gate, test_pcache_fetch_lock) read the lines they read there.
- prepare_run's compute warm: with the worker caches and watched compiles through create_watched, under the gate it holds;
  otherwise P22's create_cached or main's guarded_compute_held, as they are.
- behind_worker, the precompile's worker under compiler_service, builds a compute pipeline as the classic worker now
  does (#252): on Intel's one-at-a-time builds (serial_builds_on) without the optimizer first, the optimized build owed
  to its first dispatch (precompile_compute_quick, optimize_owed). On MoltenVK, where compiler_service is on, that never
  applies unless RX_VK_COMPUTE_SERIAL=1; it keeps the two workers' builds the same.
- test_precompile_handoff's source checks follow main's spellings: the watched and plain lines in cached_* after the gate,
  compiles_watched, the warm, the dispatch's inline build named as main spells it (p, not the integration branch's plain),
  and behind_worker's quick build.
- docs: the slow list is cache/longshot_pipelines.bin.slow since #321 moved the notes into the game folder's cache/.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
On the Mac's cold start 14, prepare_run's compute warm kept 13 of its 16 workers on compute compiles of
122k-209k SPIR-V words for 205-274 s each. The preparation reached 278 of 38,809 programs in 430 s, and the
precompile, whose hand-off lets the title start while such compiles go on behind it, never began.

With compiler_service (MoltenVK only) the warm compiles nothing. Each compute variant is still translated and
kept by seed_compute (program_cache_save), and the state list and the prepared record follow as before. The
precompile compiles the pipelines: every kept compute program on a first start (computes_plan), in its order
and with its hand-off; one it does not take is compiled at its first dispatch. Every other driver's warm is
unchanged.

tests/test_warm_handoff.py runs prepare_run's own decision and warm_compute against stand-ins on both arches,
and pins the source chain that keeps each variant for the precompile.

Assisted-by: Claude Opus 5.5

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 84c32d68c536468b61af24230ea8c283c5f091ef)
Start 14 had 13 compute compiles of 122k-209k SPIR-V words in flight at once while the other programs went
at 0.1-0.3 a second, and Metal's compiler service aborted twice (LLVM bad_alloc).

precompile_handoff.h Giants: at most RX_VK_GIANT_COMPILES (4) compiles of more than RX_VK_GIANT_WORDS
(100,000) SPIR-V words in flight at once among behind_worker's workers, before the game and behind it. A
giant claimed while the bound is reached is deferred, never dropped, and named once with a count; its
worker takes the next item, and the deferred ones are taken first once a giant ends. A worker with nothing
else waits for them and leaves only when none is deferred. behind_worker claims with claim_below, which
never passes the end, and a worker adds its one "ran out" to next as it leaves, so the wait's
next >= total + threads still means every worker has left. The classic worker (every other driver) is
unchanged.

The precompile_handoff fixture runs the gate with blocking giants: at most 2 of 6 in flight, the small
items going on meanwhile, each deferral named once with its count, every item built once, before the game
and behind it with the hand-off.

Assisted-by: Claude Opus 5.5

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit f1e028c8db05ef31383fa44c05fa156984c0f6c9)
Assisted-by: Claude Opus 5.5

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 8aaa2f33d51b6b6f8db335cac69639137833537e)
The bound on giant compiles in flight is now min(RX_VK_GIANT_COMPILES, workers in the phase - 1), and at
least 1. Before the hand-off the phase is the precompile's workers (16 on an M4 Max: 4); at the hand-off
precompile_run sets it to the share behind the game (4 workers: 3), so one worker there always stays on the
smaller items. Giants already in flight over the new bound finish; no more start until they are under it.
RX_VK_GIANT_COMPILES=0 is still no bound. The order line, the hand-off line and each deferral line give the
bound in use; docs/development.md says so.

The precompile_handoff fixture gains a hand-off by the stall time with three giants (4 workers: 3) and a
stuck small item holding every worker and most of the list still to hand out: behind the game, on a share
of 2, one giant at a time and the other worker on the small items, every item built once. Plus limit_for
and phase.

Assisted-by: Claude Opus 5.5

Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit acd3a71a2717b1e1bfc83c1952932fdcf1afd9de)
test(vk): test_warm_handoff follows main's warm and dispatch (#252, #190)
All checks were successful
PR description / Description follows the template (pull_request_target) Successful in 8s
658b854052
The fixture cuts prepare_run's warm out of vk_draw.cpp. On main the warm builds
under Intel's compiler gate (#252 IntelDriverGate) through guarded_compute_held,
with the worker caches through create_cached (#190, PrepFast) or create_watched
where compiles are watched (PrecompileStall's compiles_watched). The fixture now
has stand-ins for those; without worker caches, as there, the warm still goes to
guarded_compute_held into pcache and is counted the same way.

The source check for a dispatch's build of an unmade compute pipeline reads
main's name for the dispatch's program, p (the topic's tree had a later
rename to plain).

Assisted-by: Claude Opus 5.5

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Run 17 (MoltenVK 1.4.2, M4 Max): the game thread's first vkQueueSubmit2 waited about four minutes in
MVKCmdBufferImageCopy::encode. The AGX driver compiled a blit program variant for the copy synchronously
(findOrCreateBlitProgramVariant -> MTLCompilerScheduler::buildRequest -> MTLXPCCompilerConnection::tryBoost ->
dispatch_sync), and its request waited on one of Metal's 16 compiler connections behind a giant compute compile
that ended only when the compiler service, crashing under 16 of them, gave up on it.

Under compiler_service (MoltenVK) only:
- Giants::hold: from the hand-off, no compile of more than RX_VK_GIANT_WORDS starts until the game has shown
  RX_VK_GIANT_HOLD_PRESENTS frames (1), or RX_VK_GIANT_HOLD_S (120 s) at most; the ones in flight finish, the
  deferred ones go first after it. Nothing is dropped.
- RX_VK_COMPILE_QOS (utility): behind the game every run, and every giant before it, is built at QoS class
  utility; the rest, which the title waits for, at the thread's own (DEFAULT, the game threads' class).
  MoltenVK dispatches each Metal compile from the calling thread to the default global queue, whose QoS is a
  fallback only, so the request carries the worker's class.

The classic worker and every other driver are unchanged; compile_qos_apply does nothing off macOS.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 4f319a0d9a658d4205d52fe25be645443764ed75)
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 36e04c5eb85f15398ec3a82afdef7205bd37e304)
fix(vk): a giant compile that failed is tried once more alone, not at once among the other giants
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 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 8s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 8s
Third-party parts / As their notes say (pull_request) Successful in 14s
40edc9da95
Runs 17 and 18 (MoltenVK 1.4.2, M4 Max): Metal's compiler service aborted 13 and 7 times on an LLVM
allocation failure while it held giant compute compiles, and each abort takes every compile of that
instance with it (up to 8). A giant that came back VK_ERROR_INITIALIZATION_FAILED was compiled again
at once, with a new pipeline cache, among the giants still in flight.

Under compiler_service (MoltenVK) only, in the precompile's behind_worker: a giant's failed compile is
only said (create_watched, Retry::Later); its worker gives its place back (Giants::solo_ask) and waits
until no other giant is in flight and none is held, while no giant starts (Giants::solo), then
compiles it once more with a new pipeline cache (Retry::Fresh) and says so. Its compute_state stays
claimed meanwhile; a stop cancels its turn. Every other compile, and every other driver, as before.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit b6069bbab0719980b61f26c6b1df469f989f930a)
With RX_VK_COMPILER_SERVICE (MoltenVK only), the precompile goes on behind the game with three or four giant compiles in flight,
and Metal's compiler service ended under 13 and 8 compiles in starts 17 and 18. A compute pipeline a dispatch's build could not
make was failed for good: program_pick never gave its program again, and every dispatch of it was left out.

- precompile_handoff.h Board: a compile a dispatch waits for is wanted from its push until it ends; while one is, the precompile
  starts no giant (Giants::share), deferred ones included; play's own compiles never wait on the giant bound.
- A build that failed while another giant compile was in flight is handed to compute_retry_alone: tried again on a thread of its
  own, once no giant compile is in flight and with none beginning beside it, at most RX_VK_ALONE_TRIES times (2). Its
  compute_state stays 1 meanwhile, so its dispatches are left out and counted as still compiling; one not made alone either is
  failed as before.
- RX_VK_SKIP_LOG=1: each dispatch left out named with its program key and why, once per key and reason (skip_log.h).
- tests/test_loading_stall.py with tests/loading_stall_test.cpp; seg_gathers_test.cpp takes the named skips.

Every other driver: no board call (each behind compiler_service_on()); RX_VK_SKIP_LOG off unless set.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 830c1a023812449db91b6436a3406ff58e5d24e6)
Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 40683ae53e44ba5702f2229a1ddd62cb0cd92b90)
SKIP_DISPATCH and the still-compiling skip ask skip_log_on() before they work out a key or reason (compute_code_key, the
retry's lock), so every driver's skip paths cost what they did before with the log off.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 8885591d407ece67a9511a4f908d0f4bbcdfa94e)
The build's failed() captured board_on to let the want go on a throw; test_jobs_failure runs each [job]() noexcept recovery as
it stands and found one missing. BuildWant now lets the want go when the build ends, by return or throw (before failed()
runs), unless a retry alone took it over; failed() is back to what it was.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 9e1a04021849d9acf945f35400cf87151ddeb0bd)
The semantic merge fix for Integration 28's TitleStall (b6069bba) and
LoadingStall (8885591d) inputs. Each brought a retry alone of its own:
the precompile's failed giant queued in Giants (solo_), play's failed
dispatch build waited on the Board, with two bounds (once; and
RX_VK_ALONE_TRIES, 2) and two wordings. Merged as they were (2c4a9231),
a giant that gave its place back for its try alone (Giants::solo_ask)
stayed counted on the board it shares, so Board::giants() never came
back to 0 and no try alone of play's ever ran after it (shown on a
2c4a9231 archive: board giants 1 after the solo, play's alone_begin
not run within 2 s), and the two could run at once.

Now, with the parent's rules:
- One place: Board's ticket queue (alone_ask/alone_go/alone_again/
  alone_cancel, alone_begin for play) is the only alone slot. Giants'
  solo calls go to the board it shares (a board of its own when it
  shares none); solo_ask ends its giant on the board too. Play's tries
  go ahead of the precompile's; the precompile's also waits for TitleStall's
  first-present hold and for every compile play wants. While any try is
  queued or runs, no precompile giant starts; a giant of play's waits
  for a running try, a small compile does not.
- One count: RX_VK_ALONE_TRIES (2) tries alone per program, the
  precompile's and play's together (Board::alone_left/alone_spend by key
  and variant); used up, neither path tries again, and one line says so
  once with its key (alone_used_up). The precompile's then leaves it to
  its first dispatch, as before; 0 turns both off.
- Every try alone uses a new pipeline cache from the start
  (Retry::Fresh), play's too.
- One wording from three functions (alone_asked, alone_tried,
  alone_used_up); the deferral line names a try alone apart from play's
  want.

New tests/test_retry_alone.py and tests/retry_alone_test.cpp cover the
combined rules; test_loading_stall.py's two source needles that pinned
compute_alone_tries() as play's bound now pin alone_left. Both lanes'
fixtures pass unchanged.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit d89514202b92e65d2ef27eac9e62dce8441e4207)
Main's dispatch build has Intel's worker path (#252: on_worker, made_how,
the quick first build and compute_optimize), which returns early when a
worker of Intel's compiler gate makes the pipeline. BuildWant now opens the
build, so play's want on the compile board is let go on those returns too,
not only on the ones after the compile. (On the Mac on_worker is never set:
the gate is Intel's; the scope makes the rule hold by construction.)

test_loading_stall follows main's code: the build's capture list, the serial
push with main's third argument, the dispatch's program named p (the topic's
tree had a later rename to plain), the order check against the build's own
last compute_state store (main's worker path stores above it), and the
named skips counted against main's dispatch (8; the segmented binding, not on
main, brings five more).

Assisted-by: Claude Opus 5.5

Co-Authored-By: Claude Opus 5.5 <[email protected]>
test(vk): test_intel_quick_compute reads the dispatch's build with the compile board's capture
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 8s
CI / Conventional Commit title (pull_request) Successful in 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 9s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 10s
Third-party parts / As their notes say (pull_request) Successful in 15s
9260653dc8
Main's check (#252) finds the dispatch's build by its capture list. The build
now also captures board_on (the compile board, compiler_service only), so the
check looks for that list; what it checks inside the build is 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]>
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)
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]>
feat(vk): let the segment planner add segments that pay for themselves
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 17s
CI / Conventional Commit title (pull_request) Successful in 16s
CI / Workflow lint (actionlint) (pull_request) Successful in 24s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 20s
Third-party parts / As their notes say (pull_request) Successful in 33s
aae09e20f7
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)
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
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 5s
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
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 28s
fceb934f11
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]>
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
Some checks failed
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
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s
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 6s
Third-party parts / As their notes say (pull_request) Successful in 9s
639ab978be
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]>
Separate macOS from Linux, select arm64 or x86_64 without changing the
Windows/Linux command arrays, and discover LLVM/Apple tools through the
existing prefix, PATH and xcrun. Use the Apple SDK, a 14.4 deployment
target, Mach-O export/map/order/rpath options and system libc++.

Keep the runtime port outside this change. Pick up Darwin host helpers,
exclude the Linux sampler, and test command construction, prefix quoting,
missing tools and real C/C++ dylib loading on native ARM and Rosetta.
The a64-kit adaptation and refreshed tree digest are recorded in its
PROVENANCE.md. No game or generated game content is included.

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 require Rosetta in an ordinary Apple Silicon game-free suite or
skip all native x86_64 evidence on Intel. Keep both simulated target
contracts everywhere and add repeatable --arch options for explicit
dual-route qualification. Keep every binding, export, symbol-order,
C++ runtime and undefined-symbol assertion and all production flags.

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]>
Use canonical POSIX paths only for the virtual /fixture command inputs.
Keep host paths for real discovery, temporary-directory helpers and real
Mach-O runs, and patch exists on the selected namespace path object.

Exercise the real frozen-array comparison under an ntpath host view for
all six configurations. Leave the baseline JSON and production flags
unchanged.

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]>
CONTRIBUTING keeps third_party/ as its upstream copies. Restore a64-kit's
toolchain.py, build_emit.py and PROVENANCE.md to main's bytes and move
their macOS behaviour into nx2/darwin_toolchain.py, an LS file.

On macOS, kit.py and make_game.py install it: toolchain's find, need,
native_target and native_link become its own (tools from EMSDK, PATH,
then xcrun; -arch, a 14.4 target and Apple's SDK; a Mach-O linker), and
build_emit builds liba64emit-<process arch>.dylib, rebuilt when the
compiler, its version or the flags change, as nx2/insn.py overrides
a64emit.translate. system(), architecture(), darwin_sdk() and version()
move with it; build.py, make_game.py, darwin_build.py, host_cpu.py and
zbic.py call them there. Windows and Linux never import the file.

kit.py's import makes the new file one of the lift's sources
(make_game.py lift_sources), so every platform lifts the game again
once, as the a64-kit edit did; build_emit is imported by name for that
walk (tests/test_lift_cache.py).

The command contracts now load main's own toolchain.py, so the 36
Windows/Linux arrays are compared with a64-kit untouched. A new check
proves kit.py and make_game.py install the functions on a Mac. The
test_cpm_discovery stub no longer needs toolchain.system().

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rebased on main 6c9edffe, three of main's changes meet code this branch
moved or added:

- The renderer link (#569): RX_VK_PGO=gen links the Windows back end
  without clang's profile library. renderer_link, which this branch made
  from main's inline link, now says so too, with main's comments.
- No prebuilt renderer (#574): build.prebuilt_renderer() and
  LONGSHOT_VK_PREBUILT are gone, so the macOS preflight checks the
  Vulkan headers always, not a prebuilt dylib's Mach-O slice.
- -march=native and no -mtune (#524): host_mtune is gone. The command
  contract loads host_march and native_cpu, and its frozen Windows/Linux
  arrays are recaptured from main 6c9edffe with --capture-main: the same
  arrays without -mtune=znver2.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Main moved to e090cc48 (#629's third-party objects, #630's work lock). Its
Windows and Linux command arrays, captured with --capture-main from
e090cc48's own expressions, are identical to the frozen ones; only the
recorded base and nx2/build.py's hash change, so the contract names the
main it was checked against.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
test: main's third-party objects test reads build.py with this PR's macOS names
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
PR description / Description follows the template (pull_request_target) Successful in 4s
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
cb8eb9a47d
Main's #629 added tests/test_third_party_objects.py, which runs build.py's
flag_text() in a namespace of its own and finds main()'s Linux link line
by its text. This PR's flag_text() reads DARWIN (as test_shader_stamp_cache's
namespace already gives it, "DARWIN": False) and its link line is
`and (LINUX or DARWIN):`. The test's namespace gets DARWIN False and its
index the PR's line; what it checks is unchanged on every platform.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This PR needs the macOS waits PR (#437); its commits are kept as they are.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Classify Darwin image-local helpers separately from executable-owned wait,
guest-memory, profiler, CPU, control and SDL/Vulkan state. Fail on unknown
helpers instead of broad-globbing host dependencies into the renderer.

Resolve and inspect exact target/minimum-OS dependency inputs before the
standalone build. Relocate and ad-hoc sign only owned copies, preserve the
SDL3 compatibility alias, and deploy the complete hash-bound output manifest.
Qualify otherwise unstamped NASM members only through bounded source/tool,
member/import/ISA evidence and an exact semantic-review receipt.

Keep strict two-level undefined-error links and the frozen Windows/Linux
command arrays. Retain real negative weak-link evidence and verify the real
canonical wait provider with production host flags on arm64 and Rosetta.
Document carried evidence and unproved full-game/dependency qualification.
No experiment-specific lifted-source reuse subsystem or third-party edits.

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]>
SDL2-compat dlopens SDL3 by file name and its own build carries only an
@loader_path rpath. Require LONGSHOT_SDL3 and its internal alias whenever
the selected SDL2 file names SDL3, not only when an rpath spells sdl3; a
classic SDL2 that names no SDL3 still needs none.

The strict-import check read only LC_SYMTAB ordinals. Also decode the
binding dyld applies: LC_DYLD_INFO bind, weak-bind and lazy-bind opcodes
and the LC_DYLD_CHAINED_FIXUPS import table (all three import formats).
Flat-lookup or main-executable imports, and library ordinals that name no
loaded dylib, refuse. C++ weak-definition coalescing (-3) stays two-level.

Validate ar member sizes as plain in-archive decimals before special
members are handled, which also ends the negative-size infinite loop.
A Mach-O member's object is the extent its own load commands describe;
only fewer than eight trailing newlines (Apple ar framing) may follow,
and member SHA-256 covers the object bytes, equal to the original NASM
output. Unknown file-bearing load commands and fat members refuse.

Only alias entries in longshot-macos.json may be symlinks. The maker
clears a dangling staged .new alias with lexists on macOS, so a retry
after a stopped first SDL3 install recovers. Windows/Linux staging is
unchanged.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
dyld keeps the library ordinal across a lazy-stream DONE; DONE only ends
the entry. The reader reset it to 0, so a lazy entry without its own
SET_DYLIB after a flat-lookup (-2) entry was checked as a self import
and admitted while dyld binds it flat. Clear only the symbol on a lazy
DONE and keep the ordinal, with that exact stream as a regression.

Correct the wording for ordinal -3 (BIND_SPECIAL_DYLIB_WEAK_LOOKUP): it
is not two-level. dyld resolves it by a process-wide, load-order search
among images with weak definitions. It stays admitted because ld emits
it only for weak definitions it resolved while linking. The previous
commit message's "stays two-level" was wrong in the same way.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The macOS build flags PR moved system(), architecture(), darwin_sdk()
and version() from a64-kit's toolchain.py into nx2/darwin_toolchain.py,
which main's toolchain.py does not have. The dependency plan and the
post-link check still asked toolchain for them, so on a Mac
dependencies() and post_link() stopped with AttributeError. Ask
darwin_toolchain there, and point the package and preflight tests'
stand-ins at it (the preflight's scope gets _darwin, as make_game.py
names it).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
build(macos): no prebuilt renderer on macOS either, as main has none
All checks were successful
PR description / Description follows the template (pull_request_target) Successful in 5s
db824e4a96
Main builds the Vulkan back end in every build, the AppImage's too, and
removed build.prebuilt_renderer() and LONGSHOT_VK_PREBUILT (#574). The
macOS packaging this branch adds still took an optional prebuilt
longshot_vk.dylib through them. It goes the same way: darwin_build's
dependencies() admits no renderer input, package() always adds the
renderer the build linked, the preflight always checks the Vulkan
headers, and the variable leaves the macOS dependency table.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
fix(macos): a window without Vulkan extensions ends as a clean exit, as main's exit inventory asks
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 4s
PR description / Description follows the template (pull_request_target) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
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
f270209f99
Main's #75 classifies every host process exit (test_crash_record's
inventory, read from the source on every platform): a normal exit marks
the clean end first, a fatal one is listed as fatal. The macOS
ExitProcess(1) after hos_darwin_vulkan_capture() fails is the same kind
of exit as the SDL_CreateWindow failure just above it (a message, then
the end), so it marks the clean end the same way, and the inventory
lists display_sdl.c's second ExitProcess(1). Windows and Linux compile
the same code as before (the exit is inside #if defined(__APPLE__)).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
feat(macos): Metal's shader cache in a folder the caller names
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 5s
CI / Workflow lint (actionlint) (pull_request) Successful in 10s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 10s
Third-party parts / As their notes say (pull_request) Successful in 15s
PR description / Description follows the template (pull_request_target) Successful in 4s
c39e0fb4e7
RX_VK_METAL_CACHE_DIR=<folder>, macOS only and off by default, puts
Metal's own shader cache (compiled functions, libraries, front-end
modules) in that folder instead of Metal's per-user folder under
/var/folders, which a sandbox that keeps every game file in one folder
(tools/ls-run.py) denies. With it denied, every start compiled all of
its pipelines again; the game run's 2,489 "fopen failed for list file:
errno = 1" lines came from that.

gpu/vk/metal_cache.h looks up Metal's exported MTLSetShaderCachePath
and MTLGetShaderCachePath (SPI: in the SDK's Metal.tbd, declared by
WebKit, in no public header) and CoreFoundation's string calls with
dlopen/dlsym, so the renderer links nothing new and has no Objective-C.
load_global asks for the folder before the host loads the Vulkan
library (Metal fixes its folder on first use and silently ignores a
later call), and mkwvk_init logs after create_device whether Metal took
it. A relative or over-long path, a folder that does not exist (the
game never makes it), Metal or a symbol missing, or a folder Metal had
already chosen is logged as "RX_VK_METAL_CACHE_DIR NOT applied" and the
start goes on as without the switch. Unset or empty, nothing is loaded,
called or logged. All of it is under #if defined(__APPLE__): vk_main.cpp's
Windows and Linux text is byte-identical to 5fd1c74b.

Measured with a game-free MoltenVK 1.4.2 probe on the M4 Max (macOS
26.7.1, reports/2026-11-14-metal-cache-dir.md): a synthetic compute
pipeline compiled in ~400 ms with Metal's folder denied, ~213 ms with
a writable empty folder, and 2.6 ms in a second process that found it
there, the folder carried by clonefile; a call after vkCreateDevice was
ignored. Not run in the game.

tests/test_metal_cache_dir.py compiles the real load_global and header
against made-up Metal, CoreFoundation and host loader calls: the order
(the folder is handed over before the library is loaded), the unset,
empty, refused, missing-symbol and mismatch paths, and the source (macOS
only, the check after create_device, the docs row). It fails on
5fd1c74b and on three mutations of the change.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit d7586df37f65b60e507cc3ffb79f38e0f7f7faf0)
The Metal archives' code needs thread_stack.h (mac/thread-stack 7707c634, in
loading-stall through mac/precompile-stall) and the compile board's giants()
that its saver waits on (precompile_handoff.h Board, loading-stall). In
mkwvk_init's Apple block metal_cache::check stays first, then the renderer
threads' stack line.

Assisted-by: Claude Opus 5.5

Co-Authored-By: Claude Opus 5.5 <[email protected]>
RX_VK_METAL_ARCHIVE_DIR=<folder>, macOS only and off by default, keeps
every pipeline MoltenVK makes in Metal binary archives (MTLBinaryArchive)
in that folder, so that a start makes every pipeline an earlier start
compiled without compiling it again.

Metal's own shader cache cannot do that for this game. Each of its two
files (functions.data, libraries.data) holds at most 1 GiB:
MTL_SHADER_CACHE_SIZE and FS_CACHE_SIZE can only lower it. And
libCoreFSCache evicts only entries the running process has neither
looked up nor added, so once a start has touched 1 GiB, everything it
compiles after that is dropped (insert error 0xd3, ignored by Metal).
On runs #17 and #18 both files stopped growing in the first minute of
the precompile, and the large compute programs, compiled last, were
compiled again on every start (static reading of macOS 26.7.1's Metal
and libCoreFSCache: reports/2026-11-14-metal-cache2.md).

An archive is a file the app owns, with no size limit (WWDC20 10615).
MoltenVK 1.4.2 makes none (no newBinaryArchiveWithDescriptor: call, no
VK_KHR_pipeline_binary; MoltenVK issue 1765 is open), and rebuilding it
is not an option here, so gpu/vk/metal_archive.h adds them beneath it.
After the device is made it replaces, on the class of Metal's device,
the two calls MoltenVK 1.4.2 makes every pipeline with
(newComputePipelineStateWithDescriptor:options:completionHandler: and
newRenderPipelineStateWithDescriptor:completionHandler:, MVKPipeline.mm).
MoltenVK makes them inside @synchronized(device), so the replacement
only copies the descriptor and returns; a thread of its own then looks
the pipeline up in the archives earlier starts saved (with
MTLPipelineOptionFailOnBinaryArchiveMiss: no compile), or adds it to a
run archive that only that thread uses meanwhile (the one compile) and
makes it from there. Run archives are saved through a .tmp renamed in
place every 64 pipelines, at once after an add of 2 s or more, after
30 s, and when the window closes (mkwvk_close_asked); one saved at
64 MiB is let go with its memory. Archives live in
<folder>/<macOS build>-<GPU name>/, since an archive serves one pair.

MoltenVK's completion handler is called exactly once on every path:
anything that fails (no add, not found after the add, no run archive)
makes MoltenVK's own call, with the descriptor as MoltenVK made it, so
the pipeline is compiled as without the switch; after 4 such add-path
failures with none added a start stops adding. A descriptor with
archives of its own, a call that asked to fail on a miss, and every
call after the close go to MoltenVK's call unchanged. A refusal at the
start (bad path, no folder, a Metal without the calls) is logged as
"RX_VK_METAL_ARCHIVE_DIR NOT applied" and nothing is replaced. Unset or
empty: nothing is loaded, replaced or logged. Everything is looked up
with dlopen/dlsym (libobjc's C functions, CoreFoundation's NSURL and
NSArray types, the blocks ABI): no new link, no Objective-C source. All
of it is under #if defined(__APPLE__): vk_main.cpp's Windows and Linux
text is byte-identical to 1c53cff1. Not kept: the libraries (MSL to
AIR) MoltenVK compiles before the pipeline call stay in Metal's
library cache with its 1 GiB.

tests/test_metal_archive.py runs the real header against made-up Metal
calls on any OS, and on macOS also its Objective-C binding against
made-up classes registered with libobjc (no Metal, no GPU).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 050961c01610e7fd2bb19d46d3e288330bbd21db)
A pipeline's job runs on its own thread and may still be compiling
when the process exits (the hand-off leaves giants compiling behind the
game, and the window's close saves only the run archives not in use).
As a static object, metal_archive::Archives would be destroyed by
exit() under such a job: its mutex and lists freed while the job uses
them. vk_main.cpp now makes it once with new and never destroys it.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 426ffa5571c28620896a110d8a12bff19dd0e97d)
As tests/test_bug_report.py does: clang on Windows takes no -pthread,
and with -Werror its unused-argument warning would stop the build.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit f4cd09cb873cedd6ec3d05341ded530e9388e567)
Run #21 (reports/2026-11-14-device-lost-21.md) found the RX_VK_METAL_ARCHIVE_DIR hook rewriting every
run archive in full every 64 adds (1,042 saves of 42 archives), never letting go of an archive whose save
failed, leaving 12 GB of Metal's gpuarchiver-* staging in the shader cache folder, saving on the compile
jobs' own threads, and slowing the graphics precompile from about 30 s to about 140 s.

- A run archive takes pipelines until it is estimated at 64 MiB (each kind's bytes a pipeline as this
  start's saves measured them, 64 KiB before), is then saved once and let go; the ones still open when
  the game starts are saved at the close. At most 16 take pipelines at once and those not saved yet hold
  an estimated 1 GiB at most; past either a pipeline is compiled without them, and counted.
- One saver thread at utility QoS saves them one at a time; it waits while a giant compile is in flight
  (the compile board) or the game is in play (its first present), unless 512 MiB or more wait. From the
  game's first frame pipelines are only looked up.
- A failed save is logged with Metal's reason every time, tried once more 5 s later, and let go after
  the second failure. Render pipelines without a fragment function go to archives of their own; once
  one of those is let go for Metal's "stage in pipeline no." they are compiled without the archives.
- Metal stages each archive in gpuarchiver-XXXXXX under MTLGetGPUArchiverCachePath() and leaves it
  after a successful save (MTLCompiler generateMachO deletes it only on failure): the folder the archive
  names through getArchiveIDWithError: is removed after its save.
- A job compiles at its requester's QoS class; the close line adds the time spent adding, making from
  the run archives, looking up and saving, and the staging removed.

tests/test_metal_archive.py covers each defect; docs/development.md's row says the new rules.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit ccebc6a66fd9c3cfc8c1f8c0285c980eb5f71cd6)
fix(macos): compile_board has external linkage, for the Metal archive saver in vk_main.cpp
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 13s
CI / Conventional Commit title (pull_request) Successful in 9s
CI / Workflow lint (actionlint) (pull_request) Successful in 15s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 12s
Third-party parts / As their notes say (pull_request) Successful in 18s
bf5be68150
vk_main.cpp declares mkwvk::compile_board() and calls it from the archive saver's gate, but vk_draw.cpp
defined it inside its anonymous namespace, so the renderer dylib's link failed with an undefined symbol
(found by Integration 30's build). The anonymous namespace now closes before the definition and reopens
after it; nothing else moves.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 54ec563e5ef1674a1c7b3221fffcc835ed9419e1)
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]>
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]>
fix(macos): vk_memory.cpp compiles on macOS: #324's evicted_pools declaration is not Apple's
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 16s
CI / Conventional Commit title (pull_request) Successful in 14s
CI / Workflow lint (actionlint) (pull_request) Successful in 22s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 21s
Third-party parts / As their notes say (pull_request) Successful in 33s
f66d0a87b8
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
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 8s
CI / Workflow lint (actionlint) (pull_request) Successful in 12s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 14s
Third-party parts / As their notes say (pull_request) Successful in 25s
PR description / Description follows the template (pull_request_target) Successful in 5s
61766e3eca
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]>
Carries the vk-admission pull request's commits, which the packaged driver choice needs (host/posix/darwin_vulkan.{c,h} and test_vk_darwin_admission). #479 (mac/build-complete, this branch's start) brings darwin_build.py and darwin_package.py; the two merge without a conflict.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
With LONGSHOT_VULKAN_LOADER (LunarG's macOS SDK loader) the package carries the loader, MoltenVK and, on arm64 builds
with LONGSHOT_KOSMICKRISP, KosmicKrisp, each driver with a loader manifest beside it whose library_path is relative to
the manifest's folder. At startup, after longshot_env.txt and before any thread, the game points VK_DRIVER_FILES at
one packaged manifest: KosmicKrisp on Apple silicon with macOS 26 or later (it needs Metal 4), MoltenVK otherwise
(Rosetta, macOS 14 and 15). RX_VK_DRIVER=kosmickrisp|moltenvk chooses for measurement; a VK_DRIVER_FILES or
VK_ICD_FILENAMES already set is left to the loader. No system loader or driver is searched while a packaged driver is
there. Without the loader the package is as before: MoltenVK loaded directly.

KosmicKrisp is linked with flat (dynamic-lookup) imports: Mesa's weak dispatch entrypoints and CoreGraphics' colour
spaces. The packager admits exactly those for that one input (FLAT_IMPORTS["kosmickrisp"]) and keeps refusing every
other flat import. Its fixes stay keyed to MoltenVK's driver ID: KosmicKrisp gets none of them.

Apple only: hos_main.c's call and every new path are inside defined(__APPLE__); other platforms are unchanged.

On main with #479 and #438: main keys no vendor fix on MoltenVK's driver ID, so test_vendor_fixes.py stays as main has
it (a KosmicKrisp row would check fetch_bounds alone; it goes with the follow-up that keys TexOrder's stored image
reads on KosmicKrisp). The display fixture's driver stub stands beside main's hos_game_layout() stub, without the
kept-startup stub of the Darwin host PR. #479 packages no prebuilt renderer, so that block and the LONGSHOT_VK_PREBUILT
row are not brought back; development.md gets no RX_VK_METAL_CACHE_DIR row (another PR's).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 61774ddb7c189db7e82cbdf7c5258bb122e7fd67)
#479's build takes the architecture and SDK from darwin_toolchain (cd2490c6), so VulkanDriverPackage's build_plan
patches darwin_build.darwin_toolchain.architecture and .darwin_sdk, as the other build fixtures in this file do;
toolchain.need stays patched where it is.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
icd_manifest's dict holds a bool (is_portability_driver) beside strings; Pyright inferred dict[str, str] and refused it.
No change to the written manifest.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 2ed98ef31814a46ef9c023774a1c996f2cc628bd)
Start #20 (both drivers packaged, KosmicKrisp chosen by the macOS 26
rule) compiled 244 of 38,809 pipelines in about 14 minutes; MoltenVK's
whole precompile took 626 s in #19. KosmicKrisp is now used only with
RX_VK_DRIVER=kosmickrisp (its manifest packaged), or when it is the
only packaged driver. Its Apple silicon and macOS 26 range only words
the log. The log reads `vk: Vulkan driver: MoltenVK (default;
RX_VK_DRIVER=kosmickrisp to measure KosmicKrisp): VK_DRIVER_FILES=...`
(the macOS version left the parenthesis). Apple only.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 73ee181e0fd8c29d795b835f18fc178f91794a52)
test(macos): the display fixture counts longshot_env.txt's loads on Apple only
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 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 8s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 8s
Third-party parts / As their notes say (pull_request) Successful in 18s
PR description / Description follows the template (pull_request_target) Successful in 6s
7872c6e71c
The count is read only by the Darwin driver-choice stub, so off Apple the fixture keeps main's empty
load_longshot_env() stub and its text off Apple is main's again.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Under Rosetta 2 the process's page is 4 KiB (getpagesize, vm_page_size, MoltenVK's minImportedHostPointerAlignment)
but the GPU maps the hardware's 16 KiB pages (vm_kernel_page_size). darwin_mem_discard replaced freed guest memory in
4 KiB pieces, which left 16 KiB pages in two memory objects; IOGPU then refused every host-memory import over them
(memoryDescriptorWithClientBuffer failed, MoltenVK VK_ERROR_OUT_OF_DEVICE_MEMORY) and x86_64 run #29 skipped about
1,640 draws a frame (vk_draw.cpp:9540). Natively both pages are 16 KiB, so arm64 never met it.

- darwin_mem_discard replaces only the whole hardware pages inside a free (darwin_mem_hardware_page,
  darwin_mem_whole_pages) and zeroes the rest in place: the same bytes, no split page. Natively unchanged.
- The guest layer's native page is the hardware's (hos_posix_guest_reserve): guest protections, watches and pins
  round to 16 KiB under Rosetta as on arm64. RI-01 showed a 16 KiB page with one 4 KiB part read-only is refused too.
- memory_init aligns imports to the hardware page too (darwin_import::alignment): 16 KiB under Rosetta, as on arm64.
- macOS: a refused import is remembered with its VkResult and the call that refused it (ImportRefusals) and asked
  again only when the host's backing generation for its range changes (new host API, macOS version 24:
  guest_backing_generation, moved by a discard's remap or a protection change) or, for lack of memory, after an
  import was given back. No time-out, no cap; asks answered from memory are no longer counted or logged as
  imports (run #29: about 22 million). Held ranges are reported with the frame statistics, once each a frame.
- Windows and Linux keep ImportHolds' 2 s holds unchanged.

Tests: rosetta_import_geometry_test (the alignment and free arithmetic under a simulated 4 KiB process page),
ImportRefusals cases in import_hold_test, refusals_remembered and the Rosetta route in darwin_import_lifetime_test,
hardware_page_discard in darwin_guest_memory_test (real VM, arm64 and x86_64 under Rosetta), the API-24 layout in
test_vk_darwin_admission and darwin_renderer_startup_test.

Report: reports/2026-11-14-rosetta-import.md (macOS work area).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit f9a2386715a69ae8e289900010627ebe8fc5c792)
fix(macos): the backing generation is Apple host API version 25, after #491's import pins at 24
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 6s
CI / Workflow lint (actionlint) (pull_request) Successful in 14s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 13s
Third-party parts / As their notes say (pull_request) Successful in 13s
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
a0027de2ac
The topic was written on a base whose Apple host API was 23 and gave its new callback, guest_backing_generation,
version 24. On this stack #491 already took 24 for the typed import pin/unpin callbacks (offsets 376/384, size 392),
so the generation is appended after them as version 25: offset 392, size 400.

- gpu/vk/vk_api.h: MKWVK_API_VERSION 25 on Apple, the callback's comment says version 25.
- tests/test_vk_darwin_admission.py: the Apple layout requires the generation at 392, size 400, version 25 (the pick
  kept this base's layout where it conflicted).
- tests/darwin_renderer_startup_test.cpp: version 25, size 400.
- docs/development.md: the Apple renderer ABI is 25; non-Apple stays 22.

The pick before this commit alone does not pass these two ABI checks (its version still says 24 with five callbacks);
a squash merge hides that. Windows and Linux are unchanged: their ABI is still version 22, size 360.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Metal shader cache folder's ask moves into the host's start, which adds a slot to the Apple host table: it follows
#686's layout (API 25, guest_backing_generation at 392, size 400), and #667 holds the ask, the check and the archives
it changes. #686 carries #658 (the renderer core, #491, #431, #647 and #438 #480 below them); #667 carries #653 and the
precompile stack (#666 and below).

Resolved as macos/integrated (79e2e006) has it: vk_draw.cpp dispatch(), the upscale stand-in admitted first (#658's
upscale_stub_demands), then #667's named skips (SKIP_DISPATCH, RX_VK_SKIP_LOG).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Metal shader cache folder is asked in main() right after the Vulkan driver's choice, inside its macOS block, and
the startup test checks that order: both are #654's (host/hos_main.c, host/posix/darwin_vulkan.{c,h},
tests/display_startup_failure_test.cpp). #654 carries #479 (with #453 and #437) and #438.

Resolved as macos/integrated (79e2e006) has it: docs/development.md, the RX_VK_DRIVER row, then RX_VK_METAL_CACHE_DIR and
RX_VK_METAL_ARCHIVE_DIR. Everything else merged as it is.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
RX_VK_METAL_CACHE_DIR was asked for in the renderer's load_global. In a
windowed start the renderer's thread starts only after the main thread
has started SDL's video, loaded the Vulkan library and made the window,
and by then Metal had already fixed its shader cache's folder: start #35
(the first windowed run) logged "RX_VK_METAL_CACHE_DIR NOT applied:
Metal's shader cache is .../C//com.apple.metal", so its warm folder went
unused and every compile went to Metal's default folder, which the
runner's sandbox denies (112,837 "fopen failed for list file: errno = 1"
in its os_log; precompile 1,464 s against windowless #32's 391 s).

main() now asks right after longshot_env.txt (which may set the switch)
and the Vulkan driver's choice, before SDL, the Vulkan library or any
other thread (host/posix/darwin_vulkan.c hos_darwin_metal_cache_ask). The
renderer has the host check after create_device, where it checked
before, through a new macOS MkwVkHost slot, metal_cache_check
(MKWVK_API_VERSION 25 on macOS). The logic and its log lines are
metal_cache.h's, moved to host/posix/darwin_metal_cache.h and written in
C for the host: only where and when the folder is asked for changes.
Unset or empty, nothing is loaded, called or logged, as before; players,
who do not set it, are unchanged (Metal's own per-user folder).

tests/test_metal_cache_dir.py compiles the host's real ask and check and
the header (also as C) against made-up Metal and CoreFoundation calls and
checks main()'s order; tests/display_startup_failure_test.cpp runs the
real main() and requires the ask once, after longshot_env.txt and the
driver's choice and before SDL, a window or a thread. The ABI fixtures
move to API 25 (400 bytes, the slot at 392) and the startup fixture calls
the wired slot. Both checks fail on 79e2e006.

(Picked into mac/windowed-metal-cache, on mac/metal-cache2 with
mac/rosetta-import and mac/kosmickrisp merged. On this stack #686 already
took Apple host API 25 for the backing generation (offset 392, size
400), so the API number and the ABI fixtures' layout in this pick are
#686's where they met (vk_api.h's define, test_vk_darwin_admission.py's
offsets and line, darwin_renderer_startup_test.cpp's layout check); the
next commit gives the slot the next free number, 26. Until it, the slot
sits past the fixtures' 400 bytes.)

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit c77008a8982610d5db559b37426402657480263b)
fix(macos): the Metal cache check is Apple host API 26, after the Rosetta imports' backing generation at 25
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
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
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 10s
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
5c37ec101d
On this stack #686 (mac/rosetta-import) took Apple host API 25 for guest_backing_generation (offset 392, size 400).
metal_cache_check, appended after it, is the next version: MKWVK_API_VERSION 26 on macOS, the slot at offset 400, the
Apple table 408 bytes. Windows and Linux keep version 22, size 360.

- gpu/vk/vk_api.h: the macOS define 26, the slot's comment "(macOS version 26)".
- tests/test_vk_darwin_admission.py: the Apple ABI asks for metal_cache_check at 400, size 408, version 26; its line and
  docstring say so.
- tests/darwin_renderer_startup_test.cpp: the exact API26 layout (version 26, 408 bytes) and its PASS line.
- tests/test_metal_cache_dir.py: the macOS version raised to 26 for the slot.

The pick before this commit has the slot past #686's 400 bytes at version 25, so test_vk_darwin_admission and
test_darwin_renderer fail on its layout there; they pass here.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mooki force-pushed mac/windowed-metal-cache from 5c37ec101d
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
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
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 10s
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
to 976bb0ac6d
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 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 9s
2026-11-15 15:05:42 +00:00
Compare
Author
Member

Folded into #849 (fix(macos): MoltenVK's precompile and pipelines on Metal)

Folded into #849 (fix(macos): MoltenVK's precompile and pipelines on Metal)
mooki closed this pull request 2026-11-15 18:08:52 +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 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 9s

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!786
No description provided.