feat(vk): add the inert contract for segmented binding consumers #662
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 @ 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!662
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/seg-contract"
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: low · Tested on: macOS 26.7.1, Apple M4 Max (MoltenVK 1.4.2), game-free at this head · Needs: Windows, Linux
Why
On a Mac, one GPU memory can hold at most about 41.7 GB, and the game's memory window is larger. #658 makes the renderer
import a guest range that one operation needs and that is larger than that cap as several adjacent pieces (segments). A
buffer or texture view that crosses a cut between two segments cannot be bound as one buffer, so today it is left unbound,
and whatever reads it on the GPU reads nothing. Three follow-up PRs (#663, #663, #663, best reviewed together) bind such
views piece by piece, and #674 makes the shaders cope with descriptors that are absent. They were written side by side
against one frozen set of interfaces. This PR is that set: declarations, stubs, fields and constants, with nothing calling
them yet. It changes no behaviour on any system; it only gives the follow-ups a reviewed shape to fill in. It is part of
the macOS port. See #420.
What changed
The interfaces the segmented consumers code against, each with its contract in its doc comment (lifetime, threads and
locks, what an empty result means, how failures are reported). Nothing new has a caller: every module the back end emits,
every descriptor layout and every push range is what it was. Two review fixes to #658's
gpu_span_bindcome with it.gpu/vk/vk_internal.handgpu/vk/vk_memory.cpp(SegMem's consumer interface):gpu_span_any(): is any segmented domain live? A relaxed count stored inseg_commit(domains are kept for the run,so once true it stays true). Off macOS no domain is ever made, so it stays false.
gpu_span():gpu_buffer's flavour ofgpu_span_bind(import buffers, no written marks, no mirror).SpanLayout::bind_stamp(gpu_bind_stamp()as the layout was read): a segment can be imported again with its cutskept, so
cut_genkeys piece counts andcut_genplus the stamp key piece buffers.span_unused_piece's semantics ({null_storage, 0, 1}, set once by #663).SegArenaRef,seg_arena_take,seg_gather,seg_scatter, with stub bodies ingpu/vk/vk_draw.cppafter
stream_allocthat give no room and log at error level (rate-limited and counted); #663 fills them.gpu_span_bind(#658's renderer-core review, P2-3 and P3.2): each piece carries its buffer's deviceaddress (the import's, or the mirror's when it binds the mirror; 0 only with
bufferDeviceAddressoff), filled in onepass under the import lock; the one-piece path stores the translated guest address, not the caller's virtual one.
gpu_buffer_binditself is unchanged.gpu/vk/vk_texture.hand.cpp:texel_view_pieces(returns 1) andtexel_piece_views(returns false andlogs), the texel piece views #663 fills;
texel_codec::Planis forward-declared, so the header adds no back-end include.gpu/vk/vk_shader.h:Program::presence_words, in what was padding.PRESENCE_PUSH_OFFSET(88,static_asserted) inbackend/spirv/emit_spirv.h,Profile::emulate_null_descriptorsinprofile.h,Bindings::presenceandpresence_readwith the bit numbering rulein
backend/bindings.h.docs/segmented-binding.md(new): the Apple-only gate, the contract summary, one section per consumer.tests/test_segmented_domains.pyandtests/segmented_domains_test.cppexecute the contract: the live count (0, 1,2 → 1 when one operation merges two disjoint domains, 0),
gpu_span's pieces, piece addresses with the fixture'sgpu_buffer_bindreturning none as the real one does, the bind stamp across a re-import, a virtual address's guest.tests/test_resolver_coverage.pylistsgpu_span_bindandgpu_spansites too (scoped togpu/vk:gpu/renderhasan unrelated
rx::gpu_span), checks that each follows its owngpu_buffer_bind/gpu_bufferin the same function,and splits its site list into a base block plus one block per consumer.
tests/test_seg_shader.pyaccepts a base tree of this back end's generation and then compares every module (it usedto refuse one with texel codecs).
Measurements
Verdict: passes; nothing to measure for speed. The contract is inert: every module the back end emits is byte for byte
its base's on both CPU routes, and the only executed change is the address and guest fix inside
gpu_span_bind'ssegmented path, which no consumer calls until #663, on macOS only.
Hardware: macOS 26.7.1, Apple M4 Max, 64 GB, Apple clang 21 (game-free: no GPU, no game)
Scene: none (interfaces and stubs; the proof is byte identity and the contract's fixture)
Method:
tests/test_seg_shader.py --base-treean archive of this PR's base (c512cc78: #658 with #649 merged), arm64 and--arch x86_64(Rosetta);tests/test_segmented_domains.pyat the head and with--sourcethe base (it must fail there).Byte comparisons and exact fixture checks: one run per route.
How to reproduce:
python -I -B tests/test_seg_shader.py --headers <SPIRV-Headers/include> --spirv-val spirv-val --base-tree <git archive of the base> [--arch x86_64];python -I -B tests/test_segmented_domains.py --cxx clang++ --headers <Vulkan-Headers/include> --arch arm64 --arch x86_64 [--source <base tree>]Baseline: the "before" column is this PR's base, #658 with #649, because the contract extends their interfaces (main has
neither SegMem nor the texel codecs to compare with).
c512cc78ccc6b071test_segmented_domains, arm64 and x86_64--sourcethe base): nogpu_span_any, nogpu_span, no piece addresses, no bind stamp, a virtual guesttest_resolver_coverageFlags and settings
None in this PR's own commit. The diff against main also shows the switches of the PRs it stacks on, theirs to list:
HOS_KEEP_FREED_PAGES,HOS_ZERO_ALL_PAGES,RX_NVN_WATCH_AFTER,RX_NVN_WATCH_ALL,RX_NVN_WATCH_TEXELS_SRC(#480),RX_VK_LIBRARY(#438),RX_VK_DLSS_ACTORS,RX_VK_GPU_DUMP,RX_VK_MESH_IND_COUNT_WORD,RX_VK_NO_UPSCALE_STUB(#658)and
RX_VK_TEXEL_CODECS(#649).Testing
Tested: on macOS 26.7.1, Apple M4 Max, at
ccc6b071, this PR's head before the rebases ontoe090cc48andaa95d35f(main
6c9edffe, #658's stack, #649's stack, this PR), game-free, every run under the shared check slot with a fresh HOMEand TMPDIR:
tests/test_seg_shader.py --base-treethe base, arm64 and x86_64: exit 0, 57 of 57 byte-identical (as in Measurements).test_darwin_renderer,test_darwin_audio_service,test_parallel_init_order,test_import_reach,test_alias_admission,test_admission_handoff,test_resolver_coverage,test_segmented_domains,test_span_segments, andtest_import_buffer_bindingandtest_texel_buffer_ownershipper arch: 13 of 14 exit 0.test_mirror_trimfails as onmain
6c9edffe(main's own macOS failure: "call to 'wait' is ambiguous" intests/test_mirror_trim.cpp; #454 fixes it).test_segmented_domainswith--sourcethe base: fails on both routes as required (above).vk_memory.cpp,vk_draw.cpp,vk_texture.cpp,vk_shader.cpp,vk_progcache.cppand the back end'semit_spirv.cppandspirv_emit_context.cpp, compiled with main's renderer flags (nx2/build.py'svflagsplus-arch, main's non-Windows-fPIC -I host/posix -D_GNU_SOURCE) for arm64 and x86_64, at the base and at this head:14 of 14 each, exit 0. Every tree got #492's two Darwin fixes and a compile-only prelude declaring
posix_fallocateandposix_fadvise, which main's renderer calls and macOS lacks (#476 gives them Darwin paths). (Compiled at this stack'sfirst build,
939d0f4fon #658's earlier head444e05c1;gpu/,nx2/andhost/are byte-identical at this head andits base: the restack changed
tests/test_split_ub_flush.pyonly.)95fa06d4, history): mutants m1 to m5 (no address pass, the virtual guest, no livecount store, no bind stamp, pieces through
gpu_buffer_bind) each caught on both routes, two planted unlistedgpu_spansites caught by the coverage check, its full suite with no new failure,sizeof(Program)320 before andafter on both routes. This PR's back-end headers,
vk_texture.h, the docs andtest_segmented_domains.pyare byte forbyte the topic's; the other files differ only where #658 and main moved the surrounding code.
reports/PubWave-files/PubSeg/(proof/p23-*.log,checks-p23-ccc6b071/,neg-segdom-base-c512cc78/,objects-*.out,stamps.txt,offapple-p23-*.txt).aa95d35f, at0401eeb2(ccc6b071before 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).Not tested: the game at this head (per-PR game runs are not planned; nothing new runs until #663). The stubs' log lines
were compiled, not executed. The mutants were not rerun at this head. Windows and Linux: not built on Windows or Linux (9
changed renderer and back-end files differ from the base's off Apple too,
unifdef -U__APPLE__); its test request covers it.Must be tested:
0401eeb2, it carries #658 and #649 with the PRs they stack on) and mainaa95d35fthe same way and start each to the title, and runpython -I -B tests/run_all.pyon Linux, expecting the buildto compile this PR's every-platform files (each differs from the base's with every
__APPLE__branch resolved off:gpu/vk/vk_internal.h,vk_memory.cpp,vk_draw.cpp,vk_texture.{h,cpp},vk_shader.h, and the back end'sbindings.h,emit_spirv.h,profile.h),tests/test_segmented_domains.pyandtests/test_resolver_coverage.pytopass, the shaders prepared once more (a new translator stamp) and #649's
vk: texel codecs: off (…)line, and everyother line to match main's (by hand: test request T-DHTU)
Risks
What could break: nothing calls the new interfaces yet.
gpu_span_bind's address pass and its translated guest runonly on a domain, which only macOS makes and only #663's consumers reach.
seg_commitstores one more relaxed counter. Ifa later consumer misread the contract, the stubs fail loudly (error log, counted), never silently.
Turning it off: revert the commit (there is nothing to switch off).
Compatibility: every platform's shaders are prepared again once: the back-end headers (
bindings.h,emit_spirv.h,profile.h) are in the translator's stamp, which goes fromc3bf927f4a1bb7f4(#649) todc271e7fc99bb36c; thetranslated modules are byte for byte the same.
kProgramCacheFormatstays 2. Nothing changes in saves,longshot_options.inior the vk ABI (MKWVK_API_VERSION).Reviewer notes
ccc6b071alone (c512cc78..ccc6b071, one commit,-xfrommacos/seg-contract95fa06d4): the doc commentsin
vk_internal.hfirst (they are the contract), thengpu_span_any,gpu_spanandgpu_span_bind's address pass invk_memory.cpp, thentest_segmented_domains.gpu_span_bind,SpanLayout) and #649 (texel_codec::Plan). This branch is #658's head(
11d09b9a) with #649 (c16eac09) merged (c512cc78, no conflicts); until they merge this PR's diff shows theircommits too. #663, #663 and #663 (one PR,
mac/seg-consumers) and #674 build on it.6c9edffeis the base of the whole stack).tests/test_seg_shader.py's docstring, against thearray_pointer_callsparagraph of #648's msl-arrayptr port; both kept, this PR's paragraph first.git merge-treeat this head): this PR's own commit conflicts withnone. Conflicts inherited from its base: #454 and #490 (
tests/test_collect_wide.py; #490 alsohost/posix/win32_posix.c, from #491 in the stack) and three other lanes' local branches ingpu/vk/vk_draw.cpp.__APPLE__gate on a declaration),and the translator stamp changes; behaviour does not change anywhere. Not split for that reason.
reports/2026-11-14-*.md(the port's design notes), which are not in this repository.Issues
See #420
Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), head0401eeb2, on #649c5cbd39d, on #658f8d4e51a, without a conflict; this PR's own commits, old → new:c512cc78→5d169c3b,ccc6b071→0401eeb2;the commit names elsewhere in this description are from before the rebase.
python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (On the wave's merge8217be31(msl-depth, which carries this PR, + mvk-pushlimits): 556 passed, 32 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the failures are main's own excepttest_gpu_hang_watch, which failed under load and passes on its own on that tree;test_collect_widenow passes; 25 new tests pass;test_darwin_audio_service, Darwin-only, is skipped by the runner;reports/PubWave-files/waveB/. After the rebase onto mainaa95d35f(head0401eeb2): 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.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]>A guest span larger than one device allocation (the cap on MoltenVK/M4 Max is 41,747,087,360 bytes) is bound as pieces: one descriptor per allocation segment, at the descriptor's own binding (reports/2026-11-14-full-range-design.md sections 4.1-4.4). This is the shader half; the draw side (descriptors, layout, gathers) comes later. - runtime_info.h: SegEntry {Storage|Table|TexBuf|ImgBuf, desc, pieces} and RuntimeInfo::segmented. - Back end: a segmented storage buffer is an array of pieces at its binding (a table count * pieces, entry k's piece j at k * pieces + j); its loads, stores and atomics call DontInline helpers made after main, which find the piece by the pieces' OpArrayLength in order, with constant indices. The global memory fallbacks sum the pieces' lengths for a segmented descriptor's bound end and branch to its piece (single, native candidate, table written out and in the loop). Texel and image buffers are arrays too, their fetches, reads, writes and atomics through helpers that pick the piece by OpImageQuerySize, and a texel buffer's size query sums the pieces. ImageQuery was declared for every module already. With RuntimeInfo::segmented empty nothing new is made: the SPIR-V is byte-identical. - vk_shader: SegSig (entries, hash) on Program, seg_sig and seg_variant; program_segmented finds or translates the variant of a plain program with plain's texture questions and answers and checks its descriptors are plain's; program_pick, mesh_program_locked, program_find_plain, loaded_variants and portable_variant keep the plain and segmented variants apart; every stage's RuntimeInfo::segmented comes from Program::seg. - vk_progcache: a segmented variant is never written to the program cache nor listed among the loaded programs. - tests/test_seg_shader.py + tests/seg_shader_ir.cpp: synthetic IR programs through the real back end, plain and segmented; structure checks, spirv-val (Vulkan 1.3, uniform buffer standard layout), and with --base-tree a byte comparison of the plain modules against an older tree. Five source-reading checks follow the new Program field and the atomics' new branch. The translator stamp changes with this (eden/, vk_shader.cpp, sizeof(Program)): every program cache is rebuilt once. Land it in the same release as the vk_api.h change that already does so. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit b7928876dab846485f667c1c934d34d25ecef035)The interfaces the four segmented-consumer lanes (SegBind, SegXfer, TexelDesc, NullShader; reports/2026-11-14-seg-consumer-plan.md section 3) code against, frozen before they fork. Inert: no caller of anything new, no SPIR-V, layout, push range or stream change, sizeof(Program) unchanged. - vk_internal.h / vk_memory.cpp, SegMem's consumer interface: gpu_span_any() (a relaxed count of the live domains, stored in seg_commit; domains are kept for the run), gpu_span (gpu_buffer's flavour of gpu_span_bind: import buffers, no written marks, no mirror), SpanLayout::bind_stamp (gpu_bind_stamp(): a segment can be imported again with its cuts kept, so cut_gen alone never keys piece buffers), span_unused_piece's semantics ({null_storage, 0, 1}), and the SegArena API (SegArenaRef, seg_arena_take, seg_gather, seg_scatter; stub bodies after stream_alloc that give no room and log, SegBind's to fill). - Review fixes in the same interface (renderer-core review P2-3, P3.2): every piece of gpu_span_bind carries its buffer's device address (the import's or the mirror's; 0 only with bufferDeviceAddress off), and its one-piece path stores the translated guest address, not a virtual one. - vk_texture.h/.cpp: texel_view_pieces / texel_piece_views, stubs that return 1 and false (TexelDesc's to fill). - vk_shader.h: Program::presence_words, in what was padding. - Back end: PRESENCE_PUSH_OFFSET (88), Profile::emulate_null_descriptors, Bindings::presence / presence_read with the bit numbering rule. - tests: test_resolver_coverage lists gpu_span_bind / gpu_span sites too (scoped to gpu/vk: gpu/render has an rx::gpu_span of its own), checks that gpu_span follows its own gpu_buffer, and splits SITES into SITES_BASE plus one block per lane. test_segmented_domains executes the contract (the count 0, 1, 2 -> 1 on a merge, 0; gpu_span's pieces; addresses with the fixture's gpu_buffer_bind returning none as the real one does; the bind stamp; a virtual address's guest). test_seg_shader accepts a base tree of this back end's generation and compares every module. - docs/segmented-binding.md: the gate (every new behaviour Apple-only, other platforms byte-identical) and one heading per lane. Conflict on this branch (not with main): tests/test_seg_shader.py's docstring, against the array_pointer_calls paragraph #P20's msl-arrayptr port added; both kept, this paragraph first. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit 95fa06d4e6526601bd2e1bc88c2f8f5720855350)ccc6b071c14c16dbc4cc4c16dbc4cc0401eeb237WIP: feat(vk): add the inert contract for segmented binding consumersto feat(vk): add the inert contract for segmented binding consumersFolded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)
Pull request closed