WIP: test: giant mesh fallbacks' shape through the real SPIR-V back end (tests/giant_shape_ir.cpp) #787
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!787
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/giant-shape"
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: test · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max, game-free on arm64 and x86_64 under Rosetta · Needs: Linux, any GPU (the test on Linux's compilers)
Why
On a Mac, four of the game's mesh programs drawn through the compute fallback are so large that Metal's compiler service dies
when two or three of them compile in one service process (#784 compiles such programs alone). To measure that
without game files, GPU probe GC-02 needed programs of the same shape: a 9 KB local memory array behind about 8,000 access
chains, branchy like real Maxwell code, 347,000-464,000 SPIR-V words, made by LS's own back end. A plain GLSL stand-in (GC-01)
had the counts but not the shape, and did not reproduce. This PR adds the generator GC-02 took its modules from, and a test
that keeps it building and exact: each module passes
spirv-val, and its CPU run gives exactly the out words of thegenerator's own model. It is part of the macOS port. See #420.
What changed
A test-only generator of synthetic programs in the giant mesh fallbacks' shape, through the real SPIR-V back end, and its
check. No game, renderer or translator file changes.
tests/giant_shape_ir.cpp(new): a Maxwell-like register machine as LS's frontend leaves it (registers live end to end,integer steps, optional ifs with phis at the merge), its local memory lowered as
load_store_local_shared.cppdoes (a Privatearray of
struct{uint}, guarded loads at computed offsets, read-insert-write 8/16-bit stores, 64-bit accesses word by word),emitted in the Mac profile (
zero_local_memory_by_loopand the other Mac flags where the profile has them). Each program isrun on a model of the window and its module on the CPU (
tests/spirv_run.h, undefined Private words a pattern, so the zeroloop must leave none); both give the same out words, written beside the module as what a GPU probe compares with.
--storage: the same program with the window in a storage buffer (diagnostic). It builds ontests/seg_shader_ir.cpp.tests/test_giant_shape.py(new): builds the generator with the back end and checks six shapes (straight-line, branchy,computed offsets in a 1,188-word window, constant offsets only, the storage form, bytes in the last word):
spirv-val(Vulkan 1.3) clean, every out word of the CPU run equal to the model's, an access chain into the local array. Skipped on
Windows (it builds the back end with
test_seg_shader.py's POSIX compiler flags and needsspirv-val).Measurements
Nothing to measure: a test-only generator; what GC-02 measured with its modules is in #784.
Flags and settings
None: tests only. The diff against main also shows the switches of the PRs it stacks on (#697's stack), theirs to list:
HOS_KEEP_FREED_PAGES,HOS_ZERO_ALL_PAGES,RX_NVN_WATCH_AFTER,RX_NVN_WATCH_ALL,RX_NVN_WATCH_TEXELS_SRC,RX_VK_DLSS_ACTORS,RX_VK_GM_LOOP,RX_VK_GPU_DUMP,RX_VK_LIBRARY,RX_VK_LOCAL_ZERO_LOOP,RX_VK_MESH_IND_COUNT_WORD,RX_VK_NO_UPSCALE_STUB,RX_VK_SET_FOR_LENGTHS,RX_VK_SET_FOR_TABLES,RX_VK_SPV_PROFILE,RX_VK_SPV_PROFILE_MIN,RX_VK_TEXEL_CODECS.Testing
Tested: on macOS 26.7.1, Apple M4 Max:
e090cc48, #697 and what it carries, this PR), under the shared check slot with a fresh HOME and TMPDIR:tests/test_giant_shape.py --headers <SPIRV-Headers vulkan-sdk-1.4.341.0> --spirv-val <the SDK 1.4.363.1's spirv-val> --jobs 3with
/usr/bin/clang++and withclang++ -arch x86_64(the generator under Rosetta): exit 0 on both,PASS: 6 giant-shaped programs: spirv-val clean, every out word of the CPU run equal to the model's, the same sixSHAPElines on both arches(
reports/PubWave-files/PubLate5/StartTrio/proof/gs-shape-*). On the base the test does not exist (tests/test_giant_shape.pyis new), so there is no base run to fail. The generator needs #697's
spirv_run.h(undefined Private words, push constants,writes past the out buffer) and the shader stack's profile and
seg_shader_ir.cpp(materialize_image_reads,bound_forwarded_depth,binding_named): on #784's base it does not compile.macos/giant-compile-231ff4adc, on the integration branch79e2e006):test_giant_shapepassed(
reports/GiantCompile2-files/checks/named-03); GC-02's twelve modules came from this generator, each spirv-val clean withits model and CPU run equal, and every made pipeline's out words matched on the GPU (59 of 59 dispatches;
reports/2026-11-15-giant-compile-2.md§6).Not tested: Linux (its compilers and
spirv-valthere); the test request covers it.Must be tested:
python -I -B tests/run_all.py test_giant_shape(or the test with--headers,--spirv-valand
--cxx g++), expectingPASS: 6 giant-shaped programs: spirv-val clean, every out word of the CPU run equal to the model's(by hand: test request T-XXXX)Risks
What could break: nothing in the game: tests only. The test takes the time of building the SPIR-V back end (as
test_seg_shaderdoes).Turning it off: revert the PR.
Compatibility: Nothing changes.
Reviewer notes
macos/giant-compile-231ff4adcpicked with-x, no conflict, line for line.needs #697 (local-mem-buffer:
spirv_run.h's undefined Private words and the Mac profile's zero loop) and through it theshader stack (#696, #694, #695, #676, #675, #674 and below), which #784's base does not carry. The branch is
#697's head
8f6597c0and the pick.git merge-tree): none (two new test files).Issues
See #420
Checklist
Everyone
type(scope): description) of the status line's type.main. (On maine090cc48through #6978f6597c0; main has moved since (35465d12): a delta sweep follows.)python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (At this head4fe409d0: 575 passed, 31 failed, 18 skipped against main e090cc48's 544/32/17 on this Mac; the 31 failures are main's own and nothing that passes on main fails;test_collect_widenow passes; 30 new tests pass,test_giant_shapeamong them;reports/PubWave-files/PubLate5/wave/full-G-4fe409d0/.)third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.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]>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)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]>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)plain7a498e5865plain1e1a3b3e0dDefineImages 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)Metal has SIMD-group functions and lane ids in kernel and fragment functions only (MSL 4.1 section 6.10), MoltenVK 1.4.2 lists fragment, compute and tessellation control in subgroupSupportedStages, and SPIRV-Cross 6c09849f refuses SubgroupLocalInvocationId in a vertex function ("Subgroup builtins are not available in this type of function."). Run #16 lost graphics pipelines to it in the precompile. On MoltenVK only, make_profile clears Profile::supported_subgroup_stages for the stages Metal runs as vertex functions (VertexA, VertexB, tessellation evaluation, geometry) that the device does not list, and the translator stamp gets a word for it then. The back end's path for such stages is each invocation as lane 0 of a 32-lane warp whose other lanes are not active: the lane masks of lane 0 (SR_GTMASK 0xfffffffe and SR_GEMASK 0xffffffff, not 0 and 1), SHFL.UP/DOWN/BFLY's bounds from lane 0 (not always out of bounds), and no subgroup barrier. Every other driver keeps every stage: its SPIR-V is byte for byte as before. tests/test_subgroup_vertex.py builds make_profile against stub devices and the back end with tests/subgroup_vertex_ir.cpp: 19 warp operations in vertex programs and 5 in tessellation evaluation ones, each emitted native and for MoltenVK, run on the CPU (tests/spirv_run.h now runs vertex inputs, outputs and a subgroup of one active lane) against the guest's lane 0 alone (PTX ISA's shfl, vote and lane masks), every SHFL b, clamp and segmentation mask; no subgroup construct may be left in a MoltenVK module of a vertex-side stage. reports/2026-11-14-subgroup-vertex.md. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit a878cef81d0f98c9517e79d51c29cbb7ae503ba6)SPIRV-Cross's MSL and Mesa's NIR inline every function, so each global access of the culling programs carried its fallback with every table entry written out: compute modules of 100k-560k SPIR-V words that Metal's compiler service compiled for minutes or failed in. Profile::search_tables_in_loops (set by make_profile on Apple only, for MoltenVK and KosmicKrisp with shaderStorageBufferArrayDynamicIndexing; RX_VK_GM_LOOP=0 off) loops each table in every fallback, the 8/16-bit stores' and float adds' too, which Intel's global_table_loop leaves written out. The program cache's stamp gets a "gmlp" word only when it is on; every other GPU's SPIR-V is unchanged. tests/test_gm_loop.py builds seg_shader_ir.cpp's table programs and big synthetic ones in the written-out, Intel, flag and both forms: spirv-val, loop counts, CPU runs equal, the flag alone changing no program without a table, and (--base) the off forms byte for byte against an older tree. spirv_run.h runs OpAtomicFAddEXT. Picked onto main 6c9edffe's line (mac/subgroup-vertex with mac/spv-size merged): one conflict, in docs/development.md's switch table, where main has rows this change's base had not (RX_VK_NO_INTEL_SERIAL, RX_VK_COMPUTE_QUICK_FIRST, the two HOS_VIDEO_ rows, main's RX_VK_DRIVER_FAULTS wording). Main's rows are kept and only the RX_VK_GM_LOOP row is this change's ("Intel's Mesa driver", as vk_coopmat.cpp's table_loop_wanted says on main too). Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit 94ea1c19bb6c6f7e542d4bc86a798ef8b8bda10e)A generator of synthetic programs in the shape of run 31's giant mesh fallbacks: a register machine as LS's frontend leaves Maxwell code (registers live end to end, integer steps, ifs with phis), its local memory lowered as load_store_local_shared.cpp does (a Private array of struct{uint}, guarded loads at computed offsets, read-insert-write 8/16-bit stores, 64-bit word by word, the zero loop on MoltenVK), emitted through the back end in the Mac profile. Each program is run on a model of the window and its module on the CPU (spirv_run.h); the out words must be equal. --storage gives the same program with the window in a storage buffer (a diagnostic form). These are probe GC-02's modules (reports/2026-11-15-giant-compile-2.md). tests/test_giant_shape.py builds it and checks six shapes: spirv-val clean, every out word of the CPU run equal to the model's. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit 31ff4adcec5f06dc2ccd7ff3f557afc78ed6fb07)4fe409d0e196b51b55b5Folded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)
Pull request closed