fix(vk): draw mesh count draws on the compute fallback, grow its records area on demand, and count what it cuts #707
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!707
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:fix/mesh-count"
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: medium · Tested on: macOS 26.7.1, Apple M4 Max (MoltenVK 1.4.2), arm64 and x86_64 under Rosetta (game-free tests and one GPU probe) · Needs: Windows and Linux, a GPU on the compute fallback (GTX 10xx, RX 5000, AMD under Linux by default, Navi 21/22 on Windows) and one with mesh shaders (build, start and play); macOS (a start, on the macOS stack)
Why
On every GPU that draws task and mesh programs through LS's compute fallback (issue 640: MoltenVK, KosmicKrisp, AMD under Linux by
default, Navi 21/22 task + mesh programs on Windows, GTX 10xx, RX 5000),
fallback_recordleaves everyMultiDrawMeshTasksIndirectCountout: no dispatch takes a count, and each record needs its own draw index. The fallback's recordsarea is a fixed 64 MB (a draw needing more is cut silently, and over 128 MB refused), and nothing counts what the fallback cuts. This
PR counts mesh draws and the fallback's cuts (log-only), draws mesh count draws on the fallback, and grows the records area on demand
within the renderer's memory allowance. It changes every platform, so it is its own PR; it carries #663 and #679 (it needs
SegBind's arena and the count-draw rewrite) and can move to main once those land. It comes from the macOS port's work. See #420.
How to reproduce
Seen on: every platform with the compute fallback (read from the source); measured on macOS 26.7.1, Apple M4 Max, MoltenVK 1.4.2
Settings and switches: defaults
Steps: probe MC-01 (standalone MoltenVK process; command in
reports/2026-11-14-mesh-count.md§6): mesh count draws, task + meshand mesh-only, strides 8/16, counts 0, 1, 7, 16, 20 and 0xFFFFFFFF at maxDrawCount 16
Expected: each record draws what it draws as a single fallback draw
Actual: on main the fallback records nothing for them (
fallback_record: left out)What changed
On every GPU that uses the compute fallback, mesh count draws are drawn now (each record's dispatches in turn, then one indirect draw),
and the fallback's records area grows past 64 MB when a draw needs more and the memory allowance holds it. Log-only counters say how
many mesh draws of each kind the game makes and what the fallback still cuts.
host/nvn_native.ccounts the game's mesh draws by kind (its first three and every 20,000thof each, the first count draw named at once); the renderer's
MeshTallyanddraw_mesh_reportwith the frame statistics (where thefallback is on or a mesh draw was made): kinds, native or fallback, batches, maxDrawCount, left out by reason; a GPU census in the
records header (16 words now; a row per program in host-cached memory) of what the task and mesh epilogues cut (past
task_cap,past the slots, past the area), printed as
vk: mesh fallback cuts so farand per program.count_draws.h/count_draws_record.glslgeneralized (Layout: the count draws' records bytefor byte as before;
count_draws::mesh()writes{count, 1, 1}or{0, 0, 0}past the count);count_rewrite_recordsholds therewrite;
fallback_record_countbinds the records and count asdraw_demandsadmits them, rewrites them into dispatch records,then per record runs its task (or mesh-only) and mesh dispatches with its own draw index and first, the allocator kept so each draw
command covers only its records; draw() draws maxDrawCount commands with one
vkCmdDrawIndirectin one pass. A fallback task ormesh stage that loads DrawID reads it pushed at offset 76 (a GLCompute stage has no
DrawIndex); stages that do not are emittedbyte for byte as before.
fallback_buffers_ofsizes the area from the census's most(
gpu/vk/mesh_area.h: a power of two from 64 MB up), and grows it only whenmesh_area::affordableholds againstvram_usage;otherwise it keeps its largest area, logs
vk: fallback: the mesh records area stays …, and the census counts the cut. The fixed128 MB refusal is gone.
README.md(the fallback row),gpu/vk/VULKAN.md. Tests:test_mesh_census,test_mesh_count_fallback+mesh_count_fallback_test.cpp,test_mesh_drawid+mesh_drawid_ir.cpp,test_mesh_area+mesh_area_test.cpp(new);count_draws_test.cppboth layouts; six fixtures get stand-ins.Measurements
Verdict: fixed where measured: in MC-01 every mesh count draw drew exactly what each of its records draws as a single fallback
draw (29 of 29 steps byte for byte). The cost follows maxDrawCount, not the GPU's count: at maxDrawCount 4,096 with count 1, 388 ms
against 0.9 ms for the one record alone (per record two dispatches, three transfers, four full barriers). #25/#26 made no mesh count
draw; the new line counts them and their maxDrawCount.
Baseline: main leaves these draws out; MC-01 compares this path (
A) with each record drawn as its own single fallback draw (ref)in one standalone MoltenVK 1.4.2 process.
Hardware: macOS 26.7.1, Apple M4 Max (40-core GPU), MoltenVK 1.4.2, 16-core CPU, 64 GB
Scene: MC-01: 29 steps: task + mesh and mesh-only, strides 8/16, counts 0-0xFFFFFFFF at maxDrawCount 16, two split steps, cost at 64
and 4,096
Method: MC-01
reports/MeshCountDraws-files/mc01/runs-mc01-arm64/result.json(cb763c3a…),probe.log(01002e00…), released onceon arm64
How to reproduce: MC-01's
runcommand (reports/2026-11-14-mesh-count.md§6)6c9edffe(left out; ref each record alone)tm_s8_c1637 cells,mo_s8_c1623)Flags and settings
None: no switch. Every platform's compute fallback.
Testing
Tested: on macOS 26.7.1, Apple M4 Max, at
0a61a9e7, this PR's head before the rebases ontoe090cc48andaa95d35f(main
6c9edffeplus #679 and what it carries):test_mesh_count_fallback(2,016 count draws, 4,466,903 checks),test_mesh_area,test_count_drawswith/usr/bin/clang++andclang++ -arch x86_64(Rosetta),test_mesh_census,test_mesh_fallback,test_seg_xfer(--arch arm64,--arch x86_64): pass(
reports/PubWave-files/PubLate4/proof-02/mc2-*);test_mesh_drawid(the real back end, spirv-val; both compilers) passed at7f2423db, one commit before this head, whose only change is a line invk_draw.cpp(proof-01/mc-drawid-*). With the head'stests on #679's head,
test_mesh_census,test_mesh_count_fallback,test_mesh_area,test_count_drawsandtest_mesh_drawideach fail (proof-01/base-mc-*).gpu/vk/vk_draw.cpp,vk_replay.cppand the four changed SPIR-V back-end files compiled whole for arm64 and x86_64 with main'srenderer flags (#476's, #492's and #490's macOS compile pieces merged in a scratch tree only): 12 objects, exit 0
(
reports/PubWave-files/PubLate4/compile/fix-mesh-count/);host/nvn_native.ccompiled whole for both arches with the macOS hostflags on a scratch tree of this head + #490 + #479 (
compile/host-fix-mesh-count.out).reports/PubWave-files/PubLate4/offapple-fix-mesh-count.txt).aa95d35f, atb41a03a9(0a61a9e7before 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 C6's mergea10d0c4b, which carriesthis head: 584/33/18 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, one new failure,
test_gpu_hang_watch, which passes run alone on the same merge on arm64 and x86_64 (its 2 s bound under the suite'sload) (
reports/2026-11-15-rebase-sweep-3.md).78ec1c47, which carries everyMac PR 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).vk: mesh draws (at present 49560): 78.9 a frame (31.9 direct, 47.0 indirect, 0.0 with a count), 0.0 drawn native and 78.9 through the compute fallback; … so far 1392601 direct, 2002960 indirect and 0 with a count, 0 native count draws past the 4096 records converted …; line 65689:vk: mesh fallback cuts so far (the census, 51 programs): 0 mesh workgroups past task_cap …, 0 meshlets refused past the slots …, 0 mesh-only workgroups past the slots, 0 records past the area …. The census is all zeros, but the scene has 0 mesh count draws, so thecount path was not exercised in the game; the game-free test above covers it.
Not tested: on a GPU other than the Mac's; a game run with it (#25/#26 made no mesh count draw); not built on Windows or Linux; its
test request covers it.
Must be tested:
b41a03a9, itcarries #679, #677, #663 and what they carry) and main
aa95d35fthe same way, start and play to the field, expectingthe
nvn: DrawMeshTasks…counters,vk: mesh draws (…)with none left out,vk: mesh fallback cuts so farall zero,no validation error, and the scenes as on main (by hand: test request T-BFLC).
Risks
What could break: every platform's fallback: mesh count draws that were left out are drawn (their cost per maxDrawCount record,
above); the records header is 16 words and reset by one 64-byte update; the area can grow past 64 MB where the allowance holds it (on a
Mac the allowance is the driver's budget until #642's unified budget lands, then that). Two silent cuts stay counted, not fixed: task
counts past 65,535/threads and
task_cap(reports/2026-11-14-mesh-count.md§4).Turning it off: revert the PR (no switch).
Compatibility: Every platform's fallback draws its mesh count draws. Fallback stages that load DrawID change (a pushed member at
offset 76, no
DrawIndex); the back-end files are in the shader stamp (nx2/translator_files.py:dc271e7fc99bb36con#679 →
5f17d46487bac8f5), so every player translates every program once more at the next start. NoMKWVK_API_VERSIONchange.Reviewer notes
c45636d2..0a61a9e7(5 commits):cherry-pick -xofmacos/mesh-countf27457c0(the counters),e9508f4b(count draws),d49a4991(the growing area),3ea03577(six fixtures' stand-ins) onto #679; conflicts only beside the base's lines(
README.md's fallback row on main's newer table,VULKAN.mdandcount_draws.hbeside #679' macOS-only rewrite,test_count_draws.pykeeping both checks).0a61a9e7:fallback_record_count's bind copied the macOS branch'sRX_VK_LMEM_LOGhook (private_memory_bound,#681's), which this base lacks, so
vk_draw.cppdid not compile; the line goes. Whichever of this PR and #681lands second adds it back, as
fallback_recordhas it.area needs the count draws' header layout).
vram_usage); this branch does not need #642 to build.39da60e1): with #704,fallback_record_countsays its own passend
MeshFallbackandcount_rewrite_recordsCountDraw(Integration 36's b4732392); with #680,test_drawid_directpins theDrawIndex condition this PR changes (b4732392); with #703, the rotation needs #708, which carries both.
Issues
See #420
Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), headb41a03a9, on #6792028d598, without aconflict; this PR's own commits, old → new:
c699eee2→435c47d4,286651ff→092afd49,820b456a→17cace13,7f2423db→02db4980,0a61a9e7→b41a03a9; the commit names elsewhere in this description are from before therebase.
python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (On the wave's merge39da60e1= the nine late renderer PRs' heads (fan-key592cd8d3, fan-indirectfa6dd43a, provoking-firstb8194380, perf-instrea90d3ca, readback-whole-waits203b8b7c, fix/indirect-quadsc6e0c37f, indirect-quads978079d4, fix/mesh-count0a61a9e7, mesh-provoking56c09f1b) with their sibling fixes: 570 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; 38 new tests pass;reports/PubWave-files/PubLate4/wave4/full-39da60e1/. After the rebase onto mainaa95d35f(headb41a03a9): wave C6's mergea10d0c4b, 584/33/18 against main aa95d35f's 543/33/17, one new failure,test_gpu_hang_watch, which passes run alone on the same merge on arm64 and x86_64 (its 2 s bound under the suite's load).)third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.mainand passing here.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)plaine37c097da2plaina997b3629aKosmicKrisp has no fillModeNonSolid and MoltenVK no POINT polygons, and Metal restarts an indexed strip or fan at the all-ones index whatever Vulkan's restart enable says. Before anything is fixed, count whether the game needs it (mode_census.h, Apple only; nothing built or drawn changes): - the graphics pipelines built, where st_pipelines counts them, and the draws recorded, where st_draws counts them, by NVN's polygon mode, with the first 8 LINE and POINT pipelines (program, pipeline key, topology) and programs named; - the indexed strip and fan draws with restart off; a direct one's indices are read whole, a page at a time, through memory_read_as_submitted for the index type's all-ones value (0xFF, 0xFFFF, 0xFFFFFFFF). An indirect one's range is the GPU's and a range the CPU cannot read is not guessed: both are counted as not scanned. Any other draw costs restart_off_strip's compares and its polygon mode's count on the thread's own counters. Printed with the frame statistics every 120 presents ("at present N") and at close ("at close"). The two tests that compile build_graphics and draw()'s binds from the source get a stand-in for the counts. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit 1bd30dceb348909ca03c4cf12709dccbb0900f4a)On a GPU without VK_EXT_mesh_shader (MoltenVK, KosmicKrisp, AMD under Linux by default, Navi 21/22 task + mesh programs on Windows, GTX 10xx, RX 5000) task and mesh programs run as compute dispatches (issue 640), and fallback_record left every MultiDrawMeshTasksIndirectCount out: no dispatch takes a count, and each record needs its own draw index (reports/2026-11-14-mesh-gap.md 3.1-3.2). - count_draws.h and shaders/count_draws_record.glsl are generalized: a Layout reads copy_words words of a record and writes them, then `ones` words of 1. count_draws::plain(indexed) is the count draws' (every word copied, no ones: their records are byte for byte as before); count_draws::mesh() reads a record's count word and writes {count, 1, 1} under the count, {0, 0, 0} past it. The push block is eight words; the SPIR-V header is regenerated (glslang 16.6.0). - vk_draw.cpp count_rewrite_records holds the rewrite's commands (two full barriers, chunks, push descriptors); count_rewrite gives it the plain layout as before. - fallback_record_count: the records and count bound as draw_demands admits them (records across a segment's cut gathered into the arena), the header reset with the census row, the records rewritten into dispatch records, then per record i: its draw command emptied and its first vertex set to the allocator (i > 0; the allocator is never reset, so record i's draw command covers exactly the records its workgroups took), draw index i and its own record as the task's input pushed, the task (or mesh-only) dispatch indirect from dispatch record i, the mesh dispatch from the header, the command copied into the arena, a full barrier between steps. draw() then draws maxDrawCount commands with vkCmdDrawIndirect in one pass (one pass break a count draw), in record order, inside the occlusion query. Records at or past the count dispatch and draw nothing; records past maxDrawCount are never read. What cannot be bound or rewritten is left out loudly and counted. The fallback's records buffer is a transfer source too. - DrawID in a fallback task or mesh stage (a GLCompute entry point, where Vulkan has no BuiltIn DrawIndex) reads a pushed u32 at MESH_DRAW_ID_PUSH_OFFSET (76: the 4 bytes after a fallback task's u32 first, so no offset moves; PRESENCE_PUSH_OFFSET stays 88) and declares neither DrawIndex nor DrawParameters. Stages that do not load DrawID are emitted byte for byte as before. push_constants pushes 0 for single draws. - mesh_convert_bytes makes no 4096-record ring for a count draw where every mesh program takes the fallback. Every platform's fallback changes: count draws that were left out are drawn. Cost follows maxDrawCount: per record two dispatches, three transfers and four full barriers, then maxDrawCount draws (on MoltenVK encoded at submit). Tests: tests/count_draws_test.cpp runs the shader for both layouts (the mesh one over strides 8-20, count words 0-3, every count to past maxDrawCount); tests/test_mesh_count_fallback.py + mesh_count_fallback_test.cpp model the shader, the header steps and the epilogue's allocation under random workgroup orders (each record draws what it draws alone, in order; the census counts what a full area cuts) and check the source; tests/test_mesh_drawid.py + mesh_drawid_ir.cpp emit fallback stages with and without DrawID through the real back end (spirv-val, the pushed member, no builtin; the others byte for byte the base's); test_count_draws, test_resolver_coverage (SITES_MESH), test_mesh_fallback, test_mesh_census and seg_xfer_test follow. README and VULKAN.md say so. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit e9508f4b810c2e1859914c353b9a76cd2b8c4fa4)MeshCountDraws was written on the macOS integration branch, where fallback_record names each compute pipeline it binds for RX_VK_LMEM_LOG (private_memory_bound, MetalScratch). fallback_record_count's bind copied that line. This branch is on the count-draw rewrite, which has no RX_VK_LMEM_LOG, so vk_draw.cpp did not compile ("use of undeclared identifier 'private_memory_bound'"; the CPU tests cut the code out and passed). The line goes; the pipeline is bound as before. Whichever of this PR and the RX_VK_LMEM_LOG one lands second adds the line back, as fallback_record has it. vk_draw.cpp compiled whole for arm64 and x86_64 after the change. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]>0a61a9e755833ac3b900833ac3b900b41a03a9acWIP: fix(vk): draw mesh count draws on the compute fallback, grow its records area on demand, and count what it cutsto fix(vk): draw mesh count draws on the compute fallback, grow its records area on demand, and count what it cutsView command line instructions
Manual merge helper
Use this merge commit message when completing the merge manually.
Checkout
From your project repository, check out a new branch and test the changes.