WIP: perf(macos): Mac performance on MoltenVK #852
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 @ 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
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!852
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/topic-mac-perf"
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: perf · 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 the game ran at about 30 fps in the first field, and a run could not say why: no reason for each render pass end, no
attachment traffic, no GPU timeline. With those lines (#704) the frame turned out to be held by passes broken for every mesh draw
(the Mac has no mesh shaders, so each draw's compute stages end the pass, and on Apple's tile GPUs every break stores and loads
every attachment), by MoltenVK's own widening of 8-bit indices (which loses restart-off primitives at vertex 255 and splits the
Metal pass) and by the presenting thread's walk over the write marks, 0.31 ns a mark on the Mac. Batching the mesh draws dropped
provoking-vertex draws in batched passes, 0.6-0.9 a frame (run #36). This PR folds the macOS port's performance PRs and two
lanes' follow-ups into one: what measured faster is on by default on the Mac, what did not is off, and the measuring lines are
off by default everywhere. It is part of the macOS port. See #420.
What changed
On MoltenVK a run of mesh fallback draws on the same targets is drawn in one pass, their dispatches first; restart-off 8-bit
draws are widened by LS so they fetch vertex 255 as NVN does; and provoking-vertex draws in a batched pass take their list's room
after the admission instead of being left out. On macOS the write marks' walk reads 64 marks a test in 16-byte vector loads. Two
switches stay opt-in (a pass's leading clears as load ops; LS's widening ahead of a pass), and three log-only switches (frame cost
lines, a GPU timeline, the bench's A/B arm) are off by default on every platform. One commit per original PR, in order, then the
ports and the merge fixes:
feat(vk): log-only frame cost lines, a GPU timeline and the bench's A/B exactness arm, all off by default, every platform(
gpu/vk/vk_perf.cpp,gpu/vk/perf_lines.h, hooks invk_draw.cpp,vk_replay.cpp,vk_texture.cpp; NVN's discards countedin
host/nvn_native.c, the poll's sleeps inhost/hos_thread.c;RX_VK_PERF_LINES,RX_VK_GPU_TIMELINE,RX_VK_BENCH_AB).Was: #704
perf(macos): a pass's leading whole clears as its attachments'LOAD_OP_CLEAR, off by default (gpu/vk/load_clear.h,begin_passinvk_draw.cpp;RX_VK_LOAD_CLEAR=1). Was: #780fix(macos): restart-off 8-bit indices drawn as NVN does on MoltenVK; LS's widening ahead of a pass opt-in (gpu/vk/widen8.h,widen8_scan.h, the shaderwiden8.comp,vk_vendor.hindex8_raw/widen8;RX_VK_INDEX8_RAW,RX_VK_WIDEN8=1). Was:#782
perf(macos): batch the mesh fallback's draws of a pass on MoltenVK (gpu/vk/mesh_batch.h, secondary command buffers invk_draw.cppandvk_recorder.h;RX_VK_MESH_BATCH). Was: #783fix(vk): no provoking-vertex draw left out in a mesh batch's segment; its list's room taken after the admission(
provoking_room,records_into_recordinginvk_draw.cpp; tests/test_stream_room.py), and test_pipeline_share's count ofdraw()'s third pipeline lookup. From
macos/stream-roomae7daf5f and d987ab79 (reports/2026-11-15-stream-room.md); itsevery-platform middle commit bbfb465f (a direct quad draw's triangles grow the stream under
RX_VK_STREAM_GROW_OFF) is a PR ofits own,
fix/quad-stream-grow.perf(macos): the write marks' walk 64 marks a test in 16-byte vector loads (collect_ininhost/rx_backend.c, macOSonly;
HOS_COLLECT_VECTOR=0the byte walk). Frommacos/page-scan74619b9b (reports/2026-11-15-page-scan.md), placed inmacOS's branch of the walk (Windows and Linux keep main's word loads).
indirect quads' draw hooked by PerfInstr, test_perf_lines' six sites (#704 x #705, I41 74da414a); test_stream_room cuts
provoking_first(the port x #703); test_darwin_guest_memory takes the walk's pieces apart (the port x #480); fan_indirect'sfixture knows Widen8's bind (#702 x #782, I41 47f4695b); and the merge of topic 7's fixes.
Measurements
Verdict: helps: MeshBatch is worth 1.0 ms of frame and 1.1 ms of GPU a frame (+1.1 fps) and the vector walk 3.0 ms of the
presenting thread's replay and 3.2 ms of frame − GPU, each against a run-to-run noise of 0.17 ms of frame (E7); LoadClear and
Widen8's look ahead each measured under the noise on an M4 Max, so both ship off. The provoking-vertex draws left out in batched
passes went from 0.6-0.9 a frame (run #36: 11,756) to 0 (E9a). The fixes are exact on the GPU where probed: W8-01 r2 (14 of 14
scenes as NVN's reference), LC-01 (327 folds), MB-01 (8 of 8 batched as unbatched).
Hardware: macOS 26.7.1 (25G241), Apple M4 Max, 64 GB; MoltenVK main 5189f8de with the argbuf-slots patch (
c170a745)Scene: the field-dawn save's first area (slot 00, #24's inputs), windowed 1280x720, LS's defaults, frame cap 60 (E7: 240)
Method: one start each with the lanes paused, a 600 s hold (E9a 1,197 s); the medians over the hold's 120-present periods from +20 s of the FPS counter's Dev level and
RX_VK_PERF_LINES' L1-L6 and the replay linesHow to reproduce: the macOS runner (
tools/ls-run.py, Rev13/Rev14) on the named build, quiet-required, field-dawn slot 00, the switches as in the columnBaseline: main does not start on a Mac, so each pair is one integration build with the change's switch off and on: I39
7a7726fefor the three PRs' topics (E3 all off, E4 MeshBatch off, E2 all on), I4178ec1c47for the walk (E9bHOS_COLLECT_VECTOR=0, E9c the vector walk). I41 carries every member here and both ports as they were before Sweep3's rebase; Widen8's look ahead and LoadClear are off by default there, as here.About half of E9b against E9c's 6.46 ms of frame is GPU time (22.8 against 19.5 ms) the walk cannot be credited with; at equal
GPU time the walk's whole-frame saving is about 3.1 ms (reports/2026-11-15-mac-perf-E9.md).
Flags and settings
Eight new switches: MeshBatch and
RX_VK_INDEX8_RAWon by default on MoltenVK only, the vector walk on by default on macOS only;LoadClear and Widen8's look ahead off; the three measuring switches off on every platform.
RX_VK_BENCH,RX_VK_CHUNK,RX_VK_DRAW_STATS,RX_VK_PASS_BARRIER,RX_VK_PASS_TIMING,RX_VK_BARRIER_DEDUPE_OFF,RX_VK_BARRIER_EVERY_DRAW,RX_VK_PUSH_STATE_OLDandHOS_COLLECT_WIDEappear in the diff as main's switches, read as before.RX_VK_PERF_LINES=1RX_VK_GPU_TIMELINE=1or2RX_VK_BENCH_AB=<arm>pass_barrier,dedupe_off,chunk:<n>,load_clear,widen8,mesh_batch), withRX_VK_BENCHRX_VK_LOAD_CLEAR=0or1RX_VK_INDEX8_RAW=0or1RX_VK_WIDEN8=0or1RX_VK_MESH_BATCH=0or<n><n>members, 2-64)HOS_COLLECT_VECTOR=0=0for A/BTesting
Tested: on macOS 26.7.1, Apple M4 Max, at
1b7c7656on topic 7, whose tree this PR's headbc0c7799keeps (its last commit,a merge of topic 7's last fix, changes no file here):
member ran it (36 of them x86_64 or both): 82 of 82 pass. Logs:
reports/Consolidate-files/proof/c8z-m-*.b83bc3d0,--source,five fail, as they should), the tests it touches or cuts (test_provoking_first, test_drawid_direct, test_widen8,
test_seg_gathers, test_draw_side_effects, test_draw_stream_visibility, test_recording_admission, test_fsr_uniform_ownership,
test_mesh_batch, test_mesh_provoking, test_quad_draws, test_load_clear, test_pipeline_share), test_collect_wide (both walks;
with the pre-port
rx_backend.cit fails: no vector walk built), test_darwin_guest_memory, and the merge fixes' tests(test_perf_lines, test_fan_indirect, test_fan_key, test_index8_bind, test_vendor_fixes, test_subgroup_vertex): 46 of 46
pass, the 3 runs on the pre-port tree fail. Logs:
proof/c8z-*.vk_draw.cpp,vk_replay.cppandhost/rx_backend.ccompiled whole on arm64and x86_64 with nx2/build.py's own macOS commands at the head: 6 of 6 exit 0; against topic 8's pre-port tip
b83bc3d0, noundefined symbol added in
vk_draw.cpporrx_backend.c.python -I -B tests/run_all.pyat the head against main aa95d35f's 543/33/17 on this Mac: 622 passed, 15 failed, 17skipped; by name nothing fails here that passes on main; 17 of main's failures pass (the macOS host's and caches' fixes from
topics 1-4), 61 new checks pass,
test_appimage_package_retryis skipped on macOS (#454) andtest_dav1d_av1runs (#454);the 15 failures fail on main too: test_appimage_player_build, test_cpm_discovery, test_crash_dialog, test_env_on,
test_game_layout, test_lift_cleanup_error, test_linux_preflight, test_make_game_cache, test_options_budget_once,
test_options_path, test_pipeline_tour, test_readback_late, test_split_giants, test_vk_pgo, test_walk_key
(
reports/Consolidate-files/checks/compare-mac-perf.txt).mkwvk::name declared in one gpu/vk file and defined in another,each defining file compiled on both arches): 0 not exported, 0 undefined across the 34 objects of each arch.
unifdef -U__APPLE__of every changed non-test C/C++ file, against topic 7): 16 files differ and 9 are new, allof them #704's every-platform measuring lines (log-only, off by default:
vk_perf.cpp,perf_lines.h,perf_op.h,vk_perf.h, their hooks and includes, NVN's discards counted inhost/nvn_native.candgpu/render/rx_nvn.h, the poll'ssleeps in
host/hos_thread.candhost/rx_backend.c) or the MoltenVK paths compiled everywhere and gated at run time on theMoltenVK driver (
load_clear.h,mesh_batch.h,widen8*.h, the vendor flags invk_vendor.handvk_internal.h,vkCmdExecuteCommandsinvk_loader.h,vk_recorder.handvk_work.h,gpu_buffer_bind_importedinvk_memory.cpp, thestream-room port's
provoking_room, behindprovoking_first(), false off macOS). The page scan's walk is the same off Apple,token for token.
78ec1c47(arm64, 1,197 s hold, 12/12 must-shows, clean end, 0 skipped draws), an integration buildthat carries every member here and both ports; run #34 on I38 (x86_64 under Rosetta, 16 m 52 s, every draw drawn, 19/19,
clean exit), which carried #704 and earlier versions of #780, #782 and #783 (not the ports).
Not tested: Windows and Linux (no such machine here): the measuring lines are every platform's, off by default, and the
MoltenVK paths compile there gated off. A Mac with less memory bandwidth than an M4 Max with LoadClear or the look ahead on.
Rosetta in a game run with these members as they are now.
Must be tested:
bc0c7799) and mainaa95d35fthe same way, runpython -I -B tests/run_all.pyon each and start each to the title, then once more withRX_VK_PERF_LINES=1 RX_VK_GPU_TIMELINE=1, expecting the build to succeed, the new tests to pass with no other check changing status, the vk lines to match main's with the switches off, and thevk: L1-L6and T1 lines with them on (Test Farm: @Beanstalk windows linux, build, run_all and start unchanged, then with the perf lines on)Risks
What could break: on MoltenVK, a batched pass drawing something other than the unbatched replay (MB-01 found none), and
restart-off 8-bit draws widened by LS (W8-01 r2 found none). On macOS, the walk missing a write mark (test_collect_wide compares
it with the byte walk on random and dense marks; the pages, order and marks cleared are the byte walk's). Elsewhere, the measuring
hooks cost one flag load each with their switches off (about 1 ns a call; 7.6 µs a frame of run #25's shape).
Turning it off:
RX_VK_MESH_BATCH=0,RX_VK_INDEX8_RAW=0,HOS_COLLECT_VECTOR=0; or revert the commits.Compatibility: Nothing changes for a player's saves,
longshot_options.ini, the shader and pipeline caches (Widen8's computeshader is a new pipeline, built at the first use) or
MKWVK_API_VERSION. Windows and Linux players see no change with theswitches unset.
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: onaa95d35f, through topic 7 (draw-path-quads-mesh).python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (On this Mac at1b7c7656, this head's tree: 622/15/17 against main aa95d35f's 543/33/17; the 15 failures, named under Testing, fail on main too; no check fails here that passes on main.)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.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.