feat(vk): bind storage and texel descriptors as piece arrays in the SPIR-V back end #648
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!648
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/seg-shader"
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; GPU probes on both CPU routes (history) · Needs: Windows, Linux
Why
On a Mac, MoltenVK caps one device allocation at 41,747,087,360 bytes (M4 Max). The guest memory spans the game binds as
storage and texel buffers can be larger, so the macOS port imports such a span as several allocations (segments), and a
descriptor whose range crosses a cut cannot be one buffer. This PR is the shader half of binding it anyway: the SPIR-V back
end can declare such a descriptor as an array of pieces, one per segment, at its own binding, and every access finds its
piece. It also carries the fix for the one pattern of it MoltenVK refused: a segmented program with a compare-exchange
loop (an increment, a decrement, or a float add the GPU has no atomic for) failed pipeline creation with "no matching
function for call". Nothing is drawn with pieces until the draw side (#663) asks for a segmented variant, so on its own
this PR changes no frame on any platform. It is part of the macOS port. See #420.
What changed
The back end learns
RuntimeInfo::segmented: a list of descriptors to bind as pieces. With the list empty, which is everyprogram today, it emits byte for byte what it did. With it, the descriptor is an array of pieces at its binding, and each
load, store, atomic, texel fetch and size query goes through a small helper function that walks the pieces by their lengths
with constant indices. The renderer keeps such a variant apart from the plain program (never cached on disk, never picked for
a plain draw). The second commit writes the compare-exchange loops out inside those helpers, because SPIRV-Cross's MSL cannot
pass a pointer to one piece of a buffer array to a function.
gpu/vk/eden/shader_recompiler/runtime_info.h:SegEntry {Storage|Table|TexBuf|ImgBuf, desc, pieces}andRuntimeInfo::segmented.backend/spirv/spirv_emit_context.{h,cpp}:DefineSegmentedDescriptorschecks the list against the program(refused with
LogicErrorotherwise);DefineSsbos,DefineTextureBuffersandDefineImageBuffersdeclare a flaggeddescriptor as
TypeArray(…, pieces)at the same binding (a table'scount * pieces, entry k's piece j atk * pieces + j);SegmentedStorageCall,SegmentedTextureBufferCall,SegmentedImageBufferCallandSegmentedTextureBufferSizeask for one DontInline helper per (view, op, result type), made after main byDefineSegmentedHelpersand named in the calls byPatchSegmentedCalls;DefineGlobalMemoryFunctionssums a segmenteddescriptor's piece lengths for its bound end and branches to the piece (single, native candidate, table written out and in
the loop).
emit_spirv_memory.cpp,emit_spirv_atomic.cpp,emit_spirv_image.cpp,emit_spirv_image_atomic.cpp: thestorage loads, stores and atomics (CAS loops, float adds, the 64-bit and 32x2 fallbacks), texel fetches, image buffer
reads, writes and atomics dispatch to the helpers for a flagged descriptor;
emit_spirv.cppcalls the two new steps,which do nothing without helpers.
EmitContext::CasWordHere,CasLoopHereandCasStoreHerewriteCasLoop's loop (and
write_storage_cas_loop_func's) out in the piece, attempt for attempt, with the sameCAS_LOOP_MAX_TRIESceiling;CasLooprecords each loop's operation (cas_operations);SegmentedCasLoopandWriteStorageByCasLoopuse them, andDefineSegmentedHelpersmakes the loop's two variables first in the helper.gpu/vk/vk_shader.{h,cpp}:SegSig(sorted entries and a hash) onProgram,seg_sig(),seg_variant(),program_segmented()(finds or translates the variant of a plain program with plain's questions and answers, and failsit loudly if its descriptors are not plain's);
program_pick,mesh_program_locked,program_find_plain,loaded_variantsandportable_variantkeep the variants apart; every stage translation setsRuntimeInfo::segmentedfrom
Program::seg(empty in every plain variant).gpu/vk/vk_progcache.cpp: a segmented variant is never written to the program cache nor listed among theloaded programs.
tests/test_seg_shader.pyandtests/seg_shader_ir.cpp(new) run synthetic IR programs through the real backend, plain and in pieces, check the segmented modules' structure and capabilities, run every module through
spirv-val(Vulkan 1.3, uniform buffer standard layout, as CI validates the back end), check that no module passes a pointer into a
buffer descriptor array to a function, and with
--base-treecompare the plain modules byte for byte with an oldertree's. Seven source-reading checks follow the new field, the atomics' new branch and the new loop:
test_native_float_atomic_add,test_pipeline_state_portable,test_program_cache_cleanup,test_program_cache_one_variant,test_seed_kept_loaded,test_seed_variant_cleanup,test_cas_loop_ceiling.Measurements
Verdict: passes. The plain SPIR-V is byte for byte main's on both CPU routes, every module validates, and the array
pointer check fails without the fix (3 segmented modules, 54 calls) and passes with it. No speed to measure: nothing is
drawn with pieces until #663, and the plain path's SPIR-V is unchanged.
Hardware: macOS 26.7.1, Apple M4 Max, 64 GB, Apple clang 21 (game-free: no GPU, no game)
Scene: none (an emitter change: its proof is the fixture's modules)
Method:
tests/test_seg_shader.pyat this head with--base-treeagit archiveof main 6c9edffe's back end, on arm64and with
--arch x86_64(compiled for Intel, run under Rosetta); the same test file against the first commit's back end(
cbcdb34d, the pieces without the fix). Every output is a byte comparison or a hash, so one run per route and side; thetwo routes' 29 modules have equal SHA-256.
How to reproduce:
python -I -B tests/test_seg_shader.py --headers <SPIRV-Headers/include> --spirv-val spirv-val --base-tree <git archive of main> [--arch x86_64]Baseline: the middle column is this PR's first commit alone, the pieces before the fix; main has no segmented module to
check.
6c9edffecbcdb34d9d48901aspirv-valseg-storage18 calls,seg-storage-native-fadd15,seg-storage-narrow21): exit 101a0f43f…01a0f43f…, as SegProbe pinnedHistory, GPU (MoltenVK 1.4.2, M4 Max, arm64 and x86_64 under Rosetta), and why it holds for this head:
E.read_g1_layoutandE.write_g2_layoutPASS on both routes, 0[mvk-error](reports/2026-11-14-seg-probe.mdsection 3d). This head emits the same module (SHA-256
01a0f43f7da317c61ab1ee0dab18448d8dcafb798a1fcabdf5d08a0db8b69cc0).reports/2026-11-14-msl-arrayptr.mdsections 11 and 12): every pipeline wasmade, and the written-out loops (increment, decrement, float add, f32x2 add/min/max, the narrow profile's 8- and 16-bit
stores) gave the CPU interpreter's bytes in every piece;
ma-cas-seg,ma-cas-narrow-seg, their whole controls and thetable program
mc-tableMATCH on both routes. Those five modules were built from the NullShader branch's back end; thishead's back end emits the same five programs byte for byte (
reports/PubWave-files/PubShader/compare-mslprobe.txt), sothe result is this head's.
no matching function for call,&ssbo0_4[0u]given to adevice _66 *parameter (NullProbe revision 02; the cause in SPIRV-Cross's source is inreports/2026-11-14-msl-arrayptr.mdsection 1).Flags and settings
None.
Testing
Tested: on macOS 26.7.1, Apple M4 Max, at
9d48901a, this PR's head before the rebases ontoe090cc48andaa95d35f(main
6c9edffeplus this PR), game-free:tests/test_seg_shader.py --base-tree <main 6c9edffe>on arm64 and x86_64 (Rosetta): exit 0 both, as in Measurements;the 29 modules of the two routes are byte-identical, and byte-identical to the macOS port's integration tree after the
fix merged (
7cd720da, both routes: every plain and segmented module and arm E). Without the fix (the first commit's backend) the three segmented storage modules are the topic's original bytes (
b7928876ona4802cb5) and fail the arraypointer check.
build_cas_runwhole and in 3 pieces, both profiles, andbuild_table_run, copied from theNullShader branch's fixture into a scratch copy of this head's, bit off) through this head's back end: byte-identical to
MslProbe revision 02's frozen GPU inputs; all validate and pass the array pointer check.
vk_shader.h(vk_shader.cpp,vk_progcache.cpp,vk_draw.cpp,vk_predict.cpp,vk_replay.cpp,vk_programs.cpp) compiled for arm64 and x86_64 at this head and on main6c9edffewith main's rendererflags (
nx2/build.py'svflagsplus-arch, and main's non-Windows-fPIC -I host/posix -D_GNU_SOURCE). Every tree got#492's two Darwin fixes (
environ,RUSAGE_THREAD) and a compile-only prelude declaringposix_fallocateandposix_fadvise, which main's renderer calls and macOS lacks (#476 gives them Darwin paths): 12 of 12 objects on each,exit 0.
tools/ls-suite.pynamed run): 5 pass.test_program_cache_cleanupandtest_program_cache_one_variantcannot compile their fixture on this Mac, exactly as on main6c9edffe(main'svk_progcache.cppusesPOSIX_FADV_WILLNEED; main's own macOS suite fails both the same way). With #476's two Darwinhunks (
vk_progcache.cpp,pipeline_cache_file.h) applied to scratch copies of main and this head, both pass on both(main 9 tests OK and the same variant for 9 picks; this head the same).
reports/PubWave-files/PubShader/proof/p20-*.log,proof/fadv-*.log,compare-p20.txt,compare-mslprobe.txt,compile-*.out,named/,merge-tree.txt,stamps.txt.aa95d35f, at9b73e829(9d48901abefore 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;
python -I -B tests/run_all.pyon wave B's mergedb0e2e16, which carriesthis 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, and nothing here runs before #663 asks for a
segmented variant. The GPU at this head: the probes above are history whose modules this head reproduces byte for byte. The
f16x2 compare-exchange loops have no fixture program (the CPU interpreter has no 16-bit float vectors); they share the f32x2
loops' code path. Windows and Linux: not built or run there (11 changed renderer and back-end files differ from main's off
Apple,
unifdef -U__APPLE__); its test request covers the build, run_all on Linux and a start.Must be tested:
9b73e829) and mainaa95d35fthe same way(the 11 every-platform files: the back end's
emit_spirv.cpp,emit_spirv_atomic.cpp,emit_spirv_image.cpp,emit_spirv_image_atomic.cpp,emit_spirv_memory.cpp,spirv_emit_context.{h,cpp},runtime_info.h;vk_shader.{h,cpp},vk_progcache.cpp); on Linux runpython -I -B tests/run_all.py, expectingtests/test_seg_shader.pyto pass (it needsspirv-valand clang++; Windows skips it); start each build to thetitle, expecting this PR to translate the shader caches again once (the stamp changed) and nothing else to differ from
main, in the log or on screen (by hand: test request T-WLDY)
Risks
What could break: nothing runs differently until #663 calls
program_segmented: every program'sRuntimeInfo::segmentedis empty, and with it empty the back end's output is byte for byte main's (checked on 14 programs,both routes).
Programgrows by aSegSig(an empty vector and a hash). Once #663 uses it on macOS: a segmented variant istranslated on the drawing thread under
shader_muat its first draw, and one whose descriptors differ from plain's is failedand logged, never drawn with pieces.
Turning it off: revert the commits (there is no switch: nothing asks for pieces before #663).
Compatibility: every platform's shader caches are translated again once:
eden/andvk_shader.cppare in thetranslator's stamp (
nx2/translator_files.py:ebbf160ce9626041on main,40b414b30dad80e1here), so each player'sprogram cache, prepare record and pipeline cache are dropped at the first start after updating.
kProgramCacheFormatisunchanged here (#649 raises it). Nothing changes in saves,
longshot_options.inior the vk ABI (MKWVK_API_VERSION).Reviewer notes
cbcdb34d..9d48901a: this PR's two commits,cbcdb34d(the pieces,-xfrommacos/seg-shaderb7928876)and
9d48901a(the MslArrayPtr fix,-xfrommacos/msl-arrayptrbd396010). Readruntime_info.h, thenDefineSegmentedHelpersandSegmentedStorageCallinspirv_emit_context.cpp, thenprogram_segmentedinvk_shader.cpp; thenCasWordHere/CasLoopHere.mainhassegmented atomics that MoltenVK refuses.
bd396010was committed on the NullShader branch, where a storage descriptor bound whole can alsoreach the segmented helpers (
StorageThroughHelper). Here every access that reaches a helper is in pieces, so the loops arealways written out; the bound-whole branch, the guarded helper's
cas_loop, and the tests that need NullShader ortests/spirv_run.h(the loops' and a table's CPU runs,--changed-segmented, the*.cpuruncomparison) stay for #674.The cherry-pick's conflicts (
emit_spirv_memory.cpp,spirv_emit_context.{h,cpp},seg_shader_ir.cpp,test_seg_shader.py,spirv_run.habsent here) were all that difference.6c9edffe, no dependency. #649 stacks on it; the draw side (#662, #663, #663) and #674 use it.vk_shader.cpp(#252make_module'sIntel gate and
driver_faulted_programintranslate_into, whichprogram_segmentedreaches throughtranslate_into,so a program the driver faulted on is not asked again as a segmented variant either),
vk_progcache.cpp(#321's paths,#252) and
test_native_float_atomic_add.py(#191, #444) beside this PR's lines. #370 moved the stamp's file list tonx2/translator_files.pyand tookvk_api.hout of it: the first commit's message still says to land it with avk_api.hchange; the stamp changes here either way.6c9edffe:none (
git merge-tree).reports/2026-11-14-*.md(the port's design notes and the MslArrayPtr report), which are not in thisrepository.
Issues
See #420
Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), head9b73e829, without a conflict; this PR's owncommits, old → new:
cbcdb34d→1f9183c2,9d48901a→9b73e829; the commit names elsewhere in this descriptionare from before 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: 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, which #658's tree at the time broke and its head fixes;test_collect_widenow passes; the new tests pass,test_seg_shaderamong them, andtest_darwin_audio_service, Darwin-only, is skipped by the runner;reports/PubWave-files/waveA/. After the rebase onto mainaa95d35f(head9b73e829): 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. (It fails on this PR's first commit, thepieces without the fix;
mainhas no segmented module.)tests/that reads changed source updated in this PR, not weakened.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)WIP: feat(vk): bind storage and texel descriptors as piece arrays in the SPIR-V back endto feat(vk): bind storage and texel descriptors as piece arrays in the SPIR-V back end9d48901abf407a0cfc93407a0cfc939b73e82942Folded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)
Pull request closed