feat(vk): admit guest buffers per operation and import over-cap spans as segments on macOS #658

Closed
mooki wants to merge 46 commits from mooki/Longshot:mac/renderer-core into main
Member

Type: feat · Risk: medium · Tested on: macOS 26.7.1, Apple M4 Max (arm64, and x86_64 under Rosetta; game-free checks only) · Needs: Windows, Linux (build and game-free tests), a game run on a Mac

Why

The macOS port (#420). On MoltenVK, two host imports of the same guest bytes read stale data within one submission: a
draw that reads through one import does not see what another import of the same bytes was given earlier in the same
command buffer. The renderer imports guest memory as it binds it, and the same bytes can end up in several imports (a
chunk, then a larger import over it). Guest views can also be larger than one device allocation (41,747,087,360 bytes on
this Mac), and then nothing bound them at all. This PR keeps one memory per guest byte on macOS ("alias isolation"): it
works out every guest buffer an operation will bind before it binds the first, imports what is missing in one go, and
waits for recorded work before it replaces an import that work may still reach. A span larger than one allocation is
imported as segments, cut where the segment planner (#647) says. See #420.

What changed

On macOS only (alias_isolation, set in memory_init under __APPLE__), each operation's guest buffers are admitted
before the operation binds them; off macOS every one of these paths returns at its first test and binding is unchanged
(details under "Windows and Linux" below).

  • seam: memory_admit / replay_admit (gpu/vk/vk_memory.cpp, gpu/vk/vk_replay.cpp) on the replay thread: draw(),
    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 (vk_xfer.cpp).
  • resolver: one side-effect-free description of what each binding path binds, built from the binders' own helpers
    (uniform_bind_size, storage_binding, texel_buffer_extent, buffer_extent, the *_bytes helpers in
    vk_draw.cpp), including the GPU unswizzle's source (texture_unswizzle_source, vk_texture.cpp), the upscaler's
    actor motion (temporal_actor_demands, vk_temporal.cpp) and the diagnostic readers. tests/test_resolver_coverage.py
    holds every gpu_buffer_bind / gpu_buffer call site to its resolver demand and its seam.
  • reuse and handoff: 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. Otherwise the pass ends, uniform updates up to this point are flushed, the command buffer is
    submitted and waited for, the old memories are retired and kept until their work is done, the new ones imported and the
    page table updated, then recording goes on. Different operations whose union passes the cap get one memory each.
  • segments (seg_plan, seg_commit, gpu_span_bind / SpanRef in vk_memory.cpp, vk_internal.h): a plan past the
    cap or over a domain becomes a segmented domain cut by span_segments::plan; each segment is an ordinary pool. A view
    inside one segment binds; one across a cut is refused, counted and logged until the segment consumers (#663, texel-desc +
    seg-bind + seg-xfer) bind it piece by piece.
  • review fixes (the second half of the commits): the page table keeps every byte it reached mapped when an admission
    retires imports; one view per byte of an Apple spare backing; bg_drain publishes no background import over this
    epoch's retired memory or across a domain; a parallel batch admits its uniform updates in order; admission merges stop at
    1 GiB so mirror_make can still mirror them; a segment keeps a crossing import's bytes as leftovers clipped at the cuts;
    every new import over a retired import's bytes inherits its share of that import's VRAM mirror (Macs with mirrors on).
  • also: a committed NOACCESS guest page no longer refuses the whole Apple import (the driver decides); a missing
    hos_posix_audio_open is named once on stderr (host/posix/win32_posix.c, Apple block).

Windows and Linux

Alias isolation is never set off Apple, so memory_admit returns Ready without locking and every seam is one branch. What
still runs there, all checked to keep main's behaviour:

  • pool_lookup is split into import_for / plan_import / import_planned: the same steps in the same order.
  • imported_add takes its Pool by move (the join of #431 and #491, 0030e14a): the same pool at the same key.
  • helpers factored out of binders (storage_binding, uniform_used, buffer_extent, unswizzle_shape,
    mesh_convert_bytes, stats_query_may_begin): the same expressions, in the same order.
  • flush_ub_updates(cmd, end = SIZE_MAX), op_admit, texture_admit, replay_handoff_cmd: return at once; one extra
    small copy per replayed operation.
  • actor motion: actor_reads gives actor_motion main's byte counts, including main's count << itype for the indices.
    The macOS stack's copy bound nothing for an index type outside 0..2; that would change Windows and Linux too, and 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 it is left out of this PR.

No shader stamp file changes, so no platform rebuilds its shader caches for this PR.

Measurements

Verdict: nothing to measure yet: no game run on this branch (named under Testing); its game-free tests are under Testing.

Flags and settings

None added. Alias isolation has no switch: it is on on macOS and off elsewhere. RX_VK_POOL_END_IMPORTED (A/B, existing)
is ignored on macOS under alias isolation, logged once, because it imported to answer a resolver's question.

Testing

Tested: on macOS 26.7.1, Apple M4 Max, Apple clang 21.0.0, arm64 and x86_64 under Rosetta where a test takes
--arch, each case in its own process. The head before the rebases onto e090cc48 and aa95d35f (11d09b9a) is the
branch restacked on #491's 7bf15899 plus one test commit; the tree before that commit (508fcb41) is identical to the
earlier head 444e05c1 (66981742), where the runs below were made (their folders keep that name); checks-508fcb41
and cases-508fcb41 repeat the fixture set and the cases after the restack
(reports/PubWave-files/renderer-core-restack/), and main's test_split_ub_flush passes at this head on both
architectures (proof/rc-split-ub-*.log; at 508fcb41 it stopped with a ValueError,
rc-split-ub-before-508fcb41.log):

  • The renderer-core fixture set, 14 checks (checks-444e05c1): the Darwin renderer (the real Apple import, retirement
    and spare-backing code), audio service, parallel init, import reach, alias admission, admission handoff, resolver
    coverage, segmented domains, span segments, and the import and texel originals per architecture: 13 pass; the 14th,
    test_mirror_trim, fails as on main 6c9edffe ("call to 'wait' is ambiguous" in its fixture) and passes with #454's
    one-line fixture fix (rc-mirror-trim-with-454-fixture).

  • The review fixes' cases (cases-444e05c1), 21 cases × 2 architectures, the controls bg-elsewhere (off alias isolation)
    and mirror-off included: 42 of 42 pass.

  • Failing without this PR's code, with the tests at the head:

    • on the tree below this PR's own commits (then 6da98bb2, now 0030e14a: main + #647 + #491 + #431 + the join), alias admission, admission
      handoff, segmented domains and resolver coverage fail, e.g. "an import over memory recorded work still reaches was
      made in the same handoff epoch", "an import over the cap, or two live memories over the same bytes", and two
      bindings no resolver covers (neg-checks-base-6da98bb2, exit 0 with --expect-fail);
    • on the tree before the review fixes (now 05b032fb, then e52d7149), every one of the 19 review-fix cases fails on both architectures
      (e.g. "a byte the page table reached before the admission is unreachable after it (cohort)", "a background import
      was published across a domain's cut", "only the first segment over a retired import inherited its GPU-only mirror
      data") while both controls pass (neg-cases-e52d7149);
    • before the #59 rework (now 508fcb41, then ec49f7fa), resolver coverage fails on the actor motion ("temporal_actor_demands does not admit actor_motion's
      gpu_buffer_bind(reads.gpu[0], …)"; neg-checks-rework-2808063d); before the #250 fixture commit (now e9098f0c), the
      admission fixtures did not compile on main's #250 (checks-dcf50595, cases-dcf50595).
  • Whole renderer files as objects with main's renderer flags (vflags plus -arch, MacOSX SDK, 14.4), arm64 and x86_64: the
    six gpu/vk .cpp files this PR's own commits change (vk_draw, vk_memory, vk_replay, vk_temporal, vk_texture,
    vk_xfer) compile whole at 508fcb41 (reports/PubWave-files/compile/renderer-core-508fcb41/; the last commit changes a
    test only), with #476, #492 and #490 merged into the scratch tree only, as main's renderer needs them on macOS. Before #491 took the
    evicted_pools fix (its 7bf15899), vk_memory.cpp did not build for macOS at #491's earlier head 0f54963d
    (tu-491-0f54963d.out).

  • The one host hunk, 4 lines in win32_posix.c's darwin_audio_resolve (5717d397), sits in #491's macOS audio block, which
    compiles for macOS only with #490 merged, and #490 rewrites that block (the known #490 × #491 conflict); the same 4 lines are in
    the integrated macOS builds since Integration 5 and compiled and linked there (latest DiagBuild2 at 6962f8fb, arm64 and
    x86_64).

  • A named run of the 224 game-free tests that read the renderer files this PR touches (named-444e05c1, the suite's own
    runner and environment): 217 passed, 8 failed. Six of the eight fail on main 6c9edffe's full run too
    (test_pcache_header_read, test_pipeline_play_cache, test_pipeline_cache_save, test_mirror_trim,
    test_vram_budget_row, test_program_cache_one_variant); test_present_aspect_vulkan (needs a Vulkan loader to link)
    and test_precompile_compute_guard (Windows exceptions) are skipped in a full run and fail only when named, as on main.

  • #647's planner test at its own head, both architectures (that PR's body).

  • Off macOS the text compiled changes: with unifdef -U__APPLE__ on each changed C/C++ file at 7628abcc (the
    dependencies merged) and at this head, 8 of the 9 renderer and host files differ (gpu/vk/vk_draw.cpp, vk_internal.h,
    vk_memory.cpp, vk_replay.cpp, vk_temporal.cpp, vk_texture.cpp, vk_texture.h, vk_xfer.cpp: the moved helpers,
    the seams and the admission code, compiled there though not run); host/posix/win32_posix.c is the same. So the
    Windows and Linux builds are in Must be tested.

Evidence: reports/PubWave-files/PubCore/ in the Mac's work area; the runs use the harnesses of the topic lanes
(RendererCore2, CoreFix), unchanged.

  • After the rebase onto main aa95d35f, at f8d4e51a (11d09b9a before the rebases onto e090cc48 and aa95d35f):
    Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they
    apply) gave the same results as before, but the renderer startup fixture's line, now Apple25 typed host callback wiring (it carries #491, whose pins main's #729 moved to 25); python -I -B tests/run_all.py on wave B's merge
    db0e2e16, which carries this head: 569/32/18 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac,
    no new failure (reports/2026-11-15-rebase-sweep-3.md).
  • macOS game runs with this PR in the stack: arm64, windowless: run E9a on I41 78ec1c47, which carries every Mac
    PR through #708 and the I41 fixes: a start from the field-dawn save to a 1,197 s hold in play, the runner's 12 of 12
    checks passed, 0 skipped draws in the hold, 0 XPC errors, a clean exit (reports/2026-11-15-mac-perf-E9.md); x86_64
    under Rosetta 2: run #34 on I38 3f872b0c: 16 m 52 s of play, 19 of 19 checks, 0 skipped draws, 1,906 draws a frame,
    no device loss, a clean exit (reports/2026-11-14-mac-startup-34-x86_64.md).

Not tested: a game run on this branch (the macOS game runs so far used macos/integrated, where these commits sit on
older main: alias isolation and SegMem since Integration 5, the review fixes since Integration 10 and 13); the handoff's
frequency and wait in play; any GPU probe of the handoff, the page table after it, or segment binding; Windows and Linux
(not built there, no compiler for them here: its test request covers the build, run_all and a scene). Main's render thread (#588, "Threaded rendering") with alias isolation: Auto stays off on
Apple silicon, where the pools have no mirrors, but turns on on a Mac with mirrors and 12 or more logical processors, and
the admission was written for the replay on the submitting thread.

Must be tested:

  • Windows and Linux, any GPU: build this PR (head f8d4e51a, it carries #647, #491 and #431) and main aa95d35f the same way
    (the 8 files above change text there), run python -I -B tests/run_all.py and play Hyrule Field from a save on each,
    expecting the build to succeed, no run_all failure that main does not have, and the same frame time and no new binding
    warning against main (by hand: test request T-C9TT)
  • macOS, Apple silicon: a game run from the field-dawn save, expecting no stale or flickering geometry against
    macos/integrated, no more [mvk-error] lines, and vk: admission handoff N (… ms …) lines whose wait stays a small
    part of the frame (by hand: the owner's Mac).
  • macOS with Threaded rendering On (RX_VK_ASYNC=1): the same run, expecting no device loss or abort (by hand).

Risks

What could break: on macOS, a binding the resolver does not describe would bind outside its admission (the coverage
check guards the call sites, not their arguments); a handoff in the middle of a pass costs a submit and a wait, so
admissions that hand off often would slow frames; a view across a segment cut is not bound until the segment consumers
land. On Windows and Linux: the moved code above.

Turning it off: no switch; revert the PR.

Compatibility: Nothing changes for players: no save, options, shader stamp, cache format or MKWVK_API_VERSION change in
this PR's own commits (#491 below it carries the macOS API 24 bump).

Reviewer notes

  • Review 7628abcc..11d09b9a (15 commits); everything before is #647 and the merges of #491 and #431. In order:
    • 0030e14a joins #431 and #491 (below), then 5717d397 (NOACCESS pages, audio name);
    • 6e765c8d the admission (seam, resolver, reuse, handoff, cohorts), e1251140 SegMem, 05b032fb three extractions;
    • a575aba4..53a68299: the seven review fixes, each with its reproducer (the CoreFix cases);
    • e9098f0c, 508fcb41 and 11d09b9a: follow-ups to main, below.
      Start with memory_admit, admit_plan and admit_apply in vk_memory.cpp, then replay_admit in vk_replay.cpp.
  • Stacking: depends on #647 (the planner), #491 (import pins; it carries #480, #438, #436 and #478) and #431 (the import
    table's reach). The branch is #647's head plus merges of #491 7bf15899 (c70e4444) and #431 (7628abcc). #491 and #431 conflict
    with each other in vk_memory.cpp (imported_add/imported_take against the move-only macOS pool and its queue slot
    first), test_mirror_gpu_only.py and test_parallel_rb_refresh.py: the merge keeps #491's order with #431's helpers.
    0030e14a then makes imported_add take Pool&& and teaches test_import_reach.py and test_darwin_renderer.py the
    macOS pool; without it neither builds on macOS. #431 could take that commit itself.
  • Commits cherry-picked with -x from macos/renderer-core (9e383263 ae1d7812 9fb9b3b4) and macos/core-fix (39542f89
    f29396c7 0c1d2aea 755ac7e8 9ccc7c19 596f0ff4 6aa7c2af), plus suite-fix's 64a84e4d. Left out: 53b8914d (the lookback is
    #431) and the seg-plan merge be98d9e4 (#647 is below). Each changed pick says so in its message:
    • 6e765c8d: vk_memory.cpp keeps #324's retire grace beside alias_ref in imports_give_back; vk_temporal.cpp's
      pose copy follows #59's placed/copied branches; test_import_reach.py keeps #431's source check and prefetch case and
      adds the admission excerpts and --arch (its main() call needed no restoring: #431's copy has it); actor_reads as
      above.
    • 05b032fb: #431 already cuts the table's helpers into test_import_windows and test_parallel_rb_refresh; only the
      admission parts are added.
    • 53a68299: the trims_waiting stub sits beside #491's sparse_wanted stub.
  • Follow-ups to main 6c9edffe, each its own commit at the end: e9098f0c (#250's sparse_wanted in three admission
    fixtures), 508fcb41 (#59: the actor motion's resolver demands a pose's bones and origin only where actor_motion
    copies them, both through pose_read_at_draw) and 11d09b9a (main #189's test_split_ub_flush reads flush_ub_updates
    by its signature, which the admission gave an end, SIZE_MAX by default: the check follows it and still requires the
    writes from ub_first_of_recording to the last update). The macOS evicted_pools compile fix found here is now #491's own
    (7bf15899).
  • vk_draw.cpp does not compile for macOS on main 6c9edffe (main's #2649 posix_fallocate in pipeline_cache_file.h);
    #476 fixes that, and with #476 in the scratch tree it compiles here (above).
  • When #431 and #491 both land, 0030e14a (the join) is what makes them build together on macOS: it stays on this branch, and
    both PRs' Reviewer notes say "needed when both land".
  • Review together with #662 (the segment contract) and #663 (texel-desc + seg-bind + seg-xfer, the segment consumers),
    which build on this.
  • Conflicts with main: none (git merge-tree). With the open macOS PRs: #490 in host/posix/win32_posix.c and
    tests/test_collect_wide.py, the same two files #491 conflicts in; none with #431, #437, #438, #479, #480, #491, #492.

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 request T-C9TT.)
  • Rebased on current main: on aa95d35f (#761, 2026-11-15), head f8d4e51a, on #431 497be2d4, on #491
    695f6859, on #647 52def683, without a conflict; this PR's own commits, old → new: c70e4444 → fddeb8b4,
    7628abcc → 15070012, 0030e14a → a1d08d92, 5717d397 → e6e92a90, 6e765c8d → a27bee68, e1251140 →
    dd4f1304, 05b032fb → 6e77a383, a575aba4 → 3fbfca08, 64fb40e1 → 616eacb8, cd889d21 → 04e00832,
    04e17046 → a3913a64, 74ee40cb → b8d16a20, 7a51f4a5 → aab3fd8a, 53a68299 → 73bbb89a, e9098f0c →
    6fa302cc, 508fcb41 → e0941ccc, 11d09b9a → f8d4e51a; the commit names elsewhere in this description are from
    before the rebase.
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named). (On the wave's merge of every macOS PR built with this one, 453eb678, with this PR then at 444e05c1: 553 passed, 32 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the failures are main's own except test_split_ub_flush, fixed at this head by 11d09b9a (passes on both architectures); test_collect_wide now passes; the new tests pass and test_darwin_audio_service, Darwin-only, is skipped by the runner; reports/PubWave-files/waveA/. After the rebase onto main aa95d35f (head f8d4e51a): wave B's merge db0e2e16, 569/32/18 against main aa95d35f's 543/33/17, no new failure.)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

When it applies

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened.
**Type:** feat · **Risk:** medium · **Tested on:** macOS 26.7.1, Apple M4 Max (arm64, and x86_64 under Rosetta; game-free checks only) · **Needs:** Windows, Linux (build and game-free tests), a game run on a Mac ## Why The macOS port (#420). On MoltenVK, two host imports of the same guest bytes read stale data within one submission: a draw that reads through one import does not see what another import of the same bytes was given earlier in the same command buffer. The renderer imports guest memory as it binds it, and the same bytes can end up in several imports (a chunk, then a larger import over it). Guest views can also be larger than one device allocation (41,747,087,360 bytes on this Mac), and then nothing bound them at all. This PR keeps one memory per guest byte on macOS ("alias isolation"): it works out every guest buffer an operation will bind before it binds the first, imports what is missing in one go, and waits for recorded work before it replaces an import that work may still reach. A span larger than one allocation is imported as segments, cut where the segment planner (#647) says. See #420. ## What changed On macOS only (`alias_isolation`, set in `memory_init` under `__APPLE__`), each operation's guest buffers are admitted before the operation binds them; off macOS every one of these paths returns at its first test and binding is unchanged (details under "Windows and Linux" below). - seam: `memory_admit` / `replay_admit` (`gpu/vk/vk_memory.cpp`, `gpu/vk/vk_replay.cpp`) on the replay thread: `draw()`, `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 (`vk_xfer.cpp`). - resolver: one side-effect-free description of what each binding path binds, built from the binders' own helpers (`uniform_bind_size`, `storage_binding`, `texel_buffer_extent`, `buffer_extent`, the `*_bytes` helpers in `vk_draw.cpp`), including the GPU unswizzle's source (`texture_unswizzle_source`, `vk_texture.cpp`), the upscaler's actor motion (`temporal_actor_demands`, `vk_temporal.cpp`) and the diagnostic readers. `tests/test_resolver_coverage.py` holds every `gpu_buffer_bind` / `gpu_buffer` call site to its resolver demand and its seam. - reuse and handoff: 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. Otherwise the pass ends, uniform updates up to this point are flushed, the command buffer is submitted and waited for, the old memories are retired and kept until their work is done, the new ones imported and the page table updated, then recording goes on. Different operations whose union passes the cap get one memory each. - segments (`seg_plan`, `seg_commit`, `gpu_span_bind` / `SpanRef` in `vk_memory.cpp`, `vk_internal.h`): a plan past the cap or over a domain becomes a segmented domain cut by `span_segments::plan`; each segment is an ordinary pool. A view inside one segment binds; one across a cut is refused, counted and logged until the segment consumers (#663, texel-desc + seg-bind + seg-xfer) bind it piece by piece. - review fixes (the second half of the commits): the page table keeps every byte it reached mapped when an admission retires imports; one view per byte of an Apple spare backing; `bg_drain` publishes no background import over this epoch's retired memory or across a domain; a parallel batch admits its uniform updates in order; admission merges stop at 1 GiB so `mirror_make` can still mirror them; a segment keeps a crossing import's bytes as leftovers clipped at the cuts; every new import over a retired import's bytes inherits its share of that import's VRAM mirror (Macs with mirrors on). - also: a committed NOACCESS guest page no longer refuses the whole Apple import (the driver decides); a missing `hos_posix_audio_open` is named once on stderr (`host/posix/win32_posix.c`, Apple block). ### Windows and Linux Alias isolation is never set off Apple, so `memory_admit` returns Ready without locking and every seam is one branch. What still runs there, all checked to keep main's behaviour: - `pool_lookup` is split into `import_for` / `plan_import` / `import_planned`: the same steps in the same order. - `imported_add` takes its `Pool` by move (the join of #431 and #491, `0030e14a`): the same pool at the same key. - helpers factored out of binders (`storage_binding`, `uniform_used`, `buffer_extent`, `unswizzle_shape`, `mesh_convert_bytes`, `stats_query_may_begin`): the same expressions, in the same order. - `flush_ub_updates(cmd, end = SIZE_MAX)`, `op_admit`, `texture_admit`, `replay_handoff_cmd`: return at once; one extra small copy per replayed operation. - actor motion: `actor_reads` gives `actor_motion` main's byte counts, including main's `count << itype` for the indices. The macOS stack's copy bound nothing for an index type outside 0..2; that would change Windows and Linux too, and 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 it is left out of this PR. No shader stamp file changes, so no platform rebuilds its shader caches for this PR. ## Measurements **Verdict:** nothing to measure yet: no game run on this branch (named under Testing); its game-free tests are under Testing. ### Flags and settings None added. Alias isolation has no switch: it is on on macOS and off elsewhere. `RX_VK_POOL_END_IMPORTED` (A/B, existing) is ignored on macOS under alias isolation, logged once, because it imported to answer a resolver's question. ## Testing **Tested:** on macOS 26.7.1, Apple M4 Max, Apple clang 21.0.0, arm64 and x86_64 under Rosetta where a test takes `--arch`, each case in its own process. The head before the rebases onto e090cc48 and aa95d35f (`11d09b9a`) is the branch restacked on #491's `7bf15899` plus one test commit; the tree before that commit (`508fcb41`) is identical to the earlier head `444e05c1` (`66981742`), where the runs below were made (their folders keep that name); `checks-508fcb41` and `cases-508fcb41` repeat the fixture set and the cases after the restack (`reports/PubWave-files/renderer-core-restack/`), and main's `test_split_ub_flush` passes at this head on both architectures (`proof/rc-split-ub-*.log`; at `508fcb41` it stopped with a ValueError, `rc-split-ub-before-508fcb41.log`): - The renderer-core fixture set, 14 checks (`checks-444e05c1`): the Darwin renderer (the real Apple import, retirement and spare-backing code), audio service, parallel init, import reach, alias admission, admission handoff, resolver coverage, segmented domains, span segments, and the import and texel originals per architecture: 13 pass; the 14th, `test_mirror_trim`, fails as on main 6c9edffe ("call to 'wait' is ambiguous" in its fixture) and passes with #454's one-line fixture fix (`rc-mirror-trim-with-454-fixture`). - The review fixes' cases (`cases-444e05c1`), 21 cases × 2 architectures, the controls `bg-elsewhere` (off alias isolation) and `mirror-off` included: 42 of 42 pass. - Failing without this PR's code, with the tests at the head: - on the tree below this PR's own commits (then `6da98bb2`, now `0030e14a`: main + #647 + #491 + #431 + the join), alias admission, admission handoff, segmented domains and resolver coverage fail, e.g. "an import over memory recorded work still reaches was made in the same handoff epoch", "an import over the cap, or two live memories over the same bytes", and two bindings no resolver covers (`neg-checks-base-6da98bb2`, exit 0 with `--expect-fail`); - on the tree before the review fixes (now `05b032fb`, then `e52d7149`), every one of the 19 review-fix cases fails on both architectures (e.g. "a byte the page table reached before the admission is unreachable after it (cohort)", "a background import was published across a domain's cut", "only the first segment over a retired import inherited its GPU-only mirror data") while both controls pass (`neg-cases-e52d7149`); - before the #59 rework (now `508fcb41`, then `ec49f7fa`), resolver coverage fails on the actor motion ("temporal_actor_demands does not admit actor_motion's gpu_buffer_bind(reads.gpu[0], …)"; `neg-checks-rework-2808063d`); before the #250 fixture commit (now `e9098f0c`), the admission fixtures did not compile on main's #250 (`checks-dcf50595`, `cases-dcf50595`). - Whole renderer files as objects with main's renderer flags (`vflags` plus `-arch`, MacOSX SDK, 14.4), arm64 and x86_64: the six `gpu/vk` .cpp files this PR's own commits change (`vk_draw`, `vk_memory`, `vk_replay`, `vk_temporal`, `vk_texture`, `vk_xfer`) compile whole at `508fcb41` (`reports/PubWave-files/compile/renderer-core-508fcb41/`; the last commit changes a test only), with #476, #492 and #490 merged into the scratch tree only, as main's renderer needs them on macOS. Before #491 took the `evicted_pools` fix (its `7bf15899`), `vk_memory.cpp` did not build for macOS at #491's earlier head `0f54963d` (`tu-491-0f54963d.out`). - The one host hunk, 4 lines in `win32_posix.c`'s `darwin_audio_resolve` (`5717d397`), sits in #491's macOS audio block, which compiles for macOS only with #490 merged, and #490 rewrites that block (the known #490 × #491 conflict); the same 4 lines are in the integrated macOS builds since Integration 5 and compiled and linked there (latest DiagBuild2 at `6962f8fb`, arm64 and x86_64). - A named run of the 224 game-free tests that read the renderer files this PR touches (`named-444e05c1`, the suite's own runner and environment): 217 passed, 8 failed. Six of the eight fail on main 6c9edffe's full run too (`test_pcache_header_read`, `test_pipeline_play_cache`, `test_pipeline_cache_save`, `test_mirror_trim`, `test_vram_budget_row`, `test_program_cache_one_variant`); `test_present_aspect_vulkan` (needs a Vulkan loader to link) and `test_precompile_compute_guard` (Windows exceptions) are skipped in a full run and fail only when named, as on main. - `#647`'s planner test at its own head, both architectures (that PR's body). - Off macOS the text compiled changes: with `unifdef -U__APPLE__` on each changed C/C++ file at `7628abcc` (the dependencies merged) and at this head, 8 of the 9 renderer and host files differ (`gpu/vk/vk_draw.cpp`, `vk_internal.h`, `vk_memory.cpp`, `vk_replay.cpp`, `vk_temporal.cpp`, `vk_texture.cpp`, `vk_texture.h`, `vk_xfer.cpp`: the moved helpers, the seams and the admission code, compiled there though not run); `host/posix/win32_posix.c` is the same. So the Windows and Linux builds are in Must be tested. Evidence: `reports/PubWave-files/PubCore/` in the Mac's work area; the runs use the harnesses of the topic lanes (RendererCore2, CoreFix), unchanged. - **After the rebase onto main aa95d35f**, at `f8d4e51a` (`11d09b9a` before the rebases onto e090cc48 and aa95d35f): Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they apply) gave the same results as before, but the renderer startup fixture's line, now `Apple25 typed host callback wiring` (it carries #491, whose pins main's #729 moved to 25); `python -I -B tests/run_all.py` on wave B's merge `db0e2e16`, which carries this head: 569/32/18 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure (`reports/2026-11-15-rebase-sweep-3.md`). - **macOS game runs with this PR in the stack:** arm64, windowless: run E9a on I41 `78ec1c47`, which carries every Mac PR through #708 and the I41 fixes: a start from the field-dawn save to a 1,197 s hold in play, the runner's 12 of 12 checks passed, 0 skipped draws in the hold, 0 XPC errors, a clean exit (`reports/2026-11-15-mac-perf-E9.md`); x86_64 under Rosetta 2: run #34 on I38 `3f872b0c`: 16 m 52 s of play, 19 of 19 checks, 0 skipped draws, 1,906 draws a frame, no device loss, a clean exit (`reports/2026-11-14-mac-startup-34-x86_64.md`). **Not tested:** a game run on this branch (the macOS game runs so far used `macos/integrated`, where these commits sit on older main: alias isolation and SegMem since Integration 5, the review fixes since Integration 10 and 13); the handoff's frequency and wait in play; any GPU probe of the handoff, the page table after it, or segment binding; Windows and Linux (not built there, no compiler for them here: its test request covers the build, run_all and a scene). Main's render thread (#588, "Threaded rendering") with alias isolation: Auto stays off on Apple silicon, where the pools have no mirrors, but turns on on a Mac with mirrors and 12 or more logical processors, and the admission was written for the replay on the submitting thread. **Must be tested:** - Windows and Linux, any GPU: build this PR (head f8d4e51a, it carries #647, #491 and #431) and main aa95d35f the same way (the 8 files above change text there), run `python -I -B tests/run_all.py` and play Hyrule Field from a save on each, expecting the build to succeed, no run_all failure that main does not have, and the same frame time and no new binding warning against main (by hand: test request T-C9TT) - macOS, Apple silicon: a game run from the field-dawn save, expecting no stale or flickering geometry against `macos/integrated`, no more `[mvk-error]` lines, and `vk: admission handoff N (… ms …)` lines whose wait stays a small part of the frame (by hand: the owner's Mac). - macOS with Threaded rendering On (`RX_VK_ASYNC=1`): the same run, expecting no device loss or abort (by hand). ## Risks **What could break:** on macOS, a binding the resolver does not describe would bind outside its admission (the coverage check guards the call sites, not their arguments); a handoff in the middle of a pass costs a submit and a wait, so admissions that hand off often would slow frames; a view across a segment cut is not bound until the segment consumers land. On Windows and Linux: the moved code above. **Turning it off:** no switch; revert the PR. **Compatibility:** Nothing changes for players: no save, options, shader stamp, cache format or `MKWVK_API_VERSION` change in this PR's own commits (#491 below it carries the macOS API 24 bump). ## Reviewer notes - Review `7628abcc..11d09b9a` (15 commits); everything before is #647 and the merges of #491 and #431. In order: - `0030e14a` joins #431 and #491 (below), then `5717d397` (NOACCESS pages, audio name); - `6e765c8d` the admission (seam, resolver, reuse, handoff, cohorts), `e1251140` SegMem, `05b032fb` three extractions; - `a575aba4`..`53a68299`: the seven review fixes, each with its reproducer (the CoreFix cases); - `e9098f0c`, `508fcb41` and `11d09b9a`: follow-ups to main, below. Start with `memory_admit`, `admit_plan` and `admit_apply` in `vk_memory.cpp`, then `replay_admit` in `vk_replay.cpp`. - Stacking: depends on #647 (the planner), #491 (import pins; it carries #480, #438, #436 and #478) and #431 (the import table's reach). The branch is #647's head plus merges of #491 `7bf15899` (`c70e4444`) and #431 (`7628abcc`). #491 and #431 conflict with each other in `vk_memory.cpp` (`imported_add`/`imported_take` against the move-only macOS pool and its queue slot first), `test_mirror_gpu_only.py` and `test_parallel_rb_refresh.py`: the merge keeps #491's order with #431's helpers. `0030e14a` then makes `imported_add` take `Pool&&` and teaches `test_import_reach.py` and `test_darwin_renderer.py` the macOS pool; without it neither builds on macOS. #431 could take that commit itself. - Commits cherry-picked with `-x` from `macos/renderer-core` (9e383263 ae1d7812 9fb9b3b4) and `macos/core-fix` (39542f89 f29396c7 0c1d2aea 755ac7e8 9ccc7c19 596f0ff4 6aa7c2af), plus suite-fix's 64a84e4d. Left out: 53b8914d (the lookback is #431) and the seg-plan merge be98d9e4 (#647 is below). Each changed pick says so in its message: - `6e765c8d`: `vk_memory.cpp` keeps #324's retire grace beside `alias_ref` in `imports_give_back`; `vk_temporal.cpp`'s pose copy follows #59's placed/copied branches; `test_import_reach.py` keeps #431's source check and prefetch case and adds the admission excerpts and `--arch` (its `main()` call needed no restoring: #431's copy has it); `actor_reads` as above. - `05b032fb`: #431 already cuts the table's helpers into `test_import_windows` and `test_parallel_rb_refresh`; only the admission parts are added. - `53a68299`: the `trims_waiting` stub sits beside #491's `sparse_wanted` stub. - Follow-ups to main 6c9edffe, each its own commit at the end: `e9098f0c` (#250's `sparse_wanted` in three admission fixtures), `508fcb41` (#59: the actor motion's resolver demands a pose's bones and origin only where `actor_motion` copies them, both through `pose_read_at_draw`) and `11d09b9a` (main #189's `test_split_ub_flush` reads `flush_ub_updates` by its signature, which the admission gave an `end`, `SIZE_MAX` by default: the check follows it and still requires the writes from `ub_first_of_recording` to the last update). The macOS `evicted_pools` compile fix found here is now #491's own (`7bf15899`). - `vk_draw.cpp` does not compile for macOS on main 6c9edffe (main's #2649 `posix_fallocate` in `pipeline_cache_file.h`); #476 fixes that, and with #476 in the scratch tree it compiles here (above). - When #431 and #491 both land, `0030e14a` (the join) is what makes them build together on macOS: it stays on this branch, and both PRs' Reviewer notes say "needed when both land". - Review together with #662 (the segment contract) and #663 (texel-desc + seg-bind + seg-xfer, the segment consumers), which build on this. - Conflicts with main: none (`git merge-tree`). With the open macOS PRs: #490 in `host/posix/win32_posix.c` and `tests/test_collect_wide.py`, the same two files #491 conflicts in; none with #431, #437, #438, #479, #480, #491, #492. ## 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 request T-C9TT.) - [x] Rebased on current `main`: on aa95d35f (#761, 2026-11-15), head `f8d4e51a`, on #431 `497be2d4`, on #491 `695f6859`, on #647 `52def683`, without a conflict; this PR's own commits, old → new: `c70e4444` → `fddeb8b4`, `7628abcc` → `15070012`, `0030e14a` → `a1d08d92`, `5717d397` → `e6e92a90`, `6e765c8d` → `a27bee68`, `e1251140` → `dd4f1304`, `05b032fb` → `6e77a383`, `a575aba4` → `3fbfca08`, `64fb40e1` → `616eacb8`, `cd889d21` → `04e00832`, `04e17046` → `a3913a64`, `74ee40cb` → `b8d16a20`, `7a51f4a5` → `aab3fd8a`, `53a68299` → `73bbb89a`, `e9098f0c` → `6fa302cc`, `508fcb41` → `e0941ccc`, `11d09b9a` → `f8d4e51a`; the commit names elsewhere in this description are from before the rebase. - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). (On the wave's merge of every macOS PR built with this one, `453eb678`, with this PR then at `444e05c1`: 553 passed, 32 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the failures are main's own except `test_split_ub_flush`, fixed at this head by `11d09b9a` (passes on both architectures); `test_collect_wide` now passes; the new tests pass and `test_darwin_audio_service`, Darwin-only, is skipped by the runner; `reports/PubWave-files/waveA/`. After the rebase onto main aa95d35f (head `f8d4e51a`): wave B's merge `db0e2e16`, 569/32/18 against main aa95d35f's 543/33/17, no new failure.) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### When it applies - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened.
Every scan down vk_memory.cpp's import table by start (pool_lookup's
covering import and merge plan, bg_drain, prefetch_chunks,
import_in_background, memory_note_gpu_write and memory_guest_imported)
stopped once an import ended more than 1 GiB below its address, on the
belief that no import is larger than that. pool_lookup's own merge escape
leaves an import above 1 GiB over the smaller ones inside it ("left
overlapping"), and the device allows imports up to max_import_size: a scan
stopped at a small import inside a large one, missed the large one, and
the same bytes were imported again, two memories over the same bytes.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

No assertion, case or expected output changes.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Off alias isolation nothing of this runs.

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

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

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

The three print what they printed at 58c5c5aa.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
mooki force-pushed mac/renderer-core from 11d09b9abb
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 8s
PR description / Description follows the template (pull_request_target) Successful in 4s
to 61766e3eca
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
2026-11-15 07:25:53 +00:00
Compare
mooki force-pushed mac/renderer-core from 61766e3eca
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
to f8d4e51ad5
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
CI / Conventional Commit title (pull_request) Successful in 5s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 7s
CI / Workflow lint (actionlint) (pull_request) Successful in 9s
Third-party parts / As their notes say (pull_request) Successful in 10s
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 5s
2026-11-15 13:54:46 +00:00
Compare
mooki changed title from WIP: feat(vk): admit guest buffers per operation and import over-cap spans as segments on macOS to feat(vk): admit guest buffers per operation and import over-cap spans as segments on macOS 2026-11-15 14:18:07 +00:00
Author
Member

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

Folded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)
mooki closed this pull request 2026-11-15 18:07:54 +00:00
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
CI / Conventional Commit title (pull_request) Successful in 5s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 7s
CI / Workflow lint (actionlint) (pull_request) Successful in 9s
Third-party parts / As their notes say (pull_request) Successful in 10s
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 5s

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