WIP: fix(macos): count draws rewritten where MoltenVK has no drawIndirectCount, and buffer updates copied from the stream #679
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!679
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/count-draws"
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: low · Tested on: macOS 26.7.1, Apple M4 Max (MoltenVK 1.4.2), arm64 and x86_64 under Rosetta · Needs: macOS, Apple silicon (a start to play on the rebased stack); Windows and Linux, any GPU (build and start)
Why
Two MoltenVK 1.4.2 gaps, both found while making the game's count draws work on a Mac, in one PR because the second was found
by the first's GPU probes and both were integrated together.
drawIndirectCount, and itsvkCmdDrawIndirectCountandvkCmdDrawIndexedIndirectCountare empty functions. LS called themwhenever the game's draw had a count, so on a Mac every such draw was dropped without a word.
vkCmdUpdateBuffer's bytes in its command pool's shared temporary buffers when itencodes, and every later direct draw whose pipeline reads the draw index zeroes the first word of the temporary buffer its own
draw-index word lies in, not that word (fixed upstream after the release,
bc0d5bf733). An update staged at that buffer's startlost its first word before the GPU copied it: an event signal, a uniform update or a stream write of LS's could arrive with its
first word zero.
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 stack without this PR)
Steps: probe MT-01's last step and CD-01 (count draws), CR-01 (buffer updates): standalone MoltenVK processes, commands in
reports/2026-11-14-count-draws.md§7/§11 andreports/2026-11-14-copy-race.md§2Expected: a count draw draws its records under the count; a buffer update's bytes all land
Actual: MT-01's 8-bit
vkCmdDrawIndexedIndirectCountdrew nothing (its centre pixel black); CR-01: 1,010 trials lost exactlythe
vkCmdUpdateBufferword, only when a direct draw reading the draw index was encoded after it in the same command poolWhat changed
On macOS, where the device has no
drawIndirectCount, a count draw's records are rewritten on the GPU into the recording's arenaand drawn as a plain indirect draw of maxDrawCount records; and on MoltenVK the renderer's buffer updates are recorded as copies
from the recording's stream. Every other system and driver records what it did before.
b97c61c8,f09886c7, then this PR's owne8e0f010):gpu/vk/count_draws.h(new: the dispatch plan, itswindows, chunks and seven-word push constants) and
gpu/vk/shaders/count_draws.comp+count_draws_record.glsl(new; the SPIR-Vheader
count_draws_comp_spv.hmade with glslang): outside the pass, between two full barriers, a dispatch copies the recordsunder the count the GPU reads and zeroes the rest up to maxDrawCount (instanceCount 0 draws nothing); each record keeps its draw
index and its words, the count is clamped by construction, and records at or past it are never read.
gpu/vk/vk_draw.cpp:count_rewrite_on(macOS and nodrawIndirectCount, orRX_VK_COUNT_REWRITE=1on a Mac whose device has it, to compare),count_rewriteatdraw()'s pre-pass point after SegBind's gathers, the indirect branch drawing the rewritten records, thedriver's count draw reached only without them; a draw it cannot rewrite (records off 4-byte boundaries, no pipeline, no arena
room) is left out loudly, never sent to the empty function.
draw_count_reportwith the frame statistics, in its own block(
vk_replay.cpp);vk_main.cppsaysvk: no drawIndirectCountat the start (macOS only).e0e1ea92):gpu/vk/vk_draw.cppcmd_update_buffer(declared invk_internal.h): on MoltenVK, whencmdis the recording's and its stream has room, the bytes go into the stream (16-aligned) and a
vkCmdCopyBuffertakes theupdate's place in the command order; otherwise
vkCmdUpdateBufferas before, logged and counted.vk_replay.cppgpu_write(after main's ordering barrier, #329) and
gpu_write_piecesrecord through it. Other drivers:vkCmdUpdateBuffer, as before.gpu/vk/VULKAN.md(both items),docs/development.md(theRX_VK_COUNT_REWRITErow,5740f525).tests/test_count_draws.py+tests/count_draws_test.cpp(new) run the shader's own record code and the plan on the CPUover every count from 0 to past maxDrawCount, strides, offsets, alignments, indexed and not, split dispatches, check the SPIR-V
header against its source and the renderer's source (both macOS gates);
tests/test_update_staging.py+tests/update_staging_test.cpp(new) runcmd_update_bufferon a model of the stream and of MoltenVK's clobber; #663'stests/seg_gathers_test.cpp(a case on a device without the feature) andtests/seg_xfer_test.cpp(not MoltenVK: its updatesstay
vkCmdUpdateBuffer, the common path's digests unchanged) follow, and main'stests/test_inline_write_order.py(#329) getsa
cmd_update_bufferstand-in (c45636d2).83c119e7,93e93a8a) and its revert (4df764c4): netzero (
4df764c4's tree isf09886c7's). CD-02 showed the drain did not help, and CR-01 found the cause above.Measurements
Verdict: both fixed where they were measured: the rewrite drew exactly in every probe step whose count was not itself lost to
MoltenVK's update clobber (CD-01: 41 of 45 as predicted; the 4 that differed are CR-01's cause, in the probe's own count update),
and copying from LS's own memory landed in all of CR-01's 3,440 copy trials while staged updates lost their first word in 1,010.
In the game (run #26) no update fell back to
vkCmdUpdateBuffer; the game made no count draws in that run, so the rewrite was notexercised there. A count draw costs a pass break and, on MoltenVK, one Metal draw a record at submit.
Baseline: main does not start on macOS. The probes compare the driver's own calls with this PR's in standalone MoltenVK 1.4.2
processes; the game run is the integration build
6962f8fb(this PR's code before its Apple gate), arm64, field-dawn save,900 s held in play, against #25 (
e7730869, without it).Hardware: macOS 26.7.1, Apple M4 Max (40-core GPU), MoltenVK 1.4.2, 16-core CPU, 64 GB
Scene: probes: 16×16 targets, counts 0, 1, 7, 16, 20 and 0xFFFFFFFF over maxDrawCount 4 to 65536 (CD-01), 17 arms × 50 trials ×
{no drain, drain}, argument buffers on and off (CR-01); the game: start, title, load, 900 s in play (#26)
Method: CD-01
reports/CountDraws-files/cd01/runs-cd01-arm64/result.json(7d28faa6…); CD-02cd02/runs-cd02-arm64/result.json(
9c74f6ce…); CR-01reports/CopyRace-files/…/runs-cr01-arm64/result.json(f2f79774…); run #26reports/2026-11-14-mac-startup-26.mdHow to reproduce: the probes'
driver.py runcommands in those reports; in the game, readvk: no drawIndirectCount,vk: count draws (…)andvk: a buffer update of … recorded as vkCmdUpdateBuffer on MoltenVK6c9edffe(MoltenVK's own calls, as main records them)vkCmdDraw*IndirectCount: nothing drawn (MT-01 black; CD-01's native steps the marker alone)vkCmdUpdateBufferbefore a draw-index draw: 1,010 trials lost the update's first wordvkCmdUpdateBufferfallbacks; 0 count draws made (215 reports); 26.6 fps against #25's 29.8 under other lanes' CPU loadFlags and settings
One new switch, macOS only, off by default.
RX_VK_COUNT_REWRITE=1drawIndirectCounttooTesting
Tested: on macOS 26.7.1, Apple M4 Max, at this head (main
6c9edffeplus #677, #663 and what it carries); the runsmarked
proof-04and the compiles were at5740f525, which this head differs from only bytests/test_inline_write_order.py:tests/test_count_draws.pywith/usr/bin/clang++andclang++ -arch x86_64(Rosetta): pass, 96,000 rewrites and 30,459,009checks, 0 failures; with the test copied onto #677's head it fails (
count_draws.compmissing), and on the picks'head
0e9cf1fd(beforee8e0f010) it fails on the macOS gate (reports/PubWave-files/PubLate2/proof-04/cd/cd-count-*,proof-02b/cd-base-count-*,proof-05/).tests/test_update_staging.py(both compilers): pass; on the commit beforee0e1ea92it fails (cmd_update_buffer (vk_draw.cpp) is missing) (proof-04/cd/cd-staging-*,proof-02b/cr-base-staging-*).tests/test_seg_gathers.pyandtests/test_mode_census.py(--arch arm64,--arch x86_64): pass (proof-04/cd/);tests/test_seg_xfer.py(both arches) passed at0e9cf1fd(proof-02b/cd-segxfer-*), whosegpu_writethis head keeps.gpu/vk/vk_draw.cpp,gpu/vk/vk_main.cppandgpu/vk/vk_replay.cppcompiled whole for arm64 and x86_64 with main's rendererflags (#476's, #492's and #490's macOS compile pieces merged in a scratch tree only): 6 objects, exit 0, no warnings
(
reports/PubWave-files/PubLate2/compile/count-draws/).unifdef -U__APPLE__): four renderer files differ (vk_main.cpponly in comments) andcount_draws.hand its SPIR-Vheader are included, but
count_rewrite_onreturns false and the device line is not compiled, so a count draw goes to thedriver's count draw as before;
cmd_update_bufferrecordsvkCmdUpdateBufferoff MoltenVK(
reports/PubWave-files/PubLate2/offapple-count-draws.txt).tests/test_inline_write_order.py(main's, #329; both compilers): pass; at5740f525, beforec45636d2, it does not compile(
cmd_update_bufferundeclared) (proof-06/cd-inline-*,inline-base-5740f525-arm64).through
tools/ls-suite.pyat #680's head (this PR plus its three picks): 209 pass, 16 fail: 13 fail on main6c9edffeon this Mac too, andtest_precompile_compute_guard,test_shader_module_guardandtest_present_aspect_vulkan, whichmain's full suite skips here and which fail the same way at #663's head when named. The first such run (at the stack before
c45636d2) also failedtest_inline_write_order(reports/PubWave-files/PubLate2/related-drawid-direct-02/,-compare.txt;related-drawid-direct-01-compare.txt;related-base-b8579f1a-01/).93e93a8a: 570/1/16;e0e1ea92: 571/1/16; the failuretest_menu_i18n, failing on their base too).Not tested: a count draw in the game (none in #26; MoltenVK 1.4.3, built from main in run #28, reports
drawIndirectCountandtakes the driver's path); a game start on this PR's stack (main does not start on macOS by itself; #26 had the code before
e8e0f010's gate, which changes nothing on a Mac); not built on Windows or Linux; its test request covers it.Must be tested:
vk: no drawIndirectCount, thevk: count draws (…)lines with 0 left out, and novk: a buffer update of … recorded as vkCmdUpdateBuffer on MoltenVKline(by hand: the parent's game run).
c45636d2, it carries #677, #663, #662, #658, #649, #648, #647, #645,#644, #643 and #491) and main
6c9edffethe same way and start each to the title, expecting novk: no drawIndirectCountorvk: count drawsline, and every other line to match main's (by hand: test request T-XXXX).Risks
What could break: on macOS without
drawIndirectCountonly: a count draw now draws (it was dropped), with a pass break andtwo barriers, and MoltenVK encodes one Metal draw a record (maxDrawCount 65536: about 10-16 ms a submit); an 8-bit count draw is
widened by MoltenVK to its pool's end (with #678, which sizes only direct binds). On MoltenVK every buffer update the
renderer records takes room in the recording's stream; without room it falls back to
vkCmdUpdateBuffer, logged.Turning it off: revert the PR (the rewrite has no off switch on a device without the feature: the alternative is the dropped
draw).
Compatibility: Nothing changes for players outside macOS. No file in the shader stamp changed (
count_draws.handshaders/are not stamped); noMKWVK_API_VERSIONchange.Reviewer notes
e1153ec7..c45636d2(6 commits):cherry-pick -xofmacos/count-drawsb97c61c8(38d61263: one conflict, the includenext to #677's, without #681's),
f09886c7(203934d4, clean) ande0e1ea92(0e9cf1fd: one conflict,gpu_write: main'sfull_barrierbefore the update, #329, kept, thencmd_update_buffer); each pick's changed lines equal itssource commit's (sorted;
pick-count-draws-*-lines.txt). Then this PR's own:e8e0f010(every-platform split):b97c61c8asked the rewrite ofdrawIndirectCountalone, on every system, so a Windows orLinux device without the feature would have changed from the driver's count draw to the rewrite; it is now macOS's alone
(
count_rewrite_on, the device line), withtest_count_drawschecking both gates. Taking it to every device without thefeature is its own PR, for the owner.
5740f525: the switch's row indocs/development.md(the topic named it inVULKAN.mdonly).c45636d2(semantic): main'stest_inline_write_order(#329, never on the integration branch) compilesgpu_write, whichnow calls
cmd_update_buffer; the fixture gets the non-MoltenVK stand-in (vkCmdUpdateBufferthrough the same work wrapper),and its barrier and work-count negatives still fail as they must. Found by the related checks at the stack's top.
83c119e7+93e93a8aand its revert4df764c4are left out: net zero (git rev-parse f09886c7^{tree} 4df764c4^{tree}: bothf234378e;drain-net-zero.txt).log_frame_stats, andtest_mode_censusholds thatlayout) and through it #663 (SegBind's gathers and arena, which the rewrite follows and uses;
seg_gathers_test.cpp,seg_xfer_test.cpp), #662, #658, #649, #648, #647, #645, #644, #643 and #491.git merge-treeof this head (reports/PubWave-files/PubLate2/merge-check-count-draws.txt):gpu/vk/vk_draw.cppwith#681 (includes next to each other: keep both); the rest inherited from #677 and #663. With #678
(also on #677): clean; read together, an 8-bit count draw rewritten on MoltenVK reaches the indirect branch, keeps the
pool's end and is counted by both reports (Integration 34). #680 stacks on this PR.
Issues
See #420
Checklist
Everyone
type(scope): description) of the status line's type.main(maine090cc48).python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (On the wave's merge937a502a= drawid-direct9c2bd272(carries this PRc45636d2and mode-census) + mvk-temp3e34116c+ metal-scratch4edae82c: 562 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; 30 new tests pass;test_darwin_audio_service, Darwin-only, is skipped by the runner;reports/PubWave-files/waveC2r/full-937a502a/.)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)c45636d23c1ca0ed47921ca0ed47922028d598e6Folded into #850 (fix(macos): draw-path emulation on MoltenVK)
Pull request closed