feat(vk): admit guest buffers per operation and import over-cap spans as segments on macOS #658
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8015c9a
beanstalk: approved @ 8d361dc
beanstalk: approved @ 9d8356e
beanstalk: approved @ abf2227
beanstalk: approved @ d82f202
beanstalk: approved @ e5f1e40
beanstalk: approved @ ee8ab5f
ci: approved
good first issue
gpu/amd
gpu/igpu
gpu/intel
gpu/nvidia
help wanted
kind
bug
kind
chore
kind
docs
kind
feature
kind
performance
kind
question
needs measurements
platform/linux
platform/macos
platform/steam-deck
platform/windows
preview
priority
critical
priority
high
priority
low
regression
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!658
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/renderer-core"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 inmemory_initunder__APPLE__), each operation's guest buffers are admittedbefore 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).
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, thetextures of every copy and clear, and
copy_b2t's buffer side (vk_xfer.cpp).(
uniform_bind_size,storage_binding,texel_buffer_extent,buffer_extent, the*_byteshelpers invk_draw.cpp), including the GPU unswizzle's source (texture_unswizzle_source,vk_texture.cpp), the upscaler'sactor motion (
temporal_actor_demands,vk_temporal.cpp) and the diagnostic readers.tests/test_resolver_coverage.pyholds every
gpu_buffer_bind/gpu_buffercall site to its resolver demand and its seam.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.
seg_plan,seg_commit,gpu_span_bind/SpanRefinvk_memory.cpp,vk_internal.h): a plan past thecap or over a domain becomes a segmented domain cut by
span_segments::plan; each segment is an ordinary pool. A viewinside 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.
retires imports; one view per byte of an Apple spare backing;
bg_drainpublishes no background import over thisepoch's retired memory or across a domain; a parallel batch admits its uniform updates in order; admission merges stop at
1 GiB so
mirror_makecan 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).
hos_posix_audio_openis named once on stderr (host/posix/win32_posix.c, Apple block).Windows and Linux
Alias isolation is never set off Apple, so
memory_admitreturns Ready without locking and every seam is one branch. Whatstill runs there, all checked to keep main's behaviour:
pool_lookupis split intoimport_for/plan_import/import_planned: the same steps in the same order.imported_addtakes itsPoolby move (the join of #431 and #491,0030e14a): the same pool at the same key.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 extrasmall copy per replayed operation.
actor_readsgivesactor_motionmain's byte counts, including main'scount << itypefor 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_usedrefuses it when the draw is noted) unless its model key matches a draw ofanother 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 ontoe090cc48andaa95d35f(11d09b9a) is thebranch restacked on #491's
7bf15899plus one test commit; the tree before that commit (508fcb41) is identical to theearlier head
444e05c1(66981742), where the runs below were made (their folders keep that name);checks-508fcb41and
cases-508fcb41repeat the fixture set and the cases after the restack(
reports/PubWave-files/renderer-core-restack/), and main'stest_split_ub_flushpasses at this head on botharchitectures (
proof/rc-split-ub-*.log; at508fcb41it 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, retirementand 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 main6c9edffe("call to 'wait' is ambiguous" in its fixture) and passes with #454'sone-line fixture fix (
rc-mirror-trim-with-454-fixture).The review fixes' cases (
cases-444e05c1), 21 cases × 2 architectures, the controlsbg-elsewhere(off alias isolation)and
mirror-offincluded: 42 of 42 pass.Failing without this PR's code, with the tests at the head:
6da98bb2, now0030e14a: main + #647 + #491 + #431 + the join), alias admission, admissionhandoff, 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);05b032fb, thene52d7149), 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);508fcb41, thenec49f7fa), resolver coverage fails on the actor motion ("temporal_actor_demands does not admit actor_motion'sgpu_buffer_bind(reads.gpu[0], …)";
neg-checks-rework-2808063d); before the #250 fixture commit (nowe9098f0c), theadmission fixtures did not compile on main's #250 (
checks-dcf50595,cases-dcf50595).Whole renderer files as objects with main's renderer flags (
vflagsplus-arch, MacOSX SDK, 14.4), arm64 and x86_64: thesix
gpu/vk.cpp files this PR's own commits change (vk_draw,vk_memory,vk_replay,vk_temporal,vk_texture,vk_xfer) compile whole at508fcb41(reports/PubWave-files/compile/renderer-core-508fcb41/; the last commit changes atest 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_poolsfix (its7bf15899),vk_memory.cppdid not build for macOS at #491's earlier head0f54963d(
tu-491-0f54963d.out).The one host hunk, 4 lines in
win32_posix.c'sdarwin_audio_resolve(5717d397), sits in #491's macOS audio block, whichcompiles 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 andx86_64).
A named run of the 224 game-free tests that read the renderer files this PR touches (
named-444e05c1, the suite's ownrunner 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 at7628abcc(thedependencies 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.cis the same. So theWindows 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.
aa95d35f, atf8d4e51a(11d09b9abefore the rebases ontoe090cc48andaa95d35f):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.pyon wave B's mergedb0e2e16, 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).78ec1c47, which carries every MacPR 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_64under 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 onolder 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:
f8d4e51a, it carries #647, #491 and #431) and mainaa95d35fthe same way(the 8 files above change text there), run
python -I -B tests/run_all.pyand 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/integrated, no more[mvk-error]lines, andvk: admission handoff N (… ms …)lines whose wait stays a smallpart of the frame (by hand: the owner's Mac).
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_VERSIONchange inthis PR's own commits (#491 below it carries the macOS API 24 bump).
Reviewer notes
7628abcc..11d09b9a(15 commits); everything before is #647 and the merges of #491 and #431. In order:0030e14ajoins #431 and #491 (below), then5717d397(NOACCESS pages, audio name);6e765c8dthe admission (seam, resolver, reuse, handoff, cohorts),e1251140SegMem,05b032fbthree extractions;a575aba4..53a68299: the seven review fixes, each with its reproducer (the CoreFix cases);e9098f0c,508fcb41and11d09b9a: follow-ups to main, below.Start with
memory_admit,admit_planandadmit_applyinvk_memory.cpp, thenreplay_admitinvk_replay.cpp.table's reach). The branch is #647's head plus merges of #491
7bf15899(c70e4444) and #431 (7628abcc). #491 and #431 conflictwith each other in
vk_memory.cpp(imported_add/imported_takeagainst the move-only macOS pool and its queue slotfirst),
test_mirror_gpu_only.pyandtest_parallel_rb_refresh.py: the merge keeps #491's order with #431's helpers.0030e14athen makesimported_addtakePool&&and teachestest_import_reach.pyandtest_darwin_renderer.pythemacOS pool; without it neither builds on macOS. #431 could take that commit itself.
-xfrommacos/renderer-core(9e383263 ae1d7812 9fb9b3b4) andmacos/core-fix(39542f89f29396c7 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.cppkeeps #324's retire grace besidealias_refinimports_give_back;vk_temporal.cpp'spose copy follows #59's placed/copied branches;
test_import_reach.pykeeps #431's source check and prefetch case andadds the admission excerpts and
--arch(itsmain()call needed no restoring: #431's copy has it);actor_readsasabove.
05b032fb: #431 already cuts the table's helpers intotest_import_windowsandtest_parallel_rb_refresh; only theadmission parts are added.
53a68299: thetrims_waitingstub sits beside #491'ssparse_wantedstub.6c9edffe, each its own commit at the end:e9098f0c(#250'ssparse_wantedin three admissionfixtures),
508fcb41(#59: the actor motion's resolver demands a pose's bones and origin only whereactor_motioncopies them, both through
pose_read_at_draw) and11d09b9a(main #189'stest_split_ub_flushreadsflush_ub_updatesby its signature, which the admission gave an
end,SIZE_MAXby default: the check follows it and still requires thewrites from
ub_first_of_recordingto the last update). The macOSevicted_poolscompile fix found here is now #491's own(
7bf15899).vk_draw.cppdoes not compile for macOS on main6c9edffe(main's #2649posix_fallocateinpipeline_cache_file.h);#476 fixes that, and with #476 in the scratch tree it compiles here (above).
0030e14a(the join) is what makes them build together on macOS: it stays on this branch, andboth PRs' Reviewer notes say "needed when both land".
which build on this.
git merge-tree). With the open macOS PRs: #490 inhost/posix/win32_posix.candtests/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
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), headf8d4e51a, on #431497be2d4, on #491695f6859, on #64752def683, 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 frombefore the rebase.
python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (On the wave's merge of every macOS PR built with this one,453eb678, with this PR then at444e05c1: 553 passed, 32 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the failures are main's own excepttest_split_ub_flush, fixed at this head by11d09b9a(passes on both architectures);test_collect_widenow passes; the new tests pass andtest_darwin_audio_service, Darwin-only, is skipped by the runner;reports/PubWave-files/waveA/. After the rebase onto mainaa95d35f(headf8d4e51a): wave B's mergedb0e2e16, 569/32/18 against main aa95d35f's 543/33/17, no new failure.)third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here.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 main5e072fc8and 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]>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]>11d09b9abb61766e3eca61766e3ecaf8d4e51ad5WIP: feat(vk): admit guest buffers per operation and import over-cap spans as segments on macOSto feat(vk): admit guest buffers per operation and import over-cap spans as segments on macOSFolded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)
Pull request closed