WIP: fix(macos): restart-off 8-bit indices drawn as NVN does on MoltenVK; LS's widening ahead of a pass opt-in (RX_VK_WIDEN8=1) #782
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!782
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:mac/widen8"
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 main 5189f8de + argbuf-slots), arm64 and x86_64 under Rosetta · Needs: macOS, Apple silicon (a start to play beyond the field-dawn hold on the rebased stack); Windows and Linux, any GPU (build and start)
Why
The game draws much of its UI and many small meshes with 8-bit indices. Metal has no 8-bit index type, so MoltenVK widens a draw's
8-bit indices to 16 bits itself, in a compute encoder, right before every such draw (
MVKCmdDraw.mmconvertUint8IndexBuffer).Its public build widens the index 0xFF to 0xFFFF whatever the draw's primitive restart state, and Metal restarts there, so an 8-bit
list or strip with restart off that uses vertex 255 loses those primitives on the Mac. NVN draws vertex 255. With this PR such draws
fetch vertex 255, as on the Switch: LS widens their indices itself, zero-extended.
MoltenVK's widening also stores the render pass's attachments, runs the widening and begins the pass again, loading them back: 57.5
times a frame in run #31's field-dawn hold. This PR can widen a pass's 8-bit indices before the pass begins, so the pass is not
broken for them; on an M4 Max that measured under the run-to-run noise, so it ships off, behind
RX_VK_WIDEN8=1. It is part of themacOS port's performance work (MacPerf §4 #4). See #420.
How to reproduce
Seen on: macOS 26.7.1, Apple M4 Max, MoltenVK main 5189f8de + argbuf-slots (its public widening,
convertUint8Indices)Settings and switches: defaults
Steps: W8-01 r2's standalone MoltenVK probe (
reports/2026-11-14-widen8.md§10.5): an 8-bit indexed list or strip with primitiverestart off whose indices hold 0xFF, bound 8-bit as LS binds it without this PR. In the game no spot is known yet: in the field-dawn
hold the census met no such index.
Expected: vertex 255 drawn, as NVN draws it (W8-01 r2's reference, the indices widened on the CPU by the draw's own restart state)
Actual: those primitives are lost: 76 to 1,443 pixels a scene differ from NVN's picture in the 10 scenes that fetch vertex 255
What changed
On MoltenVK a restart-off 8-bit draw binds its indices widened zero-extended by LS as
VK_INDEX_TYPE_UINT16: at the draw, its passended and begun again for it, as MoltenVK's own widening does. Restart-on draws keep MoltenVK's values (0xFF to 0xFFFF, NVN's
restart). With
RX_VK_WIDEN8=1(off by default),begin_passalso looks ahead in the replay's operations for the pass's 8-bitindexed draws and widens their indices in one compute run between the pass-start barrier and the pass, both forms of each range
(restart's and the zero-extended one); each draw then binds the form its own restart asks for. Anything not provably the same binding
with nothing between that may write it binds its 8-bit indices as before (with restart off: widened at the draw).
gpu/vk/widen8.h(new: the forms,mapped()andat_draw(), the ranges a pass holds and their rules,Table::find),gpu/vk/widen8_scan.h(new: the 8-bit draws ahead in the op stream, stopping at anything that ends the pass or maywrite memory),
gpu/vk/shaders/widen8.comp+widen8_pair.glsl(+widen8_comp_spv.h, made with glslang; the.glslis alsocompiled as C++ by the test), with a census of the 0xFF met zero-extended (log-only).
gpu/vk/vk_draw.cppwiden8_before_pass(inbegin_pass, after the pass-start barrier, before the begin, held ornot),
widen8_drawn(each draw of the pass: may it write memory),widen8_before_draw(a restart-off draw no range serves),widen8_rangeat the direct and indirect 8-bit binds (W8Bind),widen8_reportwith #678'svk: 8-bit indicesline;gpu/vk/vk_memory.cppgpu_buffer_bind_imported(the binding a draw would make of a range one imported pool already holds, withno import, mirror or wait, so it can be asked ahead of the draws' admission);
gpu/vk/vk_replay.cppreplay_cursor(the op thereplay is at); PerfInstr's
L1names the new pass end (restart-off 8-bit indices) andL3no longer counts a draw bound 16-bit.gpu/vk/vk_vendor.hvendor fixesindex8_raw, on on MoltenVK's driver only, andwiden8(the look ahead), off onevery driver,
RX_VK_INDEX8_RAWandRX_VK_WIDEN8=1over them (the look ahead on MoltenVK only,widen8_on); both need pushdescriptors (
gpu/vk/vk_main.cpp, one device line each). The bench's B1 arm:RX_VK_BENCH_AB=widen8(withRX_VK_WIDEN8=0, so Ais off;
RX_VK_INDEX8_RAWon in both).7fd975fb, Integration 40's2b4b7b8efor these files (vk_vendor.h, theRX_VK_WIDEN8row,test_widen8,widen8_test.cpp);test_widen8also holds that the widening at the draw needs no look ahead.7bce4186): onepass_keepsfor #780'spass_onand this PR's; a pass isbarrier, widening, then its begin, recorded at once or held for its leading clears;
widen8_before_drawends a kept pass withend_pass, which settles a held begin first.docs/development.md(RX_VK_WIDEN8,RX_VK_INDEX8_RAW, the bench arm). Tests:tests/test_widen8.py+tests/widen8_test.cpp(new);
test_load_clear(the widening inbegin_pass),test_draw_binds,test_readback_part,test_resolver_coverageandseg_gathers_test.cppfollow.Measurements
Verdict: the fix is exact on the GPU where probed: in W8-01 r2 LS's widening drew every scene as NVN's reference, 0 pixels apart
in 14 of 14, while MoltenVK's own widening lost the restart-off draws' vertex 255 in the 10 scenes that fetch it, as predicted. The
look ahead doesn't help measurably on an M4 Max: in the field-dawn hold all on but it (E4b) and all on (E2) both ran 33.7 fps and
29.67 ms a frame, GPU 19.2 against 19.1 ms, under the run-to-run noise of 0.17 ms of frame, 0.0 ms of GPU and 0.2 fps (E7), so it
is off by default. Without it the fix alone leaves MoltenVK no 8-bit draw to split (
L30.0 in both runs), for 18.8 LS pass ends aframe (0.6 with the look ahead).
Baseline: main does not start on macOS. W8-01 r2 draws each scene three ways in one standalone MoltenVK process: A, 8-bit binds
widened by MoltenVK (LS without this PR); B, LS's widening as
vk_draw.cpprecords it; C, NVN's reference (the indices widened onthe CPU by the draw's own restart state), and compares the pictures. The E-series runs are the integration build I39
7a7726fe(thisPR's code with #780's and #783's, and the other integrated macOS topics; the look ahead on by default there),
arm64, one run each: before is E4b, all on but the look ahead (
RX_VK_WIDEN8=0,RX_VK_INDEX8_RAWon), after is E2, all three on.This head's Widen8 functions are I39's text; only the look ahead's default differs (off here, as Integration 40 made it).
Hardware: macOS 26.7.1 (25G241), Apple M4 Max (40-core GPU), MoltenVK main 5189f8de + argbuf-slots (
c170a745), 16-core CPU, 64 GBScene: W8-01 r2: 14 scenes: lists and strips with restart on and off, base vertex, odd offsets, many draws in a pass, a range used
twice, indirect records and strips, a GPU update before the pass, a list using vertex 255, draws widened at the draw. The E-series:
the field-dawn save, slot 00, entered by START and A×5, then 600 s held in play, windowless; the instrument set on in every run
(
RX_VK_PERF_LINES=1,RX_VK_GPU_TIMELINE=1, the present trace); quiet required before each start, the other lanes pausedMethod: W8-01 r2:
reports/Widen8-files/w801r2/runs-w801r2-arm64/result.json(d700145a…), released once on arm64 by the parent;review
ce1a3348…(ls_head47f4695b, whose widening files this head carries unchanged), buildaa862e73…. The E-series: themedians of the hold's 120-present periods from +20 s (E4b 162 periods, E2 141); one run each, the noise from E7 (a repeat of E2);
runs
mac-arm64-7a7726fe-05(E4b),-02(E2),-08(E7); reportsreports/2026-11-15-mac-perf-E4b.md,-E2.md,-E7.md, theseries
reports/2026-11-15-mac-perf-before-after.mdHow to reproduce: W8-01 r2's run command (
reports/2026-11-14-widen8.md§10.5); the series'ls-run.py prepare/runcommands andcommon switches (
reports/2026-11-15-mac-perf-runs-i39.md§2-§3), withRX_VK_WIDEN8=0for the before runL1pass ends a frame (of themrestart-off 8-bit indices)L38-bit draws MoltenVK splits a frameL2MB loaded and stored a frameMacPerf's estimate for the 57.5 splits (1-3 ms of GPU a frame) is not borne out: across builds, I38 run #32 (without this PR) to I39
E2, the splits fell from 57.5 to 0.0 a frame and the GPU frame from 20.3 to 19.1 ms with all three topics, 1.1 ms of it MeshBatch's
(E4 → E2); different builds, not a same-build A/B.
Flags and settings
Two new switches and one new arm of #704's bench switch;
RX_VK_INDEX8_RAWon by default on MoltenVK only,RX_VK_WIDEN8off.RX_VK_INDEX8_RAW=0or1RX_VK_WIDEN8on or off;0: MoltenVK's values;1on any driver with push descriptorsRX_VK_WIDEN8=0or10: off;1on, on MoltenVK only (with push descriptors;widen8_on)1to try where memory bandwidth is shortRX_VK_BENCH_AB=widen8RX_VK_BENCH(andRX_VK_WIDEN8=0)Testing
Tested: on macOS 26.7.1, Apple M4 Max:
7fd975fb, on #7807c6f0e9cwith #678 and #703 merged):test_widen8(82,412 checks, 0 failures, andthe source checks with the look ahead off by default) and
test_load_clearpass on arm64 and x86_64; on the base30f8f3e0withthis head's tests both fail, both arches (
vk_vendor.hhas nowiden8; nopass_keeps). The other checks this PR and its mergedbase touch pass:
test_draw_binds,test_draw_side_effects,test_draw_stream_visibility,test_drawid_direct,test_fallback_pipeline_state,test_fan_key,test_fsr_uniform_ownership,test_index8_bind,test_mode_census,test_perf_lines,test_provoking_first,test_readback_part,test_recorder_sync,test_recording_admission,test_resolver_coverage,test_seg_gathers,test_vendor_fixes(both arches where they build; source-only ones on arm64).vk_draw.cpp,vk_main.cpp,vk_memory.cpp,vk_progcache.cpp,vk_replay.cppcompiled whole for both arches, 0 warnings(
reports/PubWave-files/PubLate5/PerfTrio/proof/,compile/widen8-7fd975fb/).macos/widen8a0e18543on the integration branch I383f872b0c, whose changed lines are this head'sbut the look ahead's default):
tests/test_widen8.py+tests/widen8_test.cpp: 82,412 checks, 0 failures (every value, restartstate, MoltenVK build and switch; the shader as C++ against both kernels; NVN's reference model over 400 rounds; the pass timeline in
three configurations); 36 of 36 mutants killed;
vk_draw.cpp,vk_main.cpp,vk_replay.cpp,vk_memory.cpp,vk_perf.cppcompiled whole for both arches, 0 warnings; full suite 586 passed, 1 failed (
test_menu_i18n, as on I38), 16 skipped(
reports/2026-11-14-widen8.md§10.4).7a7726fe):test_widen8andtest_load_clear(fix A's checks: one widening a pass, outside any pass, before its begin, held or not), both arches, among 103 of 103 checks; full
suite 588 passed, 1 failed (
test_menu_i18n), 16 skipped.unifdef -U__APPLE__, the base30f8f3e0against this head): every changed file differs, the change being gated by thedriver (
reports/PubWave-files/PubLate5/PerfTrio/offapple-widen8-own.txt).Known gap, being fixed: run #36 on the integration at these defaults (the look ahead off, so the restart-off draws widen at
the draw) left out over 11,000 draws that depend on the provoking vertex, for lack of room in the recording's stream. With the
look ahead on (I39's E-series), the same build family left 16 out. The fix in progress never leaves a draw out for lack of
stream room, so the default here is only settled once it is in the stack and a game run shows 0.
Not tested: a game scene drawing vertex 255 with restart off (the census met none in the field-dawn hold); MoltenVK 1.4.2 in
W8-01 r2 (main 5189f8de + argbuf-slots only); any driver but MoltenVK; not built on Windows or Linux; its test request covers it.
Must be tested:
RX_VK_PERF_LINES=1, expectingthe
vk: 8-bit indices widened by LSline to count the restart-off draws widened at the draw, its census to name any 0xFF met, andL3to count no 8-bit split (by hand: the parent's next runs).7fd975fb, it carries #780, #678, #703 and what they carry) and#780's head the same way, start each to the title, expecting no
vk: 8-bit indicesline, every other line as there, andthe same picture (by hand: test request T-XXXX).
Risks
What could break: on MoltenVK only: restart-off 8-bit draws that use vertex 255 now draw it (pictures change there, to NVN's);
each restart-off 8-bit draw is widened by LS at the draw, its pass ended and begun again for it, as MoltenVK's own widening did (18.8
a frame in the field-dawn hold). With
RX_VK_WIDEN8=1: a compute run and two barriers before each pass that holds 8-bit draws, inthe recording's arena; draws bind LS's 16-bit ranges where
Table::findallows. Other drivers keep a little per-pass bookkeeping onthe CPU and record nothing new.
Turning it off:
RX_VK_INDEX8_RAW=0(MoltenVK's values and its own widening, as before); the look ahead is off by default(
RX_VK_WIDEN8=1turns it on); or revert the PR.Compatibility: Nothing changes for players outside macOS. The compute shader is the renderer's own, not translated code: no file
in the shader stamp changed; no
MKWVK_API_VERSIONchange.Reviewer notes
30f8f3e0..7fd975fbfor this PR's own work (5 commits):cherry-pick -xofmacos/widen82634d098,47f4695b,a0e18543, then Integration 39's fix A11fe233a(its change line for line), then7fd975fb(own commit: the look ahead off bydefault, Integration 40's
2b4b7b8eline for line for these files).7c6f0e9c..30f8f3e0is its base:ffb5dfbamerges #678 (mac/mvk-temp76cdd7a7): this PR's report goes out with #678'sdraw_index8_report, its direct bindreplaces #678's sized bind, its Device fields follow #678's. Conflicts kept both, in Integration 33's order (
vk_vendor.h,vk_main.cpp, the docs rows).dc500d80merges #703 (mac/provoking-first8d552a6c, with #680 and #701):widen8_before_drawis asked only whereProvokingFirst made no list (
!pv.made), andtest_widen8reads draw() up to ProvokingFirst's branch. Conflicts kept both, asIntegration 37 has them (the includes; the 8-bit tally beside FanKey's census; DrawIdDirect,
perf_drawn, ProvokingFirst inthat order; the counted draws' lines, then the perf hook).
30f8f3e0(own commit): #704'stest_perf_linesreads the counted draws with #703'sprovoking_noteand #701's census line, asIntegration 36/37 made it (344388701, eb4d0204), at this tree's five places.
begin_pass's same-targets test kept as bothpass_onandpass_keepsuntil fix A makes them one, as Integration 39 did; theRX_VK_BENCH_ABrow namesload_clearandwiden8; theincludes, the replay's
heldandCursorKeep, the vendor fields and docs rows kept both;test_draw_bindscounts this base's 5 bindstest_readback_partmain e090cc48's 9 in-place reads + Widen8's 1 = 10, and3
note_in_place_read(!mirror_upload);test_resolver_coveragelists Widen8's block alone;tests/fan_indirect_test.cpp's stand-insleft out (#702's fixture, not in this stack).
tests/fan_indirect_test.cpp(W8Bind,widen8_range,last_dyn, as Integration 39 has them); with #705,test_draw_binds8; with #707, both lanes' blocks intest_resolver_coverage'smerged_sites(#783's merge49b8ae6adoes it); with #704 and #703 in main,30f8f3e0istheirs (drop it if main has it).
e090cc48; main has moved since, and a delta sweep follows.Issues
See #420
Checklist
Everyone
type(scope): description) of the status line's type.main. (On maine090cc48through #780, #678 and #703; main has moved since: a delta sweep follows.)python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). (On wave P = #783's head94fca509, which carries this PR, #780 and their bases: 585 passed, 31 failed, 18 skipped against main e090cc48's 544/32/17 on this Mac; the 31 failures are main's own and nothing that passes on main fails;test_collect_widenow passes; 40 new tests pass;reports/PubWave-files/PubLate5/wave/full-P-94fca509/.)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]>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)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]>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)plain7a498e5865plain1e1a3b3e0dKosmicKrisp 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)DI-01 (reports/2026-11-14-drawid-direct.md) ran the direct and one-record indirect forms on MoltenVK 1.4.2: the indirect form was exact in every shape (0 stale of 3,744 draw indices), and the direct plain, indexed and quad draws read stale words, but tessellated direct draws read draw index 0 (64 of 64, with update tags in the pool) while their indirect form took about 135 MB of GPU memory and 3 ms of submit a draw. So 3f1ee3da's choice to keep them direct stands, and the error line it logged for each ("writes a direct draw's draw index to the wrong word") was wrong for them: they are now counted in the vk: direct draws line only, as fans are (0 of 64 stale direct in DI-01). drawid_direct.h and VULKAN.md give DI-01's numbers; the fixture expects an error line only for a first instance without drawIndirectFirstInstance. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit 0a6956568f871c66b08c29dffc0a1b9614dfb5de)The mesh fallback draws one record a corner, in its mesh program's primitive order. On a device without provokingVertexLast, for a program that depends on the provoking vertex, its vertex draw is now vkCmdDrawIndexedIndirect of the same header through a static index list that reads each triangle's corners {2, 0, 1} and each line's {1, 0} (provoking::rotation): the last corner first, the winding kept. The list covers the program's record capacity (MESH_FALLBACK_RECORD_WORDS / fallback_record_words, the epilogue's fit), made once per corner count and larger when needed. The header {count, 1, 0, 0} reads as the indexed record; its fifth word, the allocator, is a first instance nothing reads (the generated vertex stage reads only its vertex index and the pipeline has no vertex input), so it needs drawIndirectFirstInstance, else the draw is drawn as before, logged. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit 1aced9b7e8b5458befea4dfc38df305913332ebc)provoking_list wrote the list into the recording's stream before the pipeline was chosen, ahead of the memory admission. The admission can hand the draw to a new recording (vk_replay.cpp admission_handoff: the old one submitted and waited, a new slot begun), which then bound an index range in the old slot's stream; that stream is rewritten when the old slot is reused, possibly while the new recording still reads it. A draw left out after the decision (no pipeline yet, a segmented variant lost) also counted a list it never drew. - provoking_list decides (the same conditions; room for the list in the stream now, stream_reserve; the game's indices read on the CPU as submitted, held by the list in the thread's lent buffer) and writes nothing. - provoking_write, at the draw (after the admission and the pipeline), allocates the list in the stream of the recording the draw is in and converts into it; the draw binds that buffer. If the stream cannot grow there (device memory), the draw is left out, logged at error level and counted ("left out, no room for the list in the stream at the draw"), since its pipeline is the list's. - listed counts the lists drawn, at the draw. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]> (cherry picked from commit 16216adc33ff7081bb3a0d3b687fbee947cb6519)PerfInstr's full_barrier asks perf_lines::bench_b("dedupe_off") (RX_VK_BENCH_AB's B arm; false unless the bench runs it). main's test_inline_write_order (#329) compiles full_barrier cut out of vk_texture.cpp, and did not compile at this head ("use of undeclared identifier 'perf_lines'"; found by the wave's full suite: the topic's base predates the test). It now includes perf_lines.h, as the topic did for test_dump_readback's fixture; its three variants (the current code and two negative controls) run as before, both compilers. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]>592cd8d3) into the provoking-vertex branch 87c384eb4f76cdd7a7) into the load-clear branch, for Widen8 ffb5dfbac18d552a6c) into the load-clear branch, for Widen8 dc500d80a3Widen8's draw() asks for a restart-off 8-bit draw's widening only where ProvokingFirst made no list for it (`!pv.made`: a converted draw binds its 32-bit list, no 8-bit indices), and test_widen8 reads draw()'s indirect branch up to ProvokingFirst's `} else if (pv.made) {`. #703 sits on #680 + #701 on #679, which this branch (#704 on #679) carries, so the merge brings in #680, #701 and #703. Conflicts, each kept both sides, as Integration 37 (macos/integrated 3f872b0c) has them: - vk_draw.cpp: the includes (drawid_direct.h, provoking.h, fan_key.h, then load_clear.h); #678's 8-bit tally and report, then FanKey's census report; DrawIdDirect's section and report, then PerfInstr's perf_drawn, then ProvokingFirst's section and report (right before draw(), as integrated); at draw()'s five counted draws ProvokingFirst's count line (and DrawIdDirect's record at the direct quads, FanKey's census at the quads and the rest), then PerfInstr's perf hook. - vk_draw.h: the DrawIdDirect, ProvokingFirst and FanKey report declarations, then draw_perf_op. - tests/seg_gathers_test.cpp: drawid_direct.h's include, then perf_lines.h's. The merge's change from the first parent is #703's change from #679, line for line. test_perf_lines' count of the draw sites follows in the next commit. Assisted-by: Claude Opus 5.5 Co-Authored-By: Claude Opus 5.5 <[email protected]>7fd975fb058b3eb471fbFolded into #852 (perf(macos): Mac performance on MoltenVK)
Pull request closed