feat(vk): emulate null descriptors in the SPIR-V back end on MoltenVK #674
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!674
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/null-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: medium · Tested on: macOS 26.7.1, Apple M4 Max (MoltenVK 1.4.2), game-free at this head; GPU probes on arm64 and both CPU routes (history) · Needs: Windows, Linux, macOS game run
Why
A game may leave a descriptor slot empty. With Vulkan's
nullDescriptorfeature (robustness2) such a slot reads as zero,drops writes and atomics, and reports a size of 0, and the game's shaders rely on that. MoltenVK, the Vulkan driver LS uses
on a Mac, has no
nullDescriptor. So #663 binds a real stand-in in every empty slot (a 1-byte storage buffer, small texeland image views) and pushes a presence word per program. A stand-in is not empty: a shader would read and write its bytes
and see its size. Storage buffers bound as pieces (#663) need the same rule past their last piece. This PR makes the
translated shaders themselves behave as if every absent descriptor were null, on Apple without
nullDescriptoronly:storage accesses are guarded by the bound range, and every texel, image and texture access checks its presence bit. It also
carries the half of #648's MoltenVK compare-exchange fix (MslArrayPtr) that needs this emulation. It is part of the macOS
port. See #420.
What changed
With
Profile::emulate_null_descriptors(declared by #662; set bymake_profileon Apple when the device has nonullDescriptor) the SPIR-V back end keeps what a null descriptor gives. Without the bit, which is every other system andevery Apple GPU with
nullDescriptor, every module is byte for byte what it was. The renderer counts the presence words aprogram reads, the translator stamp gains a word under the bit, and the program cache's records keep the count (format 3).
gpu/vk/eden/shader_recompiler/backend/spirv/):emit_spirv_memory.cppLoadStorage: a direct load is guarded inline,index < length, the length from thedescriptor's U32 view (
EmitContext::StorageViewLength,OpArrayLengthshifted to the view's elements); out of rangeit reads element 0 and selects 0.
DontInlinehelper(
StorageThroughHelper→SegmentedStorageCall, one guarded piece,null_ssbo<b>_<op>): past the range nothinghappens and an atomic returns 0. The miss log's counter and pairs and the block probe are guarded the same way.
DefineSegmentedHelpers: every piece of a descriptor bound as pieces is measured by the U32 view, the last one too;image buffer helpers bound their last piece by
OpImageQuerySizeand take a presence parameter.spirv_emit_context.cppDefinePresence,PresenceBit): every texel buffer, image buffer, textureand image element gets a bit (
Bindings::presence, numbered inDefine*order whatever the profile). With the bit thestage's push block gets the member
u32[W]at offset 88 (PRESENCE_PUSH_OFFSET, S32 length constants so noArrayStridelands on a Function or Workgroup array, VUID-10684). Readers: size and LOD queries (0 when clear), texelfetches, image buffer and storage image reads (0, the coordinate clamped), writes and atomics (nothing, through
null_img*/null_imgbuf*helpers or the codec write's extra parameter) inemit_spirv_image.cppandemit_spirv_image_atomic.cpp; image buffer accesses are bounded by their size too.gpu/vk/vk_shader.cppmake_profilesets the bit (#if defined(__APPLE__),!dev.null_descriptor);translator_stampadds"nnul"only then;set_presence_wordsfillsProgram::presence_words(#662's field) after thelast stage of
translate_intoandtranslate_mesh_intoand fails the program loudly pastmaxPushConstantsSize.gpu/vk/vk_progcache.cpp:kProgramCacheFormat2 → 3, the record head keepspresence_words.macos/msl-arrayptrbd396010): #648 writes acompare-exchange loop out in each piece of a descriptor bound as pieces (
CasLoopHere,CasStoreHere), becauseSPIRV-Cross's MSL cannot pass an element of a buffer array to a function. Here
SegmentedCasLoop(
emit_spirv_atomic.cpp) andWriteStorageByCasLoop(emit_spirv_memory.cpp) take that path for pieces only; theguarded helper of a descriptor bound whole calls the loop function with its block, as on the topic branch
(
SegmentedStorageCall:.cas_loop = cas_loop && pieces != 0).docs/segmented-binding.md, a NullShader section, and a known gap it shares: image and image buffer arrays of countabove 1 are invalid SPIR-V on every platform already (
DefineImagesandDefineImageBuffersdeclare one image); left asit is, documented.
tests/spirv_run.h(the CPU interpreter) runs storage by the byte with bound ranges, atomics and push constants, and(MslArrayPtr) uniform buffers, 64-bit integer scalars and
PackHalf2x16/UnpackHalf2x16/NMin/NMax.tests/seg_shader_ir.cppemits every fixture program with the bit too (null-*,nullseg-*,nullcodec-*,nullcodecseg-*), checks the guards, the presence member and its readers, runs them on the CPU, writes the presencemap, and (MslArrayPtr) runs the compare-exchange loops and a storage buffer table in
msl-*modules.tests/test_seg_shader.py:--presence-programs,--presence-map,--sourceand--expect-fail(a negative runagainst another back end),
--changed-segmented, and a base of #662's generation compared module for module.test_program_cache_format(format 3),test_program_stamp_knobs("nnul"only under the bit),test_program_record_head(presence_wordsround trip), each with--source;test_amd_subgroup_profiles,test_native_float_atomic_addandtest_vendor_translationfollow the source they cut.Measurements
Verdict: passes; nothing to measure for speed in the game. With the bit off every module is byte for byte the base's on
both CPU routes; with it, every guard, presence read and loop runs as the CPU reference on the fixture. On the GPU (history,
arm64, NullProbe revision 02) the guarded modules were not slower than the unguarded ones at the probe's size (one run each).
Hardware: macOS 26.7.1, Apple M4 Max, 64 GB, Apple clang 21 (game-free: no game; GPU results are the probes' history)
Scene: none (the back end's fixture programs, emitted, validated and run on the CPU interpreter)
Method:
tests/test_seg_shader.py --base-treean archive of this PR's base, arm64 and--arch x86_64(Rosetta), and ofmacos/integrated7cd720da(where NullShader and MslArrayPtr were merged and checked), arm64; the same fixture with--sourcethe base, which must fail, both routes; the stamp, cache-format and record-head checks with--sourcethe base,which must fail. Byte comparisons and exact checks: one run per route.
How to reproduce:
python -I -B tests/test_seg_shader.py --headers <SPIRV-Headers/include> --spirv-val spirv-val --presence-programs <presence-programs.json> --base-tree <git archive of the base> [--arch x86_64]; the same with--source <the base> --expect-fail;python -I -B tests/test_program_cache_format.py [--source <the base>]Baseline: the "before" column is this PR's base, #663 (
b8579f1a), because the emulation extends their back end andcontract (main has no segmented or codec module to compare with).
b8579f1ad7f7c826null*) and MslArrayPtr's (msl-*)--source), arm64 and x86_64FAILlines, from "null-storage: 0 guards for the program's 43 direct storage accesses" to "wide: presence words 0"macos/integrated7cd720datest_program_cache_format,test_program_stamp_knobs,test_program_record_head--sourcethe base)47864043)Flags and settings
None in this PR's own commits: the emulation follows the device (Apple without
nullDescriptor), with no switch. The diffagainst 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),RX_VK_TEXEL_CODECS(#649),RX_VK_SET_FOR_TABLES(#643) andRX_VK_SET_FOR_LENGTHS(#644).Testing
Tested: on macOS 26.7.1, Apple M4 Max, at
d7f7c826, this PR's head before the rebases ontoe090cc48andaa95d35f(main
6c9edffe, #663's stack, this PR), game-free, every run under the shared check slot with a fresh HOME and TMPDIR:tests/test_seg_shader.py --base-treethe base, arm64 and x86_64: exit 0, as in Measurements; the presence map is bytefor byte #663's (
TexelDesc-files/presence-map.json) on both routes.macos/integrated7cd720da, arm64: exit 0, 125 of 125 modules and 10 of 10 CPU runs identical. Themodules are also byte for byte the integration's own run (
i21-check-01), MslProbe revision 02's ten frozen inputs (theloops and the table, bit off and on: its arm E, 12 of 12 MATCH on both CPU routes, history) and the six NullProbe
revision 02 inputs the fixture makes (NS1's storage, NS4's texel and codec programs).
are exactly the bit-on modules whose guarded helper of a descriptor bound whole runs a compare-exchange loop (it now calls
the loop function), with the CPU runs word for word the same.
--sourcethe base, both routes, andtest_program_cache_format,test_program_stamp_knobs,test_program_record_headwith--sourcethe base: each fails as required (above).test_null_descriptorsandtest_null_layout_push(#663's),--base-tree#663's own base, arm64 and x86_64: exit 0;test_native_float_atomic_add, arm64 and x86_64: exit 0;test_program_cache_format,test_program_stamp_knobs(19 switches in the stamp),
test_program_record_head: exit 0.run_allby name): 67 pass;test_program_cache_cleanupandtest_program_cache_one_variantfail as on main6c9edffe(main's own macOS failure,POSIX_FADV_WILLNEED); with #476'stwo Darwin hunks on a scratch copy of this head both pass on both routes.
.cppfiles (emit_spirv.cpp,emit_spirv_atomic.cpp,emit_spirv_image.cpp,emit_spirv_image_atomic.cpp,emit_spirv_memory.cpp,spirv_emit_context.cpp,vk_shader.cpp,vk_progcache.cpp)compiled whole with main 6c9edffe's renderer flags for arm64 and x86_64: 16 of 16, exit 0 (a scratch tree with #476,
#492 and #490 merged, which main's renderer needs on macOS).
NS5 as the CPU predicted), the topic's seven mutants each caught, its full suite with no new failure by name (one
load-timed
test_gpu_hang_watchfailure, passing on rerun; its inputs unchanged).reports/PubWave-files/PubEmit/(proof/p33-*.log,compare-*.txt,equal-integrated.txt,related-p33-*,compile/p33-*,stamps.txt,offapple-p33-*.txt).aa95d35f, atb7683077(d7f7c826before 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).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: the game at this head, and no GPU run at this head (the probes are history; their modules are byte for byte
this head's where the fixture makes them). NullProbe never ran on x86_64; the mutants were not rerun at this head. Windows
and Linux: not built on Windows or Linux (9 changed back-end and renderer files differ from the base's off Apple too,
unifdef -U__APPLE__); its test request covers it.Must be tested:
b7683077, it carries #663 with the PRs it stacks 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 files on every platform (the back end's
emit_spirv*.cppandspirv_emit_context.{h,cpp},gpu/vk/vk_shader.cppandvk_progcache.cppdiffer from the base's with every__APPLE__branch resolved off), theshaders prepared once more and the program cache rebuilt once (a new translator stamp and cache format 3), and every
other line to match main's (by hand: test request T-XZBG)
program prepared again once (
nnulin the stamp), no program failed pastmaxPushConstantsSize, 0[mvk-error], andtextures and effects as on the base (by hand: the owner, the log's
vk:lines and screenshots)Risks
What could break: on macOS without
nullDescriptor, every translated program: a wrong presence word hides a resource(reads 0, writes dropped), a wrong length guard drops storage accesses; programs grow by the guards and helpers. A program
needing more presence words than the push constants hold fails translation loudly. On other systems the modules are byte
for byte the same; only the stamp and the cache format change.
Turning it off: revert the PR (no switch: the bit follows the device).
Compatibility: every platform's shaders are prepared again once (the back end is in the translator's stamp:
dc271e7fc99bb36c→ddcea9021702b56a), and every program cache is rebuilt once (kProgramCacheFormat2 → 3; its recordhead gains
presence_words). On a Mac the translator stamp also gains"nnul". Nothing changes in saves,longshot_options.inior the vk ABI (MKWVK_API_VERSION).Reviewer notes
b8579f1a..d7f7c826, this PR's four commits,-xfrommacos/null-shaderandmacos/msl-arrayptr: theNullShader commit (its first, a
wip:commit on the topic branch, reworded; content the topic's), its docs and its fixto two source-cutting checks, then "MslArrayPtr's NullShader half", the rest of
bd396010that #648's port left forthis PR. Read
docs/segmented-binding.md's NullShader section first, thenSegmentedStorageCallandDefineSegmentedHelpers,DefinePresenceandPresenceBitinspirv_emit_context.cpp, thenLoadStorage.b8579f1a, which carries #662 (the contract) and #644(programs that read a storage buffer's length get descriptor sets: without it every guarded load would read length 0),
with #643, #658, #648, #649 and #645. #663 pushes the presence words and binds the stand-ins, #663 sizes the push range
(88 + 4 per word); without them the modules would read presence words nobody pushes. Until they merge this PR's diff
shows their commits too. #675 builds on it.
backend/spirv/) and the three test files it touches(
seg_shader_ir.cpp,spirv_run.h,test_seg_shader.py) are byte for bytemacos/integrated7cd720da's, whereNullShader and
bd396010were merged and checked. Until the last commit the guarded helper of a descriptor bound wholewrites its loop out like the pieces (valid, and no pointer into a buffer array reaches a function); the last commit
makes it call the loop function with its block, as
bd396010does.6c9edffe: none of this PR's own (tests/test_program_cache_format.pyfollows main's #370,translator_files, beside the new--source).git merge-treeat this head against the open macOS PRs and thiswave's branches). The NullShader pick met #648's MslArrayPtr port in
spirv_emit_context.{h,cpp}andemit_spirv_memory.cpp(both kept; the commit message says how). Conflicts inherited from the base: #454, #490, #646,and #650/#652 and the branches built on them.
translation does not change without the bit (off Apple it is never set). 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), headb7683077, on #6635400f1a0, without aconflict; this PR's own commits, old → new:
c65b75a3→71003696,88efd9fd→17667bea,f7d87931→f53eda6c,d7f7c826→b7683077; 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(headb7683077): 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)The segmented single storage array's case (plan 4.3 item 5) had no negative: the check always cut its own tree. With --source <B2 archive> it fails as required ("moltenvk.segmented_single want=set got=push"). Conflict with #P28 (read_write textures; not with main): the docstring keeps #P28's paragraph with this usage line, and main() cuts from --source's vk_draw.cpp with #P28's read_write count (as in the macOS integration, Integration 17). Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit 5824f6828e21cf810dcceaa6be425a9224b15994)plaine37c097da2plaina997b3629aDefineImages and DefineImageBuffers declare one image whatever the descriptor's count, while Image() indexes the variable when the count is above 1, so such a module fails spirv-val ("OpAccessChain reached non-composite type"); B2 has it already, on every platform. The null_img* and null_imgbuf* helpers take their element the same way and share it. Found while extending the fixture; declaring the arrays changes every platform's SPIR-V and is left to the owner. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit 83e7e18b4fee477f1e6901871ad37efe8f23d5bf)d7f7c8260215c0f42cb315c0f42cb3b7683077e6WIP: feat(vk): emulate null descriptors in the SPIR-V back end on MoltenVKto feat(vk): emulate null descriptors in the SPIR-V back end on MoltenVKFolded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)
Pull request closed