WIP: fix(macos): draw-path emulation on MoltenVK #850
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
–
Assign to me
1 participant
Notifications
Time tracker
Due date
Manually add time
No due date set.
Dependencies
No dependencies set
Remove Dependency
This will remove the dependency from this pull request. Continue?
Reference
MagicBeanDev/Longshot!850
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/topic-draw-path"
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, any GPU
Why
On a Mac, LS's draws go through MoltenVK (Vulkan on Metal), and MoltenVK 1.4.2 draws several kinds of draw differently from
every other driver, or not at all. Loading the first scene ran the GPU out of memory (53 GB of 64) and lost the device, because
MoltenVK widens 8-bit indices to the end of the bound buffer. Count draws were dropped without a word (MoltenVK's count calls
are empty). A buffer update could lose its first word, and a direct draw that reads the draw index read a stale one (MoltenVK
writes the draw index to the wrong word). A triangle fan could be drawn as a list, or a list converted as a fan, depending on
which draw built the pipeline first; an indirect fan of several records could garble its own triangles. And every primitive took
its flat values from its first vertex, where NVN and LS everywhere else take the last. This PR draws each of these the way the
game asks on MoltenVK, counts what the game does where a fix was not yet known to be needed, and documents the MoltenVK build
the port recommends. 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
Settings and switches: defaults (the macOS port's stack without this PR: main does not start on macOS)
Steps: start the game and load the field-dawn save (START, then A), sampling the GPU's memory (
ioreg -r -c AGXAccelerator -d 1); for each other gap, the members' standalone MoltenVK probes (commands in their reports, named under Measurements): MT-01 and CD-01 (count draws), CR-01 (buffer updates), DI-01 (draw index), FK-01 (fan key), FI-01 (indirect fans), PV-02 (flat values)Expected: the load reaches play within a few GB of GPU memory; every probe draw covers its own pixels with NVN's values, as on every other driver
Actual: the GPU's memory reaches about 53 GB and the device is lost in the load (
kIOGPUCommandBufferCallbackErrorOutOfMemory); a count draw draws nothing; 1,010 CR-01 trials lose the update's first word; 1,688 of 3,744 DI-01 draws read a stale draw index; 96 of 288 FK-01 draws take the wrong topology; every packed multi-record FI-01 fan is wrong; every PV-02 pixel holds the first vertex's valueWhat changed
On MoltenVK (on macOS, or where the device lacks a feature only MoltenVK lacks), draw() binds 8-bit indices with their size,
rewrites count draws on the GPU, copies buffer updates from the stream, draws direct draw-index readers as one indirect record,
keys fans apart, splits multi-record indirect fans into a call a record, and draws triangles with their last vertex first. Every
other driver records what it did before. Two Apple-only censuses count polygon modes, restart-off strips, fans and provoking-vertex
draws (log-only). One commit per original PR, in order:
feat(macos): count NVN's polygon modes and restart-off strip draws on Apple, log-only (gpu/vk/mode_census.h, the census invk_draw.cpp, its report invk_replay.cpp). Was: #677fix(macos): bind a direct draw's 8-bit indices with their size on MoltenVK (vk_vendor.hindex8_size,RX_VK_INDEX8_SIZE;VK_KHR_maintenance5asked for only then invk_main.cpp;vkCmdBindIndexBuffer2KHRthroughbind_index). Was:#678
fix(macos): count draws rewritten where MoltenVK has nodrawIndirectCount(gpu/vk/count_draws.h, the compute shadercount_draws.comp), and buffer updates copied from the stream on MoltenVK (cmd_update_buffer). Was:#679
fix(macos): direct draws that read the draw index drawn as one indirect record on MoltenVK (gpu/vk/drawid_direct.h,drawid_indirect). Was: #680docs(macos): MoltenVK main 5189f8de with the argbuf-slots patch as the recommended MoltenVK input, and how to build it(
docs/macos-moltenvk.md, the patch, theLONGSHOT_MOLTENVKrow,gpu/vk/VULKAN.md). Was:#691
fix(macos): fans keyed apart from the other triangle topologies on MoltenVK (gpu/vk/fan_key.h, the pipeline key and theprogram cache's stamp in
vk_progcache.cpp; a fan census). Was: #701fix(macos): an indirect fan of more than one record drawn a call a record on MoltenVK (gpu/vk/fan_indirect.h,fan_indirect_split). Was: #702fix(macos): take flat values from each primitive's last vertex on MoltenVK, as NVN does, for direct triangle draws and themesh fallback (
gpu/vk/provoking.h,provoking_list/provoking_write, the fallback's rotation list). Was:#703
test(macos): test_fan_indirect's cut of the indirect block ends at draw()'s next branch (c2cfee0f): where #702 met #703,the cut took in #703's branch and its fixture stopped building; the line Integration 37's
eb4d0204has. A test only.70cc5ba1(its test-fixture fix).fix(macos): draw_index8_report's closing brace (67fd90bc): where #701's fan census report met #678's 8-bit report, thebrace was dropped and
vk_draw.cppstopped compiling from there; it goes back where Integration 41 has it.Measurements
Verdict: each gap fixed where it was measured: the game reached play and held it at 9.3 GB of GPU memory at most (lost at
53 GB before), and in each member's MoltenVK probe the fixed path drew exactly what the game asks (count draws, buffer updates,
draw indices, fans, indirect fans, flat values), at no cost the probes could tell from their noise except where a member says.
The censuses answered their questions: the game draws FILL only, its restart-off strips hold no all-ones index.
Baseline: main does not start on macOS, so nobody could run main against this PR in the game. The before column is what main
records (MoltenVK's own calls, the class key, the raw bind and draws), run in each member's standalone MoltenVK 1.4.2 probe or in
the integration build just before the member's commit; the after column is this PR's code as each member measured it, in the
probe or the integration build its row names (#678: run #23
552cc3d4against run #24e7730869; #691: starts #266962f8fb,#28
b8170e55, #31b4732392).Hardware: macOS 26.7.1, Apple M4 Max (40-core GPU), MoltenVK 1.4.2 (#691's start #31: MoltenVK main 5189f8de + argbuf-slots), 16-core CPU, 64 GB
Scene: the probes' steps (16×16 targets; each member's arms, both orders, 3 to 264 trials); the game: the title, the field-dawn save's load and 900 s held in play (runs #21-#26, #28, #31)
Method: each probe's
result.jsonand each run's longshot_log.txt lines, one run each, as each member's PR states them: MT-01 and CD-01 (reports/2026-11-14-count-draws.md), CR-01 (2026-11-14-copy-race.md), DI-01 (2026-11-14-drawid-direct.md), FK-01 (2026-11-14-fan-key.md), FI-01 (2026-11-14-fan-indirect.md), PV-02 (2026-11-14-provoking-first.md), runs #21-#26 (2026-11-14-mac-startup-2x.md), MS-01 and starts #28/#31 (#691)How to reproduce: each probe's
driver.py runcommand in its report; in the game, a macOS build of the port's stack, the field-dawn save, and thevk:lines named belowaa95d35f(as main records it; main does not start on macOS)67fd90bc(each member's code, measured where its row says)vkCmdDraw*IndirectCount: nothing drawnno free slot)Flags and settings
Two new switches, both macOS or MoltenVK only, and the recommended MoltenVK input named in an existing build switch's row.
RX_VK_INDEX8_SIZE=0or1(#678)VK_KHR_maintenance5)0off,1on where the device hasmaintenance5and 8-bit indicesRX_VK_COUNT_REWRITE=1(#679)drawIndirectCounttooLONGSHOT_MOLTENVK(#691, docs only)docs/macos-moltenvk.md; 1.4.2 still acceptedTesting
Tested: on macOS 26.7.1, Apple M4 Max, game-free, at this PR's head
67fd90bc:67fd90bc: 23 test files, 45 runs, all exit 0. 19 ran on arm64 and on x86_64 under Rosetta separately (test_count_drawsalso in its default form),test_macos_build_flagsandtest_macos_packageboth arches in one run (and build_flags' default),test_check_pr_descriptiononce (no x86_64 form). Among them shader-backend'stest_vendor_fixesandtest_subgroup_vertex, whose fixtures the base fixed at70cc5ba1.python -I -B tests/run_all.py --jobs 6) on this Mac against main's (543 passed, 33 failed, 17 skipped): 611 passed, 15 failed, 17 skipped; no check that passes on main fails here. By name: this PR's eight new tests pass, as do the stack's (the topics it carries add 42 more), 17 of main's failures here pass (the macOS host's), one becomes a declared macOS skip, and one skip runs and passes. The 15 failures are main's own on this Mac, and the same 15 as the macOS host topic's suite.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 637 declared names without a definition, 66 of 66 objects compiled, 1,250 name/arch checks with 0 failures, and on each arch 581 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 base): the macOS fixes are runtime-gated, not#ifdef'd, so the text off Apple changes invk_draw.cpp,vk_draw.h,vk_internal.h,vk_loader.h,vk_main.cpp,vk_progcache.cpp,vk_recorder.h,vk_replay.cpp,vk_vendor.h,vk_work.hand seven new headers (mode_census.h,count_draws.hand its shader header,drawid_direct.h,fan_key.h,fan_indirect.h,provoking.h). Each change returns at once off MoltenVK or off__APPLE__(dev.driver_id == VK_DRIVER_ID_MOLTENVK,provoking_first(),count_rewrite_on()): the census and report calls;vkCmdBindIndexBuffer2KHRdeclared in the loader and recorder lists and recorded only withindex8_size(MoltenVK's vendor fix),VK_KHR_maintenance5asked for only then;vkCmdUpdateBufferthroughcmd_update_buffer(the same call off MoltenVK); the fan key class and the program cache's stamp word on MoltenVK only.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.Not tested: Windows and Linux (no PC here): every change there is a runtime gate that should leave what is built, recorded and
drawn as main's, but nobody has built or started this stack off the Mac.
Must be tested:
67fd90bc) and main (aa95d35f) the same way and runpython -I -B tests/run_all.pyon each, then start each to the title, expecting this PR's new tests to pass, no other check to change status against main, the samevk:lines in longshot_log.txt as main's (no census, 8-bit, count-draw, draw-index, fan or provoking-vertex line) and no program translated again (Test Farm: @Beanstalk windows and linux: build unchanged, run_all, start to the title)Risks
What could break: on MoltenVK or macOS only: a count draw now draws (a pass break, and one Metal draw a record: 10-16 ms a
submit at maxDrawCount 65536); buffer updates and converted draws take room in the recording's stream (without room, drawn or
recorded as before and logged); one more pipeline for each program drawn both as a fan and as another triangle topology, and the
first start remakes the program cache once (the stamp word); an indirect multi-record fan costs MoltenVK's per-call dispatch a
record; a converted triangle draw reads its indices on the CPU and writes a list into the stream. Elsewhere: the runtime gates
above; nothing recorded differently.
Turning it off:
RX_VK_INDEX8_SIZE=0for the 8-bit binds; the rest by reverting the PR (no other switch turns them off).Compatibility: on macOS the program cache is rebuilt once (fan key stamp). Elsewhere nothing changes: no save, options key,
shader cache or vk ABI change.
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, #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]>View 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.