WIP: fix(vk): bound the control-flow nesting of SPIRV-Cross's Metal source on MoltenVK #693
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!693
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/compile-gaps"
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: fix · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (MoltenVK 1.4.2), game-free at this head; GPU probe CG-01 passed on both routes (arm64 and x86_64) · Needs: Windows, Linux, macOS game run
Why
On a Mac, two of the game's compute programs are never made: Metal's compiler refuses source with more than 256 braces open
at once, and the Mac starts lost 4 variants of them that way (
program_source:2591:1025: … bracket nesting level exceeded maximum of 256), so whatever they compute is missing. Column 1025 is a{after 1024 spaces: SPIRV-Cross, MoltenVK'stranslator to Metal, indents each scope 4 spaces and prints every selection, switch and loop construct (and the rest of a
block after a conditional break or continue) as a scope of its own, so this is control flow 256 constructs deep, not an
expression. #676's expression bound (and #692's) counts brackets of expressions and cannot reach it; Metal counts
braces apart from brackets. This PR makes LS's translation bound how deep a function's control flow nests, on MoltenVK only.
It is part of the macOS port. See #420.
How to reproduce
Seen on: macOS 26.7.1, Apple M4 Max, MoltenVK 1.4.2 (SPIRV-Cross
6c09849f), arm64 and x86_64 (Rosetta) startsSettings and switches: defaults
Steps: start the game on a Mac and let the start-up precompile run (the failing programs were not read); or, game-free,
python -I -B tests/test_msl_scope.py --headers <SPIRV-Headers/include> --spirv-val spirv-val --spirv-cross <spirv-cross>,which prints SPIRV-Cross's own MSL brace count for 300 nested Ifs without and with the bound
Expected: every pipeline is made
Actual: on the base
vkCreateComputePipelinesfails for compute programs6c022b3f97757822andd8b2729919914acf(2 variants each, each tried once more with a new pipeline cache),
[mvk-error]withprogram_source:2591:1025in every one,on arm64 and x86_64. Game-free: SPIRV-Cross puts the 257th
{of the 300-If module at column 1025 (1037:1025), and Appleclang with
-fbracket-depth=256refuses that layout at exactly that line and column (255 nested scopes taken, 256 refused)What changed
On MoltenVK only,
Profile::bound_scope_nestingruns a pass last inEmitSPIRV: in a function whose control flow nestsdeeper than 128 levels, the outermost construct around the deepest block that is at least 64 deep is moved into a function
of its own, repeatedly, until the function is within 128. SPIRV-Cross prints each function with its own scopes, and Metal
inlines it again (every SPIR-V function is
always_inlinein SPIRV-Cross's MSL). The move is exact: the same instructions inthe same order, the construct's values passed in, the values it makes for the rest of the function stored through Function
variables where they are made and loaded after the call, the phis of the blocks it leaves to made where it leaves, and a
returned code saying which way it left (its merge, a break, a continue or a return). A module within the bound comes back
byte for byte; without the flag nothing runs.
gpu/vk/eden/shader_recompiler/backend/spirv/msl_scope.{h,cpp}(new):BoundScopeNesting(code, levels = 128),ScopeNesting(code)(the model, for the tests) andthread_local ScopeBound scope_bound(deepest before and after,constructs moved, bounded or not). A block's levels: the headers that structurally dominate it and whose merge does not, a
selection 1, a switch 2, a loop 2, a conditional branch without a merge 1 (its
else); measured as an upper bound ofSPIRV-Cross's braces in every test module. A construct with an instruction whose operand layout is unknown here, a way in
other than its header, an
OpVariableinside, or a pointer or image value used after it is not moved (a deeper one is tried;a program left deeper is logged).
emit_spirv.cpp:EmitSPIRVruns it after #676'sBoundForwardedDepth, last;profile.h:Profile::bound_scope_nesting.gpu/vk/vk_shader.cpp:make_profilesets it for MoltenVK's driver (VK_DRIVER_ID_MOLTENVK);translator_stampadds"mslb"only then, after"msl2";translate_intologsvk: program <key>: control flow N levels deep, K constructs moved into functions of their own, M levels after (msl_scope.h; Metal takes 256 braces), or: not bounded, …, for each program the bound changed or could not.gpu/vk/VULKAN.md, the bound's paragraph after the expression bound's.tests/test_msl_scope.py(new) withtests/msl_scope_ir.cpp(new) writes 5 programs as SPIR-V assembly in the back end'sshapes (300 nested Ifs with a phi at every merge and header values read after it; 240 levels of mixed Ifs, loops with
breaks and continues, switches and returns; 150 nested switches; 140 early returns; 100 nested Ifs, within the bound),
bounds them, validates both modules, runs both on the CPU for 64 lanes (each going a different number of levels deep)
against the same program computed in Python, counts the levels independently in Python, and with
--spirv-crosscountsSPIRV-Cross's real braces and the 257th's position.
--probewrites CG-01's inputs.tests/spirv_run.h:OpSwitchon a 32-bit selector (the nested switches).tests/test_amd_subgroup_profiles.py: the bound on for MoltenVK only and carried into the pinned and graphics profiles.Measurements
Verdict: fixes it on the CPU (game-free, both routes): the three programs past 256 levels fall to 108, 93 and 108 levels
and SPIRV-Cross's braces from 301/322/301 to 109/86/109, every module's 64 lanes the program's words before and after, the
shallow one byte for byte. On the GPU: fixes it (CG-01 r3, MoltenVK 1.4.2, arm64 and x86_64: the three unbounded modules
refused at their 257th brace, column 1025, by MoltenVK's own SPIRV-Cross and Metal; all six others made with exact words; see
Testing). Speed:
not measured on the GPU (words +1.4 % and +3.5 %, CPU steps +1.5 % on the synthetic programs; Metal inlines the functions).
Hardware: macOS 26.7.1, Apple M4 Max, 64 GB, Apple clang 21 (game-free)
Scene: none (synthetic modules in the back end's shapes, emitted, validated, run on the CPU interpreter and converted by
SPIRV-Cross)
Method:
tests/test_msl_scope.pyat this head, arm64 and--arch x86_64(Rosetta), with the Vulkan SDK'sspirv-cross(1.4.363.0); on the base with this PR's test files, which must fail. History at the topic's commits: the same test with
MoltenVK main's own SPIRV-Cross (
aa217aeb, built for the test), identical numbers. Exact checks: one run per route.How to reproduce:
python -I -B tests/test_msl_scope.py --headers <SPIRV-Headers/include> --spirv-val spirv-val --spirv-cross <spirv-cross> [--arch x86_64]Baseline: the "before" column is this PR's base, #692 (
6d50fd71), whose expression bound and stamp word this PRfollows (main has neither).
6d50fd71de8f2520test_msl_scopemsl_scope.htest_seg_shader --base-treethe base), arm64 and x86_64Flags and settings
None in this PR's own commits: the bound follows the driver (MoltenVK), with no switch. 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),RX_VK_TEXEL_CODECS(#649),RX_VK_SET_FOR_TABLES(#643)and
RX_VK_SET_FOR_LENGTHS(#644).Testing
Tested: on macOS 26.7.1, Apple M4 Max, at this head (main
6c9edffe, #692's stack, this PR), game-free, everyrun under the shared check slot with a fresh HOME and TMPDIR:
tests/test_msl_scope.pywith the Vulkan SDK'sspirv-cross, arm64 and x86_64: exit 0, as in Measurements. On the base withthis PR's test files: fails (no
msl_scope.h).tests/test_msl_scope.py --probeat this head writes CG-01's 16 frozen inputs (the 9 modules, their expected words andscope-probe.json) byte for byte, so CG-01's result (below) holds for this head's modules.tests/test_seg_shader.py --base-treethe base, arm64 and x86_64: exit 0, 139 of 139 identical.tests/test_msl_depth.py(#676's bound with this pass after it): exit 0.tests/test_amd_subgroup_profiles.py: exit 0; onthe base with this PR's test: fails (no
bound_scope_nesting).emit_spirv.cpp, the newmsl_scope.cppandvk_shader.cppcompiled whole with main 6c9edffe's renderer flagsfor arm64 and x86_64: 6 of 6, exit 0 (scratch tree with #476, #492 and #490).
38d42adf): link check 0 of 621 declared names without an externaldefinition, 66 of 66 objects, 1,218 name/arch checks, closure 566 symbols a route, 0 undefined; the suite in the run_all box.
aa217aeb), identicalnumbers;
msl_scope.cppwith-Wall -Wextra -Werroron both arches; the topic's full suite with only its base's ownfailure.
msl_scope.{h,cpp},msl_scope_ir.cppandtest_msl_scope.pyhere are byte for byte the topic's, and every otherchanged line is the topic's.
reports/PubWave-files/PubNext/(proof-cg-02/,proof-cg-01/base-subgroup-profiles.*,compile/compile-gaps/,offapple-compile-gaps.txt,wave3/).predicted:
ROUTE arch=arm64 parts=5/5 safe=1 gates=9 deviations=3 data=12/0 made=6/9 predictions=9/9 mvk_errors=6 msl_files=9 footprint_stops=0, and the same line for x86_64 (Rosetta).The three unbounded modules (scope-if, scope-mixed, scope-switch) were refused by MoltenVK's own SPIRV-Cross (
6c09849f)and Metal with
bracket nesting level exceeded maximum of 256atprogram_source:1043:1025,1649:1025and787:1025.Each is the 257th
{of its dumped MSL. The positions are the same on both routes.All six others were made, with their 64 words equal to the program's and both guards intact:
Each big module ran in its own sandboxed process, under a 2048 MiB footprint cap. Peak footprint per case, arm64 / x86_64:
The refused modules stayed at most 41 MB. Each footprint is the in-process Metal compile of one pipeline, kept after it
was made. The unbounded forms cannot be compiled on Metal, so the bound's own share of these figures is not separable.
Receipts:
reports/CompileGaps-files/cg01/runs-cg01-r3-arm64/run.json1db50442d4de25cbd35a0e2bb74738e8e71fe7632ebf142610e32c962feea74bandruns-cg01-r3-x86_64/run.json88bce442085d91080b7dc2d4ee3fe8fe490dc7148cd2af5e73c79a435df4ba0c(review359e7c29…, build930dbb33…, predictions79e20c08…).An earlier revision, r2, put all six in one process: it stopped at its 512 MiB guard after mixed-on, a probe limit and not
a refusal.
Not tested: the game at this head (the failing programs were not read). Windows and Linux: not built there (3 changed
files differ from the base's off Apple, and the new
msl_scope.cppis compiled there); its test request covers it.Must be tested:
mslbinthe stamp), a
vk: program 6c022b3f97757822: control flow N levels deep, K constructs moved …line for each program thebound changed, no
not boundedline, and nobracket nesting level exceededline (by hand: the owner, the log'svk:and[mvk-error]lines)de8f2520, it carries #692 with the PRs it stacks on) and main6c9edffethe same way and start each to the title, and runpython -I -B tests/run_all.pyon Linux (where the back end'schecks run), expecting the build to compile the new
backend/spirv/msl_scope.cppand this PR's changed files on everyplatform, the shaders prepared once more (a new translator stamp), and every other line to match main's (by hand: test
request T-XXXX)
Risks
What could break: on MoltenVK only, every translated module passes through the new pass: a module within the bound comes
back as it was; a deeper one is rewritten (a fault in the move would throw at translation, so the program fails loudly, or
change a value: the CPU runs of every test module match the program's words before and after). A program the move cannot
bound is logged and left as it was (Metal then refuses it, as today). Other drivers: nothing runs; only the stamp changes.
Turning it off: revert the PR (no switch: the bound follows the driver).
Compatibility: every platform's shaders are prepared again once (the back end is in the translator's stamp:
991689a2f695b7f8→869be64b04cbc357); on MoltenVK the translator stamp also gains"mslb".kProgramCacheFormatstays 3.Nothing changes in saves,
longshot_options.inior the vk ABI (MKWVK_API_VERSION).Reviewer notes
6d50fd71..de8f2520, two commits,-xfrommacos/compile-gaps: the bound and its test, then the module parsed inplace when nothing is moved (outputs byte-identical). Read
msl_scope.cpp's model (Analyze), then the move (Outline),then
test_msl_scope.py's independent level count.6d50fd71), and through it #676 and the PRs it carries: the pass runsafter
BoundForwardedDepth, the stamp word follows"msl2", and the docs paragraph follows #692's text.profile.h,vk_shader.cpp(make_profile,translator_stamp,translate_into) andtest_amd_subgroup_profiles.py, where the integrated topic sat beside SubgroupVertex's, GmLoop's, LocalMemBuffer's andSpvSize's lines and beside main's
ModuleProgram: only this topic's own lines were kept. Its changed lines are theintegrated topic's line for line (
macos/integrated5e24611a).vk_shader.cpp,test_amd_subgroup_profiles.py), #696 and#697 (
profile.h,vk_shader.cpp,test_amd_subgroup_profiles.py), #698(
test_amd_subgroup_profiles.py): each adds its field, itsmake_profileline, its stamp block and its test lines in thesame places; whichever lands second keeps both, in the integrated order (stamp
"msl2","mslb","sgv1","gmlp","lzlp"). The conflicts with #650/#652 (VULKAN.md,vk_main.cppand others) are the base's, as in #676's notes.msl_scope.cppis compiled everywhere and the stamp changes, so every platform prepares its shaders once;the pass runs only on MoltenVK. 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(maine090cc48).python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (On the wave's merge38d42adf(this PR + the Rosetta import PR, which sits on #658 below this PR's stack): 559 passed, 31 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the 31 failures are main's own and nothing that passes on main fails;test_collect_widenow passes; 27 new tests pass,test_msl_scopeamong them;test_darwin_audio_service, Darwin-only, is skipped by the runner;reports/PubWave-files/PubNext/wave3/full-38d42adf/.)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 base, #692, withthis PR's test files; on
maintoo, which has no bound.)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)SPIRV-Cross (MoltenVK 1.4.2's pin 6c09849f) prints an OpImageRead's result, and a pure read helper's call, where the value is first used, and nothing orders it against the same invocation's later writes and atomics on that image: in MSL the read ran after them and returned the later value. TexOrder's TO-01 probe proved it on the GPU for LS's plain output, with and without the NullShader bit, on both CPU routes (reports/2026-11-14-tex-order.md). - Profile::materialize_image_reads, set by make_profile on Apple for MoltenVK only, with its translator stamp word ("rord") only when on. - emit_spirv_image.cpp InPlace: with the flag, every storage image and image buffer read's result (the plain read, the segmented read helper's call, a carrier codec's element reads) is stored to a Private variable at the read and loaded back; SPIRV-Cross prints a store where it stands. Private, since a Function variable must lead its function's first block; in the entry point's interface from SPIR-V 1.4. Off: no new ID, every module byte for byte as before. - tests/seg_shader_ir.cpp: order-* modules with the flag; the read-in-place rule (every read and read helper call stored at once), their flag-off twins breaking it, CPU runs equal to the modules without the flag, and the flag alone changing nothing without image reads. test_seg_shader.py: order- modules required; a base tree without the flag builds them with TEX_ORDER_BASE and they are not compared; --source against it fails. - tests/spirv_run.h runs Private variables (fresh per invocation) and OpMemoryBarrier (a no-op one invocation at a time). - docs/segmented-binding.md: a TexOrder section. Conflicts on mac/null-shader (#P33), with MslArrayPtr's tests that #P33's last commit carries, not with main; resolved as macos/integrated did (Integration 22, b08694d8): tests/seg_shader_ir.cpp keeps both sections and both calls (msl_arrayptr, then tex_read_order); tests/spirv_run.h keeps both storage classes (Uniform, then Private); tests/test_seg_shader.py keeps both docstring paragraphs and presence checks, leaves 'msl-' and 'order-' out of an older generation's set, and compares with a base in one loop (an order- module skipped for a base without the flag, identical counts as the same, a different segmented one listed under --changed-segmented, anything else fails). Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit a9aad5bcd6f4cce3d9fcff8b231da9628e7da4b3)de8f25209c7588bcbb977588bcbb975ceed495b2Folded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)
Pull request closed