WIP: fix(vk): draw indirect quads and mesh count draws on every driver, with their Mac halves #851
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/tooling
area/upscaling
beanstalk: approved @ 1697603
beanstalk: approved @ 2c5a62e
beanstalk: approved @ 4fd82b7
beanstalk: approved @ 64d6d09
beanstalk: approved @ 6f91fae
beanstalk: approved @ 70a78f9
beanstalk: approved @ 8015c9a
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
risk
high
risk
low
risk
medium
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!851
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/topic-draw-path-quads-mesh"
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 (arm64; x86_64 under Rosetta) · Needs: Windows and Linux, a GPU on the mesh compute fallback and one with mesh shaders
Why
LS leaves out two kinds of draws on every driver. Every indirect quad draw (
NVN_DRAW_PRIMITIVE_QUADSfrom a buffer, with orwithout a count): quads have no Vulkan topology, and draw() builds a direct quad draw's two triangles a quad on the CPU, which it
cannot do for records the GPU holds (
if (d.indirect || d.count < 4) SKIP_HERE();). And on every GPU that draws task and meshprograms 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), every
MultiDrawMeshTasksIndirectCount: no dispatch takes a count, the fallback'srecords area is a fixed 64 MB (a draw needing more is cut silently, over 128 MB refused), and nothing counts what it cuts. This PR
draws both on every driver (the fixes that were #705 and #707, folded in here) and adds their Mac halves (#706, #708). On a Mac the
first vertex of a triangle provokes (MoltenVK 1.4.2 offers the last vertex only with Metal's private API), and draw-path (the PR
before this one) draws the Mac's triangles with their last vertex first: without the halves a quad's two triangles both take their
flat values from the quad's first corner on the Mac, where every other device takes the third and fourth, and the mesh fallback's
rotation list, made for single draws in its first records area, would be read from the wrong place by a count draw and fall short
of a grown area. It is part of the macOS port. See #420.
How to reproduce
Seen on: every platform for the left-out draws (read from the source); macOS 26.7.1, Apple M4 Max, MoltenVK 1.4.2 for the
Mac halves; all measured on the Mac
Settings and switches: defaults
Steps: the standalone MoltenVK probes: QD-01 (
reports/2026-11-14-indirect-quads.md§6: indirect quad draws, not indexed and 8/16/32-bit indexed, every phase of firstIndex, counts none to past max, split dispatches, and itsprov_unrotatedstep: quads coloured flat by the provoking vertex's id), MC-01 (reports/2026-11-14-mesh-count.md§6: mesh count draws, task + mesh and mesh-only, strides 8/16, counts 0 to 0xFFFFFFFF at maxDrawCount 16) and MP-01'sRAWarms (reports/2026-11-14-mesh-provoking.md§4: the fallback's single and count draws of a program with a flat input, areas grown past the first)Expected: each indirect quad record draws what the direct path draws for it; each mesh count record draws what it draws as a single fallback draw; the quad halves show corners 2 and 3, as on every other device; every fallback pixel takes its triangle's last corner (NVN's vertex)
Actual: on main the renderer records nothing for indirect quad draws or for the fallback's mesh count draws, on every driver; on the Mac both quad halves show corner 0, and the fallback's count draws and grown areas take the first corner
What changed
On every driver, indirect quad draws are drawn: their records are rewritten on the GPU into indexed triangle-list records (over a
pattern of quads, or over triangles made from the draw's indices) and drawn with one
vkCmdDrawIndexedIndirect, with or without acount. On every GPU that uses the compute fallback, mesh count draws are drawn (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 lines
count quad draws, mesh draws and what the fallback still cuts. On Apple, where the device provokes the first vertex, each quad
triangle's corners are rotated with its winding and edges kept, and the mesh fallback's rotation list covers the count draws'
commands and the area as it grows; elsewhere the halves change nothing. One commit per original PR, in order:
fix(vk): draw indirect quad draws, with and without a count, on every driver (gpu/vk/quad_draws.h, the shaderquad_draws.comp,quad_rewriteinvk_draw.cpp; the log-only quad count). Every platform: #705's three commits as one,restacked on draw-path as
restack/fix/indirect-quads(98947b98) and merged at this PR's base (f3392371), carried unchanged.Was: #705
fix(vk): draw mesh count draws on the compute fallback, grow its records area on demand, and count what it cuts(
count_draws.h's two layouts,fallback_record_countinvk_draw.cpp,gpu/vk/mesh_area.h, the counters inhost/nvn_native.cand the records header's census; a fallback stage that loads DrawID reads it pushed). Every platform'sfallback: #707's five commits as one, restacked on draw-path as
restack/fix/mesh-count(418a9fc4) and merged at the base(
3d3939ad), carried unchanged. Was: #707fix(macos): quads provoke the same corners where the first vertex provokes (gpu/vk/quad_draws.hkFirstVertexandrotated(),quad_rotated()invk_draw.cpp, the quad report's triangles field). Was:#706
fix(macos): the mesh fallback's rotation follows the count draws and the growing records area (gpu/vk/provoking.hfallback_corners,FallbackRotationandprovoking_fallbackinvk_draw.cpp). Was:#708
c2cfee0fand brace67fd90bc, shader-backend's70cc5ba1, #705'sa27f8f47: the indirect quads' count with the provoking-vertex and fan censuses, its checks to draw-path's shape, and #707's19d6d2d4: test_drawid_direct pins #707's DrawIndex condition); one conflict, intest_provoking_first's comment, resolved tothe base's six sites.
fix(macos): log_frame_stats' MeshCountDraws report block closed again (d99221c4): the base merge of #707 had dropped theblock's closing brace in
vk_replay.cpp, which stopped compiling from there; #707's restack and Integration 41 have it.937e314b): draw-path'sf4430af4, its merge of shader-backend's36752e72(the stand-in clear's unboundedwait inside
#if defined(__APPLE__), so Windows and Linux compile none of that port here either).Measurements
Verdict: fixed where measured: in QD-01 every indirect quad draw equalled the direct path's quads byte for byte (41 of 41
steps) and the rotated quads took corners 2 and 3 on LS's indirect path and the direct path alike (unrotated: 0 and 0); in MC-01
every mesh count draw drew exactly what each of its records draws alone (29 of 29); in MP-01 every step's image equalled the
reference byte for byte, the cut steps cutting exactly the predicted corners. Both new paths cost per maxDrawCount record on
MoltenVK (its per-record Metal draws at submit), not per drawn record.
Baseline: main leaves the indirect quad draws and the fallback's mesh count draws out on every driver, and does not start on macOS.
Each probe runs in one standalone MoltenVK 1.4.2 process: QD-01 compares the rewrite with the direct path's quads (one
vkCmdDrawIndexeda record), MC-01 each count draw with its records drawn as single fallback draws, and QD-01's provoking step andMP-01 the unrotated draws with the Mac halves, at each member's head (
fc8dd951for #705 and #706,e9508f4bfor #707,3f872b0cfor #708).Hardware: macOS 26.7.1, Apple M4 Max (40-core GPU), MoltenVK 1.4.2, 16-core CPU, 64 GB
Scene: QD-01's 41 steps (plain, 8/16/32-bit indexed, wide, single, splits, past the pattern or extent, provoking, cost); MC-01's 29 (task + mesh and mesh-only, strides 8/16, counts 0-0xFFFFFFFF at maxDrawCount 16, splits, cost at 64 and 4,096); MP-01's 22 (task + mesh and mesh-only, counts 0, 1, 16 and past, 1 and 3 commands a call, grown areas, the allowance cutting, single draws)
Method: QD-01
reports/IndirectQuads-files/qd01/runs-qd01-arm64/result.json(271fb491…); MC-01reports/MeshCountDraws-files/mc01/runs-mc01-arm64/result.json(cb763c3a…); MP-01reports/MeshProvoking-files/mp01/runs-mp01-arm64/result.json(3d518854…); each released once on arm64How to reproduce: QD-01's
runcommand (reports/2026-11-14-indirect-quads.md§6); MC-01's (reports/2026-11-14-mesh-count.md§6); MP-01's (reports/2026-11-14-mesh-provoking.md§4)aa95d35f(main does not start on macOS)937e314b(each member's code, measured at its head)Start #32 (I38, with #708 at
3f872b0c, before its area fix): 53,047 rotated fallback draws logged as cut (69 error lines, each10-16 words off the 16,777,216-word area) while the census of corners past the area stayed 0: false cuts, 0 corners lost; #708's
head stops counting and logging them, and what is drawn is unchanged.
Flags and settings
None: no switch. The indirect quads on every driver, the mesh count draws on every GPU's compute fallback; the halves on Apple,
on a device without the last provoking vertex, only.
Testing
Tested: on macOS 26.7.1, Apple M4 Max, game-free, at
90ab8ea1. This head937e314bonly merges the base'sf4430af4(shader-backend's stand-in clear gate
36752e72), which changes nothing macOS compiles (unifdef -D__APPLE__ofvk_texture.cppis90ab8ea1's); the stand-in clear's test and the whole-file compile ran again at it:937e314b: test_null_dummies (the stand-in clear's test) passes on arm64 and on x86_64 under Rosetta, andvk_texture.cppcompiles whole on both arches with this tree's own nx2/build.py macOS commands, 2 of 2 exit 0 (reports/Consolidate-files/AppleGate/logs/).90ab8ea1: 19 test files, 35 runs, all exit 0. 16 ran on arm64 and on x86_64 under Rosetta separately;test_mesh_census,test_mesh_fallbackand the crash-record source contract once (no x86_64 form). #705's:test_quad_draws(#706's too),test_draw_binds,test_seg_gathers; #707's:test_mesh_area,test_mesh_census,test_count_draws,test_mesh_count_fallback,test_mesh_drawid,test_mesh_fallback,test_seg_xfer(area, count fallback and xfer #708's too); #708's:test_mesh_provoking,test_provoking_first,test_fan_key, the crash-record contract; and the base'stest_index8_bind,test_fan_indirect,test_drawid_direct,test_vendor_fixes,test_subgroup_vertex.python -I -B tests/run_all.py --jobs 6) on this Mac against main's (543 passed, 33 failed, 17 skipped): 617 passed, 15 failed, 17 skipped; no check that passes on main fails here. Against the draw-path topic's suite (611, 15, 17) the only differences are six new tests passing:test_quad_draws(#705),test_mesh_area,test_mesh_census,test_mesh_count_fallback,test_mesh_drawid(#707) andtest_mesh_provoking(#708). The 15 failures are main's own on this Mac, the same 15 as draw-path's.mkwvk::name onegpu/vkfile declares and another defines; each defining file compiled with the tree's own flags, arm64 and x86_64, and every symbol an object leaves undefined defined by one of them): 0 of 639 declared names without a definition, 66 of 66 objects compiled, 1,254 name/arch checks with 0 failures, and on each arch 583 cross-file symbols all defined.vk_draw.cppandvk_replay.cppalso compiled whole with nx2/build.py's own macOS commands: 4 of 4 exit 0.unifdef -U__APPLE__of every changed C/C++ file against the merge of draw-path, #705 and #707):quad_draws.h(kFirstVertexandrotated(apple, provokes_last), false off Apple),provoking.h(fallback_corners),vk_draw.cpp(quad_rotated()returns false off Apple;FallbackRotationandprovoking_fallbackreached only whereprovoking_first()holds, never off Apple; the fallback's header carries the area's own words as #707's does) andvk_replay.cpp(the one closing brace that makes it compile again). What Windows and Linux see run differently from the halves: the quad report line ends with its triangles' corners ({0,1,2, 0,2,3}there), a log field.vk_draw.cpp,vk_draw.h,vk_replay.cppand the newquad_draws.h(#705), and every file #707 changes (its counters inhost/nvn_native.c, the back-end files in the shader stamp,count_draws.h, the newmesh_area.h) differ against draw-path (their checks before the fold:reports/PubWave-files/PubLate4/offapple-fix-indirect-quads.txt,offapple-fix-mesh-count.txt).78ec1c47(arm64, MoltenVK main + argbuf-slots): 1,197 s held in play, 12 of 12 must-shows, a clean end, 0 skipped draws; run #34 on I383f872b0c(x86_64 under Rosetta): 16 m 52 s, every draw drawn, 19 of 19 checks, a clean exit. E9a's log has novk: quad drawsline (the field-dawn scene draws no quads, as the quad census found in runs #31-#34) andvk: mesh draws: 78.9 a frame (31.9 direct, 47.0 indirect, 0.0 with a count), all through the compute fallback, its cuts census all zero: the scene makes no indirect quad draw and no mesh count draw, so neither every-platform path ran in the game; the game-free tests and QD-01 and MC-01 cover them.Not tested: Windows and Linux (no PC here): nobody has built or started this stack there, so the every-platform fixes have run
on MoltenVK only (QD-01, MC-01 and the game-free tests), never on a GPU with native mesh shaders or another driver's compute
fallback. A game scene with an indirect quad draw or a mesh count draw (E9a's has none).
Must be tested:
937e314b) and main (aa95d35f) the same way on each (a fallback GPU: GTX 10xx, RX 5000, AMD under Linux by default, Navi 21/22 on Windows) and runpython -I -B tests/run_all.pyon each, expecting this PR's tests to pass and no other check to change status against main; then start each and play to the field withRX_VK_VALIDATION=1(the Khronos layer installed), expecting avk: quad draws (…)line once any quad is drawn, with 0 left out and its triangles field{0,1,2, 0,2,3}, thenvn: DrawMeshTasks…counters,vk: mesh draws (…)with none left out and, on the fallback GPU,vk: mesh fallback cuts so farall zero, no validation error, novk: provoking vertexline, the othervk:lines as main's, and the scenes as on main (Test Farm: @Beanstalk windows and linux, both GPU kinds: build unchanged, run_all, play to the field; by hand: the combined test request, which replaces T-VG83, T-FP2R and T-BFLC)Risks
What could break: on every driver: a pass break per indirect quad draw (two to five full barriers, one to three dispatch
passes) and device-wide buffers made at the first one (the triangles buffer up to 384 MiB a slot for a 128 MiB 16-bit pool tail,
the pattern 24 bytes a quad), grown under a mutex and retired through
vk_retirement_value(); BaseVertex reads firstVertex fornon-indexed quad records (the direct path gives 0): pixels differ only where a program reads it. On every GPU's compute fallback:
mesh count draws that were left out are drawn, at a cost per maxDrawCount record (above); the records header is 16 words, reset
by one 64-byte update; the area can grow past 64 MB where the allowance holds it; two silent cuts stay counted, not fixed (task
counts past 65,535/threads and
task_cap,reports/2026-11-14-mesh-count.md§4). On Apple only: the order of each quadtriangle's corners (edges, winding and split unchanged); the mesh fallback's rotated count draws take one fill each, and a
CPU-filled rotation list grows with the area (4 bytes a corner; a 128 MB area at 31 words is a 16 MB list), made once a growth
and kept; where the allowance holds the list back, the corners past it are cut (logged and counted), not drawn unrotated.
Turning it off: revert the PR (no switch).
Compatibility: Every platform draws the indirect quad draws and the fallback's mesh count draws it left out. Fallback stages
that load DrawID change (a pushed member at offset 76, no
DrawIndex); those back-end files are in the shader stamp(
nx2/translator_files.py), so every player translates every program once more at the next start.quad_draws_comp_spv.hismade from
quad_draws.comp(its test checks it). No save, options key orMKWVK_API_VERSIONchange.Issues
See #420. The macOS topic PRs, merged in this order; "topic N" in each description means these: 1 #845 (macos-host), 2 #846 (vulkan-on-metal, on 1), 3 #847 (mvk-descriptor-limits), 4 #848 (shader-backend, on 2, 3 and #431), 5 #849 (precompile-metal, on 4), 6 #850 (draw-path, on 4), 7 #851 (draw-path-quads-mesh, on 6; carries the former #705 and #707), 8 #852 (mac-perf, on 7).
Checklist
Everyone
type(scope): description) of the status line's type.main.python -I -B tests/run_all.pypasses (any failure that also fails onmainis named).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]>This conflicts with
mainand is built on top of the macOS series (it carries the commits of #845 and others below it).nx2/build.py,nx2/make_game.pyanddocs/development.mdconflict with #577 (the WebGPU renderer removed from the Windows build). Please rebase onto currentmainonce its base PR #845 lands or is rebased; I didn't touch it because a rebase here would have to be redone after that.WIP: fix(macos): the Mac halves of indirect quads and mesh count drawsto WIP: fix(vk): draw indirect quads and mesh count draws on every driver, with their Mac halvesView 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.