feat(vk): add a segment planner for guest spans larger than one device allocation #647

Closed
mooki wants to merge 2 commits from mooki/Longshot:mac/seg-plan into main
Member

Type: feat · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (arm64, and x86_64 under Rosetta; no GPU work) · Needs: Windows, Linux (the test only)

Why

The macOS port (#420). On a Mac the renderer reaches guest memory by importing it into Vulkan, and one import cannot be
larger than one device allocation (41,747,087,360 bytes, about 38.9 GiB, on this Mac), while overlapping views of the
game's memory can together span more than that. A
span of guest memory larger than one allocation has to be imported as several segments, and where the cuts fall decides which of
the game's buffers end up across a cut, so that the renderer has to bind them piece by piece or gather them. This PR adds
the planner that decides where to cut: it puts the cuts where they split the fewest and cheapest buffers, keeps earlier
cuts where it can, and adds a segment only when that is cheaper than gathering a large buffer. Nothing calls it yet; the
renderer's segmented imports (#658) do. See #420.

What changed

A header-only planner, gpu/vk/span_segments.h (namespace mkwvk::span_segments, no Vulkan, no platform calls), and its
test. No renderer file changes.

  • planner: plan(L, H, cap, g, demands, previous, weights) returns the cut list of least cost for the domain [L, H):
    every segment at most cap, cuts on multiples of the granularity g. Each demand ({lo, hi, class, may_write}, class
    Gather, Uniform, Texel or Storage, heaviest first) that a cut lands inside costs its class weight once; a gather costs
    its bytes (twice when written back); moving an earlier cut costs move; each segment beyond the fewest costs
    segment. Ties go to fewer segments, then the lowest cuts. When the earlier cuts still fit and split nothing, they come
    back unchanged (the common case at each admission). The search is a DP over candidate cut positions (each demand's end,
    the earlier cuts, and the cap-sized chains down from them); its range of segment counts is derived in the header's
    contract comment.
  • windows() gives each cut's exact feasible range; pieces() and texel_pieces() map a view onto the segments
    (texel views in whole elements, split at cuts and at max_elements, with one-element seams where a cut falls inside an
    element; a written view with a seam is refused). check_cuts() checks a list for the caller's debug invariants.
  • Refusals come back before any callback, with an empty list: bad granularity, a cap below it, an empty domain, a bad
    demand, more than 4096 segments (Vulkan's least maxMemoryAllocationCount), bad cuts, outside the domain, a bad
    element, a written seam. Window arithmetic is exact up to UINT64_MAX; costs are a portable 128-bit type.
  • test: tests/span_segments_test.cpp and tests/test_span_segments.py (run_all.py runs it with --cxx). It compares
    every plan with two independent oracles: a branch-and-bound enumeration of every cut list over the derived range of
    segment counts, and a plain DP over every multiple of g and every count. It sweeps every single demand in 4 KiB steps
    at two caps, runs randomized cases at four caps (each again in shuffled order and as a second admission), checks the
    real 48 GiB and 512 GiB cases, the top of the u64 range and the 4096 limit, and 20,000 random view mappings.

Measurements

Verdict: nothing to measure: the planner has no caller in this PR, and its test (under Testing) passes on both CPUs.

Flags and settings

None.

Testing

Tested: on macOS 26.7.1, Apple M4 Max, Apple clang 21.0.0, at this PR's head before the rebases onto e090cc48 and
aa95d35f (6fd02124), on main 6c9edffe:

  • tests/test_span_segments.py --cxx /usr/bin/clang++ --arch arm64 --arch x86_64: passes on arm64 and on x86_64 under
    Rosetta, with the same counts, every plan equal to both oracles:

    span_segments: ok (683899 plans against brute force, 10859809 cut lists enumerated; 14560 plans with more than the fewest segments; 669306 also against the DP over every count)
    span_segments: built for arm64, sysctl.proc_translated=0
    span_segments: ok (683899 plans against brute force, 10859809 cut lists enumerated; 14560 plans with more than the fewest segments; 669306 also against the DP over every count)
    span_segments: built for x86_64, sysctl.proc_translated=1
    
  • tests/run_all.py span_segments: 1 passed.

  • History, at the topic's own commits (macos/seg-plan 189d0636, the same header, test and driver blobs): the same
    test under ASan and UBSan on both CPUs, and 18 planner mutations, each caught; with only the oracle sections, every
    search mutation is caught by the oracles themselves.

  • No file of the game's build changes: of the 3 changed C/C++ files, all new, the header gpu/vk/span_segments.h is
    included by no source in this PR, and tests/span_segments_test.cpp is compiled only by its test. So no host or renderer
    build changes on any platform, and the host-build box does not apply.

  • After the rebase onto main aa95d35f, at 52def683 (6fd02124 before the rebases onto e090cc48 and aa95d35f):
    Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they
    apply) gave the same results as before; python -I -B tests/run_all.py on wave B's merge db0e2e16, which carries
    this head: 569/32/18 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure
    (reports/2026-11-15-rebase-sweep-3.md).

Not tested: Windows and Linux (no compiler for them here; the header uses no platform call and the test prints its
Rosetta line only under __APPLE__). No game run: nothing calls the planner in this PR.

Must be tested:

  • Windows and Linux, game-free (no GPU needed): on this PR (head 52def683) run python -I -B tests/run_all.py span_segments, expecting 1 passed and the span_segments: ok (683899 plans …) line; the header is in no game build
    yet, so nothing else changes against main aa95d35f (by hand: test request T-EKVM)

Risks

What could break: nothing at run time: no renderer file includes the header in this PR. A compiler that rejects the
header or its test would fail run_all.py only.

Turning it off: revert the PR.

Compatibility: Nothing changes: no shader stamp, cache format or MKWVK_API_VERSION change.

Reviewer notes

  • Review 411e2885..6fd02124 (two commits): the planner, then the priced extra segments (the second commit replaces the
    first one's fixed-count window DP). Start with the contract comment at the top of span_segments.h.
  • Based on main 6c9edffe; no dependency. The two topic commits were cherry-picked with -x from macos/seg-plan with no
    conflict (three new files).
  • The default weights are a policy in bytes-copied equivalents, not measured; they are Weights' defaults, one place to
    change.
  • Used by #658 (alias isolation and segmented imports on macOS), which stacks on this PR.
  • Conflicts with main: none. With the open macOS PRs (#431, #437, #438, #479, #480, #490, #491, #492): none (git merge-tree).

Issues

See #420

Checklist

Everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description) of the status line's type.
  • Why gives the reason plainly and links the issues that fit (the tracker searched).
  • Measurements has a verdict, or one line saying why there is nothing to measure.
  • Testing names what was tested, what wasn't, and what must still be tested, where and how. (Test request T-EKVM.)
  • Rebased on current main: on aa95d35f (#761, 2026-11-15), head 52def683, without a conflict; this PR's own
    commits, old → new: 411e2885 → 601c00cc, 6fd02124 → 52def683; the commit names elsewhere in this description
    are from before the rebase.
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named). (On the wave's merge of every macOS PR built with this one, 453eb678: 553 passed, 32 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the failures are main's own except test_split_ub_flush, which #658's tree at the time broke and its head fixes; test_collect_wide now passes; the new tests pass, test_span_segments among them, and test_darwin_audio_service, Darwin-only, is skipped by the runner; reports/PubWave-files/waveA/. After the rebase onto main aa95d35f (head 52def683): wave B's merge db0e2e16, 569/32/18 against main aa95d35f's 543/33/17, no new failure.)
  • No game files, dumps, build output or generated files.
  • third_party/ untouched (a new folder with a PROVENANCE.md instead).
  • The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled.

When it applies

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
**Type:** feat · **Risk:** low · **Tested on:** macOS 26.7.1, Apple M4 Max (arm64, and x86_64 under Rosetta; no GPU work) · **Needs:** Windows, Linux (the test only) ## Why The macOS port (#420). On a Mac the renderer reaches guest memory by importing it into Vulkan, and one import cannot be larger than one device allocation (41,747,087,360 bytes, about 38.9 GiB, on this Mac), while overlapping views of the game's memory can together span more than that. A span of guest memory larger than one allocation has to be imported as several segments, and where the cuts fall decides which of the game's buffers end up across a cut, so that the renderer has to bind them piece by piece or gather them. This PR adds the planner that decides where to cut: it puts the cuts where they split the fewest and cheapest buffers, keeps earlier cuts where it can, and adds a segment only when that is cheaper than gathering a large buffer. Nothing calls it yet; the renderer's segmented imports (#658) do. See #420. ## What changed A header-only planner, `gpu/vk/span_segments.h` (namespace `mkwvk::span_segments`, no Vulkan, no platform calls), and its test. No renderer file changes. - planner: `plan(L, H, cap, g, demands, previous, weights)` returns the cut list of least cost for the domain `[L, H)`: every segment at most `cap`, cuts on multiples of the granularity `g`. Each demand (`{lo, hi, class, may_write}`, class Gather, Uniform, Texel or Storage, heaviest first) that a cut lands inside costs its class weight once; a gather costs its bytes (twice when written back); moving an earlier cut costs `move`; each segment beyond the fewest costs `segment`. Ties go to fewer segments, then the lowest cuts. When the earlier cuts still fit and split nothing, they come back unchanged (the common case at each admission). The search is a DP over candidate cut positions (each demand's end, the earlier cuts, and the cap-sized chains down from them); its range of segment counts is derived in the header's contract comment. - `windows()` gives each cut's exact feasible range; `pieces()` and `texel_pieces()` map a view onto the segments (texel views in whole elements, split at cuts and at `max_elements`, with one-element seams where a cut falls inside an element; a written view with a seam is refused). `check_cuts()` checks a list for the caller's debug invariants. - Refusals come back before any callback, with an empty list: bad granularity, a cap below it, an empty domain, a bad demand, more than 4096 segments (Vulkan's least `maxMemoryAllocationCount`), bad cuts, outside the domain, a bad element, a written seam. Window arithmetic is exact up to `UINT64_MAX`; costs are a portable 128-bit type. - test: `tests/span_segments_test.cpp` and `tests/test_span_segments.py` (`run_all.py` runs it with `--cxx`). It compares every plan with two independent oracles: a branch-and-bound enumeration of every cut list over the derived range of segment counts, and a plain DP over every multiple of `g` and every count. It sweeps every single demand in 4 KiB steps at two caps, runs randomized cases at four caps (each again in shuffled order and as a second admission), checks the real 48 GiB and 512 GiB cases, the top of the `u64` range and the 4096 limit, and 20,000 random view mappings. ## Measurements **Verdict:** nothing to measure: the planner has no caller in this PR, and its test (under Testing) passes on both CPUs. ### Flags and settings None. ## Testing **Tested:** on macOS 26.7.1, Apple M4 Max, Apple clang 21.0.0, at this PR's head before the rebases onto e090cc48 and aa95d35f (`6fd02124`), on main 6c9edffe: - `tests/test_span_segments.py --cxx /usr/bin/clang++ --arch arm64 --arch x86_64`: passes on arm64 and on x86_64 under Rosetta, with the same counts, every plan equal to both oracles: ``` span_segments: ok (683899 plans against brute force, 10859809 cut lists enumerated; 14560 plans with more than the fewest segments; 669306 also against the DP over every count) span_segments: built for arm64, sysctl.proc_translated=0 span_segments: ok (683899 plans against brute force, 10859809 cut lists enumerated; 14560 plans with more than the fewest segments; 669306 also against the DP over every count) span_segments: built for x86_64, sysctl.proc_translated=1 ``` - `tests/run_all.py span_segments`: 1 passed. - History, at the topic's own commits (`macos/seg-plan` `189d0636`, the same header, test and driver blobs): the same test under ASan and UBSan on both CPUs, and 18 planner mutations, each caught; with only the oracle sections, every search mutation is caught by the oracles themselves. - No file of the game's build changes: of the 3 changed C/C++ files, all new, the header `gpu/vk/span_segments.h` is included by no source in this PR, and `tests/span_segments_test.cpp` is compiled only by its test. So no host or renderer build changes on any platform, and the host-build box does not apply. - **After the rebase onto main aa95d35f**, at `52def683` (`6fd02124` before the rebases onto e090cc48 and aa95d35f): Sweep3's checks of this head (the C/C++ it changes compiled, and its own tests run, on arm64 and x86_64 where they apply) gave the same results as before; `python -I -B tests/run_all.py` on wave B's merge `db0e2e16`, which carries this head: 569/32/18 (passed/failed/skipped) against main aa95d35f's 543/33/17 on this Mac, no new failure (`reports/2026-11-15-rebase-sweep-3.md`). **Not tested:** Windows and Linux (no compiler for them here; the header uses no platform call and the test prints its Rosetta line only under `__APPLE__`). No game run: nothing calls the planner in this PR. **Must be tested:** - Windows and Linux, game-free (no GPU needed): on this PR (head 52def683) run `python -I -B tests/run_all.py span_segments`, expecting `1 passed` and the `span_segments: ok (683899 plans …)` line; the header is in no game build yet, so nothing else changes against main aa95d35f (by hand: test request T-EKVM) ## Risks **What could break:** nothing at run time: no renderer file includes the header in this PR. A compiler that rejects the header or its test would fail `run_all.py` only. **Turning it off:** revert the PR. **Compatibility:** Nothing changes: no shader stamp, cache format or `MKWVK_API_VERSION` change. ## Reviewer notes - Review `411e2885..6fd02124` (two commits): the planner, then the priced extra segments (the second commit replaces the first one's fixed-count window DP). Start with the contract comment at the top of `span_segments.h`. - Based on main 6c9edffe; no dependency. The two topic commits were cherry-picked with `-x` from `macos/seg-plan` with no conflict (three new files). - The default weights are a policy in bytes-copied equivalents, not measured; they are `Weights`' defaults, one place to change. - Used by #658 (alias isolation and segmented imports on macOS), which stacks on this PR. - Conflicts with main: none. With the open macOS PRs (#431, #437, #438, #479, #480, #490, #491, #492): none (`git merge-tree`). ## Issues See #420 ## Checklist ### Everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`) of the status line's type. - [x] Why gives the reason plainly and links the issues that fit (the tracker searched). - [x] Measurements has a verdict, or one line saying why there is nothing to measure. - [x] Testing names what was tested, what wasn't, and what must still be tested, where and how. (Test request T-EKVM.) - [x] Rebased on current `main`: on aa95d35f (#761, 2026-11-15), head `52def683`, without a conflict; this PR's own commits, old → new: `411e2885` → `601c00cc`, `6fd02124` → `52def683`; the commit names elsewhere in this description are from before the rebase. - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). (On the wave's merge of every macOS PR built with this one, `453eb678`: 553 passed, 32 failed, 18 skipped against main 6c9edffe's 531/32/17 on this Mac; the failures are main's own except `test_split_ub_flush`, which #658's tree at the time broke and its head fixes; `test_collect_wide` now passes; the new tests pass, `test_span_segments` among them, and `test_darwin_audio_service`, Darwin-only, is skipped by the runner; `reports/PubWave-files/waveA/`. After the rebase onto main aa95d35f (head `52def683`): wave B's merge `db0e2e16`, 569/32/18 against main aa95d35f's 543/33/17, no new failure.) - [x] No game files, dumps, build output or generated files. - [x] `third_party/` untouched (a new folder with a `PROVENANCE.md` instead). - [x] The license is OK: GPL-3.0-or-later, nothing incompatible and nothing decompiled. ### When it applies - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers.
A connected guest span that the views of one operation need can be larger than
one device allocation (41,747,087,360 bytes on MoltenVK 1.4.2, M4 Max). It is
then imported as disjoint, adjacent segments of at most the cap.
gpu/vk/span_segments.h (rules only, no Vulkan) chooses the cuts: multiples of
the import granularity, the fewest segments, the least cost of the views a cut
splits (gathers by their bytes, then uniform, texel and storage buffers), a
cost for moving a previous cut, and the lowest addresses on ties. It maps any
guest range onto the segments, and splits a texel view at cuts and at
maxTexelBufferElements, reporting 12-byte seams. Invalid input gets an
explicit refusal. Nothing uses it yet: vk_memory.cpp takes it later.

tests/test_span_segments.py checks it against brute force at caps of 48 KiB,
64 KiB, 96 KiB and 1 MiB with 16 KiB granularity, plus the 48 GiB example, the
512 GiB guest window and the top of the u64 range (--arch arm64 / x86_64 on
macOS).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 11e53b66ffbdddd0cfeaa7a043361a337299d855)
feat(vk): let the segment planner add segments that pay for themselves
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 5s
6fd02124bc
With the fewest segments, a large gathered view can be cut on every draw
although one more segment would keep it whole: a 32 GiB index extent at
[8, 40) GiB in a 48 GiB domain at the 41,747,087,360-byte cap is split by
every two-segment cut list. plan() now weighs any number of segments, each
one beyond the fewest at Weights::segment (2^32 bytes-equivalent, above a
move), and returns the least cost, then the fewest segments, then the lowest
cuts. Every cut of that plan is a mark (the first multiple of g at or above a
demand's end, or a previous cut) or ceil(next cut - cap), so the search runs
over those candidates and is exact over [k_min, k_min + min(marks,
C0 / segment)]; a least plan over 4096 segments is refused. Plan::extra
reports the added segments.

The test brute-forces the same range of segment counts, checks a DP over
every count, and adds the 32 GiB extent (three segments, nothing gathered).

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 189d0636cb5e90e0d98de397d0b9676a5f8354c2)
mooki force-pushed mac/seg-plan from 6fd02124bc
All checks were successful
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 3s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Third-party parts / As their notes say (pull_request) Successful in 9s
PR description / Description follows the template (pull_request_target) Successful in 5s
to aae09e20f7
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 17s
CI / Conventional Commit title (pull_request) Successful in 16s
CI / Workflow lint (actionlint) (pull_request) Successful in 24s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 20s
Third-party parts / As their notes say (pull_request) Successful in 33s
2026-11-15 07:16:31 +00:00
Compare
mooki force-pushed mac/seg-plan from aae09e20f7
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 17s
CI / Conventional Commit title (pull_request) Successful in 16s
CI / Workflow lint (actionlint) (pull_request) Successful in 24s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 20s
Third-party parts / As their notes say (pull_request) Successful in 33s
to 52def6832c
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 10s
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s
2026-11-15 13:09:58 +00:00
Compare
mooki changed title from WIP: feat(vk): add a segment planner for guest spans larger than one device allocation to feat(vk): add a segment planner for guest spans larger than one device allocation 2026-11-15 14:13:51 +00:00
Author
Member

Folded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)

Folded into #848 (feat(vk): the SPIR-V back end on MoltenVK and guest spans past one device allocation)
mooki closed this pull request 2026-11-15 18:07:51 +00:00
Some checks failed
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been skipped
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been skipped
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been skipped
Third-party parts / The same as upstream (pull_request) Has been skipped
Website / Website on Cloudflare (pull_request) Has been skipped
CI / Conventional Commit title (pull_request) Successful in 4s
CI / Workflow lint (actionlint) (pull_request) Successful in 6s
Guide screenshots / Guide images and local UI source review (pull_request) Failing after 6s
Third-party parts / As their notes say (pull_request) Successful in 10s
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
MagicBeanDev/Longshot!647
No description provided.