WIP: perf(vk): a mirror sync's pages from the captures go in one compute dispatch, not one copy region each #326

Draft
proci wants to merge 1 commit from proci/Longshot:perf/mirror-sync-pass into main
Contributor

Why

A mirror sync copies the pages a capture holds into the mirror as copy regions. In Hyrule Field that is about 1,300 a frame, a page or a few each, and the GPU reads them from host memory over PCIe one after another. At the spot below they took 1.12 ms of GPU time a frame on main. This sends them through the write-back diff's compute pass instead, one dispatch a sync, and there they took 0.33 ms.

This is the change merged in Hookshot as NotRexed/Hookshot#3023 (2026-11-13), ported to Longshot and measured again here. None of the restore PRs carries it.

What changed

  • vk: mirror_sync (gpu/vk/vk_memory.cpp) no longer records a capture buffer's runs of pages as vkCmdCopyBuffer regions. msync_pass_add turns them into the write-back diff's chunks, and msync_pass_flush records them as one dispatch in the upload command buffer, where the copies were.
    • Why the pass is a copy. shaders/wb_diff.comp is a copy when its shadow is its destination: it reads the source, compares it with the destination and writes it there where they differ. So each chunk is {mirror = the capture's page, shadow = guest = the mirror's}.
    • How runs are split. As the write-back splits them (wbd_split): 16-byte vectors of at most 64 KB where source and destination are alike within 16 bytes, words otherwise.
  • vk: memory_sync_mirrors_before_segments (the parallel replay's presync) holds every pool's chunks until its loop is done, and dispatches them once.
  • vk: no barrier between the chunks, as there was none between the copies: in a sync each page of a mirror comes from one place. The full barrier that ends the uploads (texture_uploads_recorded) waits for the dispatch as it did for the copies.
  • vk: the chunk tables live in 16 blocks of 256 KB of host-visible memory. A block is taken again once the submission that last read it is done. They are not the write-back's own ring (wbd_table_get), whose tables are 128 KB or more a dispatch.
    • No room. A sync whose chunks find no room goes as copies, as before, and the log says so (vk: mirror syncs' pass: no table room). It never showed in the runs below.
  • vk: while the pass is on, a capture buffer is made with STORAGE_BUFFER and SHADER_DEVICE_ADDRESS too. That is cap_usage(), which the late pages' fix (#248) added for the same bits: it now gives them when either is on (used by cap_spare_make and cap_buf_get).
  • vk: the runs the pass takes still go into the pool's write-back shadow (shadow_sync_copy, from #248), as the copies' do. The pass writes the mirror only.
  • vk: the pass sets no stamps: its chunks' vecs is 0 or 1, and the push constant the write-back uses for the stamp's value is 0.
  • vk: the sync records the copies, as before, in two cases:
    • where the write-back diff's pipeline is not made (wbd_pipe_ready(): no subgroup votes in compute);
    • where a run is not in whole words.
  • vk: every 600 submissions the log says how the pass went: vk: mirror syncs' pass (600 submits): N dispatches, N chunks; N syncs as copies (no table room).
  • tests: tests/test_msync_pass.py (new, game-free).
  • docs: RX_VK_MSYNC_PASS=0 in docs/development.md, after RX_VK_WB_DIFF=0 and #248's two switches; one file in docs/tested/ for each of the ten runs.

Cost:

  • Dispatches. At the spot below: 1.00 dispatch a frame with RX_VK_PARALLEL=4 (the presync's, 1,290-1,350 chunks), 29 with the serial replay.
  • Memory. 4 MB of host-visible memory for the tables, made as they are first needed.
  • CPU. The chunks are built where the regions were. The replay's memory sync time is the same within the runs' spread (table below).

Flags and settings

Flag What it does Default now Recommendation Helps on Doesn't help on Untested on
RX_VK_MSYNC_PASS=0 The syncs' pages from the captures go as copy regions again, as before unset (the pass on) On by default: it takes about a third of the copies' GPU time, with no fps change and no fallback in the runs below GPU time of the upload command buffers (here 1.56 -> 0.80 ms a frame), so a GPU-bound setup could gain A CPU-bound spot like the one below: fps the same AMD, Intel, Linux, Steam Deck, a GPU-bound setup

Read once by the switches' rule (env_value_off) and logged once at the start: vk: mirror syncs' pages from the captures by a compute pass (RX_VK_MSYNC_PASS), or ... as copy regions, as before. The flag starts with none of translator_stamp's prefixes, so setting it keeps the program cache. It is in docs/development.md's switch table, after RX_VK_WB_DIFF=0 and #248's two switches. The README lists the switches players use, so it is not there.

Measurements

main: e51e28e9 (2026-11-13)
this PR: perf/mirror-sync-pass @ d49ffb35 (this change on e51e28e9), rebased since onto main 76699aad as e089b486
Hardware: RTX 5080 16 GB, driver 610.88; Ryzen 9 7900X3D (SMT on), 64 GB; Windows 11 Pro 26200, fullscreen
Scene: Hyrule Field, the same save every run, standing still with the camera untouched. 2560x1440, DLAA, frame generation off, frame rate cap Uncapped, Reflex off, VRAM budget 11 GB; RX_VK_PARALLEL=4, HOS_PIN=*=FFF.
Method: each run loads the save, stands still for 2 minutes or more, then opens the pause menu and closes the game from it.

  • The build. The game folder was built once from e51e28e9 (longshot e51e28e9 (source 8dd02aab)). Each side's longshot_vk.dll was built alone from its commit (RX_VK_ONLY, release, llvm-mingw). The main one is the same file as the full build's.
  • The window. One fixed rule for every run: from the first frame line after the load whose draws and dispatches match the next ten's, the first 30 of longshot_log.txt's frame lines (vk: N presents, X fps, every 120 presents), about 53 s.
  • Fps, 1% low and frame times. Four runs interleaved (main, this PR, main, this PR) with RX_VK_FRAME_LOG, its presents matched to the frame lines over the same window. The 1% low is 1000 over the mean of the slowest 1% of frame times.
  • The number the change targets. The syncs' GPU time, from a pair with the GPU profile on (RX_VK_PASS_TIMING=1), main then this PR. These are the medians of the profile's reports (one every 600 presents) in the same window.
    • The profile changes the run. It turns the parallel replay off (gprof_on() in vk_replay.cpp's segment split), so this pair ran the serial replay. Its timestamps also order every span. Those runs' fps (64 instead of 68) are not used.
  • An earlier set. Four runs interleaved without frame times, fps only, gave the same: main 68.5 / 68.5, this PR 67.5 / 68.8 (frame-line medians).

How to reproduce: the settings above, a save standing in Hyrule Field, 2 minutes still. Read the frame lines and the replay time lines. Then a pair with RX_VK_PASS_TIMING=1, reading its msync copies (CPU pages from a capture -> mirror) (on main) or msync pass (CPU pages from a capture -> mirror, one dispatch) span.

main @ e51e28e9 (run 1 / run 2) this PR @ d49ffb35 (run 1 / run 2) Δ (medians)
fps average 68.84 / 66.10 (median 67.47) 67.99 / 67.51 (median 67.75) +0.28
1% low fps 53.85 / 50.85 (52.35) 52.50 / 51.87 (52.19) -0.16
median frame time 14.46 / 15.10 ms (14.78) 14.60 / 14.67 ms (14.64) -0.14 ms
p99 frame time 17.70 / 18.33 ms (18.02) 18.16 / 18.20 ms (18.18) +0.16 ms
replay memory sync, ms a frame 0.66 / 1.06 0.68 / 0.64
the syncs' pages from the captures, GPU ms a frame (profile pair, serial replay, medians of 5 and 6 reports) 1.117 (1.098-1.156) 0.328 (0.266-0.339) -0.79 ms (-71%)
their spans a frame (profile pair): copy batches, or dispatches 53.5 29.0
regions, or chunks, a frame; MB a frame (profile pair) 1,323; 8.76 1,331; 8.66
the upload command buffers, GPU ms a frame (profile pair) 1.56 0.80 -0.76 ms
all the profiled GPU work, ms a frame (profile pair) 9.48 8.84 -0.64 ms
the pass's dispatches a frame with RX_VK_PARALLEL=4 (frame-time runs, from its (600 submits) lines) 1.00 / 1.00 (1,288 / 1,350 chunks)
syncs that went as copies (no table room), all runs 0
  • Fps. No fps change at this spot. The two main runs differ more (2.7 fps) than the two sides' medians (0.3).
  • Why. The spot is bound by the replay on the CPU (submit about 6.3 ms of a 14.5 ms frame, the GPU's recorded work 1.8 ms), not by the GPU, so the GPU time the pass saves does not show as frame rate here.
  • The unprofiled GPU time line. It counts the recordings' command buffers only, not the upload ones where the syncs are. So it reads 1.8 ms a frame on both sides.

main @ e51e28e9, frame-time run 1 (a frame line in the window and the lines before it):

[hos Presentation Thread] vk: a frame's replay time (ms): submit 6.48, present 0.09, draw 8.76, descriptors 2.71, texture lookups 0.12, uploads 0.01, pipelines 0.34, programs 1.20, commands 3.54, dispatch 0.78, memory sync 0.98, queue submit 0.21, slot wait 0.<...>
[hos Presentation Thread] vk: a frame: mirrors 67 (1468 MB): 8.69 MB synced in 1302.8 copies, 62.23 MB written back
[hos Presentation Thread] vk: 9720 presents, 69.7 fps; a frame: 3055 draws (0 skipped), 143 dispatches, 161 passes, 160 barriers, 6 submits, 0.2 uploads (0.0 MB), 86 ub updates (59 overlays), GPU-bound wait 0.0 ms; <...>

this PR @ d49ffb35, frame-time run 1:

[hos sead::MainThread] vk: mirror syncs' pages from the captures by a compute pass (RX_VK_MSYNC_PASS)
[hos Presentation Thread] vk: a frame's replay time (ms): submit 6.57, present 0.08, draw 9.22, descriptors 2.85, texture lookups 0.15, uploads 0.01, pipelines 0.36, programs 1.24, commands 3.70, dispatch 0.80, memory sync 0.90, queue submit 0.21, slot wait 0.<...>
[hos Presentation Thread] vk: a frame: mirrors 69 (1428 MB): 8.38 MB synced in 1260.1 copies, 62.15 MB written back
[hos Presentation Thread] vk: 8880 presents, 68.4 fps; a frame: 3047 draws (0 skipped), 140 dispatches, 161 passes, 161 barriers, 6 submits, 0.2 uploads (0.0 MB), 86 ub updates (59 overlays), GPU-bound wait 0.0 ms; <...>
[hos Presentation Thread] vk: mirror syncs' pass (600 submits): 252 dispatches, 1344 chunks; 0 syncs as copies (no table room)

The GPU profile, one report each (main, then this PR):

[hos Presentation Thread] vk:   9.43 ms a frame in all (recordings' command buffers + upload command buffers; <...>)
[hos Presentation Thread] vk:   upload command buffers: 1.55 ms in 33.8 a frame (1.54 ms in spans, 0.01 ms between them: texture uploads and decodes, the page table, glue)
[hos Presentation Thread] vk:   u   1.108 ms    52.3 x     21.2 us    1312.2 items    8.71 MB  msync copies (CPU pages from a capture -> mirror)
[hos Presentation Thread] vk:   8.79 ms a frame in all (recordings' command buffers + upload command buffers; <...>)
[hos Presentation Thread] vk:   upload command buffers: 0.79 ms in 34.8 a frame (0.78 ms in spans, 0.01 ms between them: texture uploads and decodes, the page table, glue)
[hos Presentation Thread] vk:   u   0.329 ms    29.0 x     11.3 us    1326.2 items    8.66 MB  msync pass (CPU pages from a capture -> mirror, one dispatch)

On Hookshot the same change measured the same way gave 1.13 -> 0.34 ms with the serial replay (NotRexed/Hookshot#3023). With RX_VK_PARALLEL=4 the pass is one dispatch a frame instead of 29. The unprofiled runs can't time it, so its GPU time there is not measured.

After the rebase. The runs are on e51e28e9. The branch is now on main 76699aad, which has the late pages' fix (#248); the conflict with it is resolved (Reviewer notes).

  • What is the same. The change's own lines in vk_memory.cpp are as they were, plus the two #248 needed: one usage function, and the shadow copy for the runs the pass takes.
  • What changed under the runs.
    • #248's fix is off when RX_VK_PARALLEL is above 1, and all ten runs set it to 4 (the profile pair too, though the profile ran the serial replay). So with these settings it stays off. Without the switch, the default, it is on with this pass, and that has not run in the game.
    • #216 changes how a capture walks its ranges (CPU side). #237 changes the mirror budget on small cards; on this 16 GB card Auto's stays 3 GB. #324 changes how the host commits and reports memory.
  • Not measured on this head. With these settings #248 stays off, so the syncs' GPU time should hold, but this head has not run in the game. I can run the A/B again on it if wanted.

Testing

Tested:

  • Game-free: tests/test_msync_pass.py (new) cuts msync_pass_on, wbd_split, WbdChunk, MsyncCopy and msync_pass_add out of vk_memory.cpp, as they are. It compiles them with -std=c++20 -Wall -Wextra -Werror against a fake pool, fake buffers and their addresses. It checks:

    • that applying the chunks, as wb_diff.comp would, gives the regions' bytes exactly. This covers sources at +0, +4, +8 and +12 bytes from the destination's 16-byte alignment, partial pages, long runs and short ones;
    • that every chunk is whole: at most 64 KB, shadow == guest, and a vector chunk 16-byte aligned at both ends;
    • that nothing is taken (the caller copies) in these cases: a run not in whole words, a run of size 0, no runs, no pipeline, a buffer without an address;
    • the switch, in a child process: unset is on; 0, false, OFF, No are off;
    • that the capture buffers are made with cap_usage(), which gives them storage and an address when the pass is on;
    • that the runs the pass takes still go into the write-back shadow (shadow_sync_copy);
    • that msync_pass_flush's push constants are the write-back's, and that the shader's Chunk fields are WbdChunk's, in order.

    Each of the three new checks for #248 (the usage function, its storage and address bits, the shadow copy) fails when its rule is undone in a copy of vk_memory.cpp.

    With main's vk_memory.cpp it fails at the first cut (the pass is not there); with this change it passes (llvm-mingw clang 23.1.5, Windows 11).

  • run_all: python -I -B tests/run_all.py --jobs 8 on Windows 11 (llvm-mingw clang 23.1.5).

    • On this branch at e089b486: 517 passed, 4 failed, 34 skipped.
    • On main 76699aad: 516 passed, the same 4 failed (test_finish_output, test_oot_state_tap, test_pad_layout, test_tested_configurations), 34 skipped.
    • test_tested_configurations on both: only its check of the CLI through an ASCII pipe fails, by \r\n against \n on Windows. The check that validates the repository's run files passes, this PR's ten included.
    • The checks that read vk_memory.cpp. 42 checks under tests/ and nx2/ read it besides this PR's, #248's three among them, and all pass unchanged on the branch. Three C++ fixtures read it too, and the checks that compile them pass (test_mirror_inherit.py, test_recorder_sync.py, test_retired_give_back.py). None needed changing.
  • Renderer: longshot_vk.dll built from e089b486 (RX_VK_ONLY, release, llvm-mingw): 0 failed, no warnings.

  • In the game: the ten runs in Measurements, on e51e28e9, with RX_VK_PARALLEL=4 (the profile pair serial). I played them and closed each from the pause menu.

    • With the pass: the log's start line said so, 0 syncs went as copies, and there was no validation, device-lost or VK_ERROR line.
    • On screen: I saw nothing wrong in any of them, the HUD's text included.

Not tested:

  • This head in the game. Above all the default serial replay, where #248's fix is on with this pass. The two meet in mirror_sync (the fix's dispatch, then the runs taken) and in the shadow copy; the test checks those from the text only.
  • Other hardware: AMD and Intel GPUs, Linux and the Steam Deck. The pass is the write-back diff's, which already writes host memory by device address on every GPU that makes its pipeline; this use of it reads host memory the same way, but it ran on one NVIDIA card only.
  • The serial replay's frame rate on Longshot. The profile pair ran it, but the profile costs frame rate. On Hookshot the fps were measured with both replays.
  • The pass's GPU time with RX_VK_PARALLEL=4. The profile turns that replay off.
  • A GPU-bound setup, where GPU time saved would show as frame rate.
  • GCC.
  • A Linux build. I built on Windows only (llvm-mingw). The change is Vulkan and C++ in gpu/vk/vk_memory.cpp, with no OS call and no path.

Needs other testers:

  • AMD GPU, Windows: Hyrule Field standing still, with and without RX_VK_MSYNC_PASS=0. Expect the log's mirror syncs' pass lines with 0 syncs as copies, nothing wrong on screen, and with RX_VK_PASS_TIMING=1 the msync pass span below the msync copies one (request to open).
  • Linux or Steam Deck: build the branch, then the same, plus fps. A GPU-bound handheld is where the saving could show as frame rate (request to open).

Screenshots

Nothing on screen: the change moves where the GPU copies the same bytes.

Risk and rollback

  • What could break. A sync's pages could come out wrong if a chunk table were reused while the GPU still read it, or on a GPU whose subgroup votes behave differently. That would show as wrong textures or geometry after a sync.
  • With #248. If the runs the pass takes missed the shadow copy, the write-back diff would put a capture's older words back over the CPU's newer ones, the flashes #248 describes. The test fails if that copy goes.
  • Mitigations. The tables are taken again only after their submission is done, and a GPU without the pipeline copies as before.
  • Turning it off. RX_VK_MSYNC_PASS=0 turns it off, or the commit can be reverted alone.

Reviewer notes

  • Where to look. msync_pass_add / msync_pass_flush and their call in mirror_sync (gpu/vk/vk_memory.cpp), and the presync in memory_sync_mirrors_before_segments.
  • The design rule. Rule 2 of docs/design.md, section 4, is "Nothing the replay reads comes from live guest memory". The pass reads only the capture buffers, as the copies did. The log's check for it, N from guest memory as it was at the replay in the mirror syncs (600 submits) lines, is 0 in every run's window on both sides. Non-zero ones show only at the save's load and at the close, on both sides alike.
  • #248, the late pages' fix, merged first. It conflicted with this one in gpu/vk/vk_memory.cpp, and the rebase resolves it:
    • One usage function. Both gave a capture buffer storage and an address, #248 with cap_usage(), this PR with cap_buf_usage(). This PR's is gone: cap_usage() gives the two bits when the fix or the pass is on.
    • The shadow copy. #248 copies a sync's runs into the pool's write-back shadow too (shadow_sync_copy), after the mirror's copy. The runs the pass takes get that copy as well, recorded where the mirror's copy was. Merged as git left it, they skipped it.
    • The order. mirror_sync first runs late_fix_emit, whose full_barrier after the fix's dispatch covers all reads, shader reads included. The pass's dispatch comes after that, at the end of mirror_sync or after the presync's loop, so it reads the fixed words. The fix's own barrier before its dispatch, which orders the shadow copies before it reads them, is unchanged.
    • The shader. #248 gave wb_diff.comp stamps: bit 1 of a chunk's vecs, and the push constant that was padding. The pass sets neither.
    • docs/development.md. This PR's row now comes after #248's two.
  • The other open PRs. I test-merged every open PR that changes vk_memory.cpp or docs/development.md (56) with this branch and with main. Each merges with this branch as it does with main: the same files conflict, or none. None conflicts because of this one. The runs are files in docs/tested/ now, so no table conflict either.

Issues

No Longshot issue found for the mirror syncs' copies (searched 2026-11-13).

Checklist

For everyone

  • One topic.
  • The title is a Conventional Commit (type(scope): description).
  • Rebased on current main.
  • python -I -B tests/run_all.py run and passing (any failure that also fails on main is named above).
  • Tested in the game, or what wasn't is named under Testing.
  • Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests).
  • Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • 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.

If applicable

  • AI assistance recorded with Assisted-by: / Co-Authored-By: trailers.
  • Flags noted, and the switch reference updated (docs/development.md, the README).
  • A file in docs/tested/ for each new measured run (format in docs/tested-configurations.md; no appended row or assembled view).
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. Built on Windows only so far; the Linux build is in Needs other testers.
  • Docs updated.

Written with Claude Code (Claude Opus 5.5). I played the in-game runs. The commit carries a Co-Authored-By trailer.

🤖 Generated with Claude Code

## Why A mirror sync copies the pages a capture holds into the mirror as copy regions. In Hyrule Field that is about 1,300 a frame, a page or a few each, and the GPU reads them from host memory over PCIe one after another. At the spot below they took 1.12 ms of GPU time a frame on `main`. This sends them through the write-back diff's compute pass instead, one dispatch a sync, and there they took 0.33 ms. This is the change merged in Hookshot as [NotRexed/Hookshot#3023](https://github.com/NotRexed/Hookshot/pull/3023) (2026-11-13), ported to Longshot and measured again here. None of the restore PRs carries it. ## What changed - vk: `mirror_sync` (gpu/vk/vk_memory.cpp) no longer records a capture buffer's runs of pages as `vkCmdCopyBuffer` regions. `msync_pass_add` turns them into the write-back diff's chunks, and `msync_pass_flush` records them as one dispatch in the upload command buffer, where the copies were. - **Why the pass is a copy.** `shaders/wb_diff.comp` is a copy when its shadow is its destination: it reads the source, compares it with the destination and writes it there where they differ. So each chunk is `{mirror = the capture's page, shadow = guest = the mirror's}`. - **How runs are split.** As the write-back splits them (`wbd_split`): 16-byte vectors of at most 64 KB where source and destination are alike within 16 bytes, words otherwise. - vk: `memory_sync_mirrors_before_segments` (the parallel replay's presync) holds every pool's chunks until its loop is done, and dispatches them once. - vk: no barrier between the chunks, as there was none between the copies: in a sync each page of a mirror comes from one place. The full barrier that ends the uploads (`texture_uploads_recorded`) waits for the dispatch as it did for the copies. - vk: the chunk tables live in 16 blocks of 256 KB of host-visible memory. A block is taken again once the submission that last read it is done. They are not the write-back's own ring (`wbd_table_get`), whose tables are 128 KB or more a dispatch. - **No room.** A sync whose chunks find no room goes as copies, as before, and the log says so (`vk: mirror syncs' pass: no table room`). It never showed in the runs below. - vk: while the pass is on, a capture buffer is made with `STORAGE_BUFFER` and `SHADER_DEVICE_ADDRESS` too. That is `cap_usage()`, which the late pages' fix (#248) added for the same bits: it now gives them when either is on (used by `cap_spare_make` and `cap_buf_get`). - vk: the runs the pass takes still go into the pool's write-back shadow (`shadow_sync_copy`, from #248), as the copies' do. The pass writes the mirror only. - vk: the pass sets no stamps: its chunks' `vecs` is 0 or 1, and the push constant the write-back uses for the stamp's value is 0. - vk: the sync records the copies, as before, in two cases: - where the write-back diff's pipeline is not made (`wbd_pipe_ready()`: no subgroup votes in compute); - where a run is not in whole words. - vk: every 600 submissions the log says how the pass went: `vk: mirror syncs' pass (600 submits): N dispatches, N chunks; N syncs as copies (no table room)`. - tests: `tests/test_msync_pass.py` (new, game-free). - docs: `RX_VK_MSYNC_PASS=0` in docs/development.md, after `RX_VK_WB_DIFF=0` and #248's two switches; one file in docs/tested/ for each of the ten runs. Cost: - **Dispatches.** At the spot below: 1.00 dispatch a frame with `RX_VK_PARALLEL=4` (the presync's, 1,290-1,350 chunks), 29 with the serial replay. - **Memory.** 4 MB of host-visible memory for the tables, made as they are first needed. - **CPU.** The chunks are built where the regions were. The replay's `memory sync` time is the same within the runs' spread (table below). ## Flags and settings | Flag | What it does | Default now | Recommendation | Helps on | Doesn't help on | Untested on | |---|---|---|---|---|---|---| | `RX_VK_MSYNC_PASS=0` | The syncs' pages from the captures go as copy regions again, as before | unset (the pass on) | On by default: it takes about a third of the copies' GPU time, with no fps change and no fallback in the runs below | GPU time of the upload command buffers (here 1.56 -> 0.80 ms a frame), so a GPU-bound setup could gain | A CPU-bound spot like the one below: fps the same | AMD, Intel, Linux, Steam Deck, a GPU-bound setup | Read once by the switches' rule (`env_value_off`) and logged once at the start: `vk: mirror syncs' pages from the captures by a compute pass (RX_VK_MSYNC_PASS)`, or `... as copy regions, as before`. The flag starts with none of `translator_stamp`'s prefixes, so setting it keeps the program cache. It is in docs/development.md's switch table, after `RX_VK_WB_DIFF=0` and #248's two switches. The README lists the switches players use, so it is not there. ## Measurements main: e51e28e9 (2026-11-13) this PR: perf/mirror-sync-pass @ d49ffb35 (this change on e51e28e9), rebased since onto main 76699aad as e089b486 Hardware: RTX 5080 16 GB, driver 610.88; Ryzen 9 7900X3D (SMT on), 64 GB; Windows 11 Pro 26200, fullscreen Scene: Hyrule Field, the same save every run, standing still with the camera untouched. 2560x1440, DLAA, frame generation off, frame rate cap Uncapped, Reflex off, VRAM budget 11 GB; `RX_VK_PARALLEL=4`, `HOS_PIN=*=FFF`. Method: each run loads the save, stands still for 2 minutes or more, then opens the pause menu and closes the game from it. - **The build.** The game folder was built once from e51e28e9 (`longshot e51e28e9 (source 8dd02aab)`). Each side's `longshot_vk.dll` was built alone from its commit (`RX_VK_ONLY`, release, llvm-mingw). The main one is the same file as the full build's. - **The window.** One fixed rule for every run: from the first frame line after the load whose draws and dispatches match the next ten's, the first 30 of `longshot_log.txt`'s frame lines (`vk: N presents, X fps`, every 120 presents), about 53 s. - **Fps, 1% low and frame times.** Four runs interleaved (main, this PR, main, this PR) with `RX_VK_FRAME_LOG`, its presents matched to the frame lines over the same window. The 1% low is 1000 over the mean of the slowest 1% of frame times. - **The number the change targets.** The syncs' GPU time, from a pair with the GPU profile on (`RX_VK_PASS_TIMING=1`), main then this PR. These are the medians of the profile's reports (one every 600 presents) in the same window. - **The profile changes the run.** It turns the parallel replay off (`gprof_on()` in vk_replay.cpp's segment split), so this pair ran the serial replay. Its timestamps also order every span. Those runs' fps (64 instead of 68) are not used. - **An earlier set.** Four runs interleaved without frame times, fps only, gave the same: main 68.5 / 68.5, this PR 67.5 / 68.8 (frame-line medians). How to reproduce: the settings above, a save standing in Hyrule Field, 2 minutes still. Read the frame lines and the `replay time` lines. Then a pair with `RX_VK_PASS_TIMING=1`, reading its `msync copies (CPU pages from a capture -> mirror)` (on main) or `msync pass (CPU pages from a capture -> mirror, one dispatch)` span. | | main @ e51e28e9 (run 1 / run 2) | this PR @ d49ffb35 (run 1 / run 2) | Δ (medians) | |---|---|---|---| | fps average | 68.84 / 66.10 (median 67.47) | 67.99 / 67.51 (median 67.75) | +0.28 | | 1% low fps | 53.85 / 50.85 (52.35) | 52.50 / 51.87 (52.19) | -0.16 | | median frame time | 14.46 / 15.10 ms (14.78) | 14.60 / 14.67 ms (14.64) | -0.14 ms | | p99 frame time | 17.70 / 18.33 ms (18.02) | 18.16 / 18.20 ms (18.18) | +0.16 ms | | replay `memory sync`, ms a frame | 0.66 / 1.06 | 0.68 / 0.64 | | | **the syncs' pages from the captures, GPU ms a frame** (profile pair, serial replay, medians of 5 and 6 reports) | **1.117** (1.098-1.156) | **0.328** (0.266-0.339) | **-0.79 ms (-71%)** | | their spans a frame (profile pair): copy batches, or dispatches | 53.5 | 29.0 | | | regions, or chunks, a frame; MB a frame (profile pair) | 1,323; 8.76 | 1,331; 8.66 | | | the upload command buffers, GPU ms a frame (profile pair) | 1.56 | 0.80 | -0.76 ms | | all the profiled GPU work, ms a frame (profile pair) | 9.48 | 8.84 | -0.64 ms | | the pass's dispatches a frame with `RX_VK_PARALLEL=4` (frame-time runs, from its `(600 submits)` lines) | | 1.00 / 1.00 (1,288 / 1,350 chunks) | | | syncs that went as copies (no table room), all runs | | 0 | | - **Fps.** No fps change at this spot. The two `main` runs differ more (2.7 fps) than the two sides' medians (0.3). - **Why.** The spot is bound by the replay on the CPU (`submit` about 6.3 ms of a 14.5 ms frame, the GPU's recorded work 1.8 ms), not by the GPU, so the GPU time the pass saves does not show as frame rate here. - **The unprofiled `GPU time` line.** It counts the recordings' command buffers only, not the upload ones where the syncs are. So it reads 1.8 ms a frame on both sides. main @ e51e28e9, frame-time run 1 (a frame line in the window and the lines before it): ``` [hos Presentation Thread] vk: a frame's replay time (ms): submit 6.48, present 0.09, draw 8.76, descriptors 2.71, texture lookups 0.12, uploads 0.01, pipelines 0.34, programs 1.20, commands 3.54, dispatch 0.78, memory sync 0.98, queue submit 0.21, slot wait 0.<...> [hos Presentation Thread] vk: a frame: mirrors 67 (1468 MB): 8.69 MB synced in 1302.8 copies, 62.23 MB written back [hos Presentation Thread] vk: 9720 presents, 69.7 fps; a frame: 3055 draws (0 skipped), 143 dispatches, 161 passes, 160 barriers, 6 submits, 0.2 uploads (0.0 MB), 86 ub updates (59 overlays), GPU-bound wait 0.0 ms; <...> ``` this PR @ d49ffb35, frame-time run 1: ``` [hos sead::MainThread] vk: mirror syncs' pages from the captures by a compute pass (RX_VK_MSYNC_PASS) [hos Presentation Thread] vk: a frame's replay time (ms): submit 6.57, present 0.08, draw 9.22, descriptors 2.85, texture lookups 0.15, uploads 0.01, pipelines 0.36, programs 1.24, commands 3.70, dispatch 0.80, memory sync 0.90, queue submit 0.21, slot wait 0.<...> [hos Presentation Thread] vk: a frame: mirrors 69 (1428 MB): 8.38 MB synced in 1260.1 copies, 62.15 MB written back [hos Presentation Thread] vk: 8880 presents, 68.4 fps; a frame: 3047 draws (0 skipped), 140 dispatches, 161 passes, 161 barriers, 6 submits, 0.2 uploads (0.0 MB), 86 ub updates (59 overlays), GPU-bound wait 0.0 ms; <...> [hos Presentation Thread] vk: mirror syncs' pass (600 submits): 252 dispatches, 1344 chunks; 0 syncs as copies (no table room) ``` The GPU profile, one report each (main, then this PR): ``` [hos Presentation Thread] vk: 9.43 ms a frame in all (recordings' command buffers + upload command buffers; <...>) [hos Presentation Thread] vk: upload command buffers: 1.55 ms in 33.8 a frame (1.54 ms in spans, 0.01 ms between them: texture uploads and decodes, the page table, glue) [hos Presentation Thread] vk: u 1.108 ms 52.3 x 21.2 us 1312.2 items 8.71 MB msync copies (CPU pages from a capture -> mirror) ``` ``` [hos Presentation Thread] vk: 8.79 ms a frame in all (recordings' command buffers + upload command buffers; <...>) [hos Presentation Thread] vk: upload command buffers: 0.79 ms in 34.8 a frame (0.78 ms in spans, 0.01 ms between them: texture uploads and decodes, the page table, glue) [hos Presentation Thread] vk: u 0.329 ms 29.0 x 11.3 us 1326.2 items 8.66 MB msync pass (CPU pages from a capture -> mirror, one dispatch) ``` On Hookshot the same change measured the same way gave 1.13 -> 0.34 ms with the serial replay ([NotRexed/Hookshot#3023](https://github.com/NotRexed/Hookshot/pull/3023)). With `RX_VK_PARALLEL=4` the pass is one dispatch a frame instead of 29. The unprofiled runs can't time it, so its GPU time there is not measured. **After the rebase.** The runs are on e51e28e9. The branch is now on main 76699aad, which has the late pages' fix (#248); the conflict with it is resolved (Reviewer notes). - **What is the same.** The change's own lines in vk_memory.cpp are as they were, plus the two #248 needed: one usage function, and the shadow copy for the runs the pass takes. - **What changed under the runs.** - #248's fix is off when `RX_VK_PARALLEL` is above 1, and all ten runs set it to 4 (the profile pair too, though the profile ran the serial replay). So with these settings it stays off. Without the switch, the default, it is on with this pass, and that has not run in the game. - #216 changes how a capture walks its ranges (CPU side). #237 changes the mirror budget on small cards; on this 16 GB card Auto's stays 3 GB. #324 changes how the host commits and reports memory. - **Not measured on this head.** With these settings #248 stays off, so the syncs' GPU time should hold, but this head has not run in the game. I can run the A/B again on it if wanted. ## Testing **Tested:** - **Game-free:** `tests/test_msync_pass.py` (new) cuts `msync_pass_on`, `wbd_split`, `WbdChunk`, `MsyncCopy` and `msync_pass_add` out of vk_memory.cpp, as they are. It compiles them with `-std=c++20 -Wall -Wextra -Werror` against a fake pool, fake buffers and their addresses. It checks: - that applying the chunks, as wb_diff.comp would, gives the regions' bytes exactly. This covers sources at +0, +4, +8 and +12 bytes from the destination's 16-byte alignment, partial pages, long runs and short ones; - that every chunk is whole: at most 64 KB, `shadow == guest`, and a vector chunk 16-byte aligned at both ends; - that nothing is taken (the caller copies) in these cases: a run not in whole words, a run of size 0, no runs, no pipeline, a buffer without an address; - the switch, in a child process: unset is on; `0`, `false`, ` OFF `, `No` are off; - that the capture buffers are made with `cap_usage()`, which gives them storage and an address when the pass is on; - that the runs the pass takes still go into the write-back shadow (`shadow_sync_copy`); - that `msync_pass_flush`'s push constants are the write-back's, and that the shader's `Chunk` fields are `WbdChunk`'s, in order. Each of the three new checks for #248 (the usage function, its storage and address bits, the shadow copy) fails when its rule is undone in a copy of vk_memory.cpp. With `main`'s vk_memory.cpp it fails at the first cut (the pass is not there); with this change it passes (llvm-mingw clang 23.1.5, Windows 11). - **run_all:** `python -I -B tests/run_all.py --jobs 8` on Windows 11 (llvm-mingw clang 23.1.5). - On this branch at e089b486: 517 passed, 4 failed, 34 skipped. - On `main` 76699aad: 516 passed, the same 4 failed (`test_finish_output`, `test_oot_state_tap`, `test_pad_layout`, `test_tested_configurations`), 34 skipped. - **`test_tested_configurations` on both:** only its check of the CLI through an ASCII pipe fails, by `\r\n` against `\n` on Windows. The check that validates the repository's run files passes, this PR's ten included. - **The checks that read vk_memory.cpp.** 42 checks under `tests/` and `nx2/` read it besides this PR's, #248's three among them, and all pass unchanged on the branch. Three C++ fixtures read it too, and the checks that compile them pass (`test_mirror_inherit.py`, `test_recorder_sync.py`, `test_retired_give_back.py`). None needed changing. - **Renderer:** `longshot_vk.dll` built from e089b486 (`RX_VK_ONLY`, release, llvm-mingw): 0 failed, no warnings. - **In the game:** the ten runs in Measurements, on e51e28e9, with `RX_VK_PARALLEL=4` (the profile pair serial). I played them and closed each from the pause menu. - **With the pass:** the log's start line said so, 0 syncs went as copies, and there was no validation, device-lost or `VK_ERROR` line. - **On screen:** I saw nothing wrong in any of them, the HUD's text included. **Not tested:** - **This head in the game.** Above all the default serial replay, where #248's fix is on with this pass. The two meet in `mirror_sync` (the fix's dispatch, then the runs taken) and in the shadow copy; the test checks those from the text only. - **Other hardware:** AMD and Intel GPUs, Linux and the Steam Deck. The pass is the write-back diff's, which already writes host memory by device address on every GPU that makes its pipeline; this use of it reads host memory the same way, but it ran on one NVIDIA card only. - **The serial replay's frame rate on Longshot.** The profile pair ran it, but the profile costs frame rate. On Hookshot the fps were measured with both replays. - **The pass's GPU time with `RX_VK_PARALLEL=4`.** The profile turns that replay off. - **A GPU-bound setup,** where GPU time saved would show as frame rate. - **GCC.** - **A Linux build.** I built on Windows only (llvm-mingw). The change is Vulkan and C++ in gpu/vk/vk_memory.cpp, with no OS call and no path. **Needs other testers:** - AMD GPU, Windows: Hyrule Field standing still, with and without `RX_VK_MSYNC_PASS=0`. Expect the log's `mirror syncs' pass` lines with `0 syncs as copies`, nothing wrong on screen, and with `RX_VK_PASS_TIMING=1` the `msync pass` span below the `msync copies` one (request to open). - Linux or Steam Deck: build the branch, then the same, plus fps. A GPU-bound handheld is where the saving could show as frame rate (request to open). ## Screenshots Nothing on screen: the change moves where the GPU copies the same bytes. ## Risk and rollback - **What could break.** A sync's pages could come out wrong if a chunk table were reused while the GPU still read it, or on a GPU whose subgroup votes behave differently. That would show as wrong textures or geometry after a sync. - **With #248.** If the runs the pass takes missed the shadow copy, the write-back diff would put a capture's older words back over the CPU's newer ones, the flashes #248 describes. The test fails if that copy goes. - **Mitigations.** The tables are taken again only after their submission is done, and a GPU without the pipeline copies as before. - **Turning it off.** `RX_VK_MSYNC_PASS=0` turns it off, or the commit can be reverted alone. ## Reviewer notes - **Where to look.** `msync_pass_add` / `msync_pass_flush` and their call in `mirror_sync` (gpu/vk/vk_memory.cpp), and the presync in `memory_sync_mirrors_before_segments`. - **The design rule.** Rule 2 of [docs/design.md](https://git.magicbean.dev/MagicBeanDev/Longshot/src/branch/main/docs/design.md#4-the-lessons-from-mkw-as-rules-here), section 4, is "Nothing the replay reads comes from live guest memory". The pass reads only the capture buffers, as the copies did. The log's check for it, `N from guest memory as it was at the replay` in the `mirror syncs (600 submits)` lines, is 0 in every run's window on both sides. Non-zero ones show only at the save's load and at the close, on both sides alike. - **#248, the late pages' fix, merged first.** It conflicted with this one in gpu/vk/vk_memory.cpp, and the rebase resolves it: - **One usage function.** Both gave a capture buffer storage and an address, #248 with `cap_usage()`, this PR with `cap_buf_usage()`. This PR's is gone: `cap_usage()` gives the two bits when the fix or the pass is on. - **The shadow copy.** #248 copies a sync's runs into the pool's write-back shadow too (`shadow_sync_copy`), after the mirror's copy. The runs the pass takes get that copy as well, recorded where the mirror's copy was. Merged as git left it, they skipped it. - **The order.** `mirror_sync` first runs `late_fix_emit`, whose `full_barrier` after the fix's dispatch covers all reads, shader reads included. The pass's dispatch comes after that, at the end of `mirror_sync` or after the presync's loop, so it reads the fixed words. The fix's own barrier before its dispatch, which orders the shadow copies before it reads them, is unchanged. - **The shader.** #248 gave `wb_diff.comp` stamps: bit 1 of a chunk's `vecs`, and the push constant that was padding. The pass sets neither. - **docs/development.md.** This PR's row now comes after #248's two. - **The other open PRs.** I test-merged every open PR that changes vk_memory.cpp or docs/development.md (56) with this branch and with main. Each merges with this branch as it does with main: the same files conflict, or none. None conflicts because of this one. The runs are files in docs/tested/ now, so no table conflict either. ## Issues No Longshot issue found for the mirror syncs' copies (searched 2026-11-13). ## Checklist ### For everyone - [x] One topic. - [x] The title is a Conventional Commit (`type(scope): description`). - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` run and passing (any failure that also fails on `main` is named above). - [x] Tested in the game, or what wasn't is named under Testing. - [x] Testing filled in: what was tested, what wasn't, and what needs other testers (with test requests). - [x] Measured: main and this PR at named commits, on the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [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. ### If applicable - [x] AI assistance recorded with `Assisted-by:` / `Co-Authored-By:` trailers. - [x] Flags noted, and the switch reference updated (docs/development.md, the README). - [x] A file in `docs/tested/` for each new measured run (format in `docs/tested-configurations.md`; no appended row or assembled view). - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. Built on Windows only so far; the Linux build is in Needs other testers. - [x] Docs updated. Written with Claude Code (Claude Opus 5.5). I played the in-game runs. The commit carries a `Co-Authored-By` trailer. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
perf(vk): a mirror sync's pages from the captures go in one compute dispatch, not one copy region each
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
08c81e4c58
Ported from Hookshot, where it was merged on 2026-11-13
(https://github.com/NotRexed/Hookshot/pull/3023), onto Longshot main: Longshot's names (longshot_env.h, the
(longshot) comment tags). docs/tested-configurations.md has the ten runs of the in-game A/B on Longshot (four for
fps, two with the GPU profile, four with the frame-time log; main and this change interleaved).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
proci force-pushed perf/mirror-sync-pass from 08c81e4c58
Some checks failed
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
CI / Conventional Commit title (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Has been cancelled
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Has been cancelled
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Has been cancelled
CI / Workflow lint (actionlint) (pull_request) Has been cancelled
Third-party parts / As their notes say (pull_request) Has been cancelled
Third-party parts / The same as upstream (pull_request) Has been cancelled
to e089b48642
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
2026-11-14 02:14:41 +00:00
Compare
mooki left a comment

Read head e089b486 against main cf605c85: the complete diff, description, empty comments/reviews, and the capture, upload, shader, recorder and allocation paths. Eight compiled fixtures pass with GCC and Clang; the shader and tested-run checks pass too. The new test against main stops at the missing pass before compiling. No game or renderer build was run.

What's good

  • The cost exists in the old mirror_sync: each capture buffer's regions become a copy command. Reusing the existing chunk format and shader keeps the change small, and the shader consumes the new chunks as intended.
  • The rebase keeps the shadow copies and the barrier after the late-page fix. The capability and table-space fallbacks retain the old copies.
  • The description separates profiled GPU time from frame rate and discloses that the current head has not run in the game. The ten results are separate files under docs/tested/, not additions to the frozen table.

Must change

  1. Align each chunk table to nonCoherentAtomSize. In gpu/vk/vk_memory.cpp, lines 5278-5279, the new table is written and flushed at b->fill, which advances only by its byte count at line 5288. msync_block_get, lines 5194-5198, permits appending while earlier readers of the block are still pending. create_buffer does not require coherent memory. With a 128-byte atom, two one-chunk tables occupy bytes [0,32) and [32,64); flushing the second also covers the first table while its submitted dispatch may still read it. Vulkan counts that whole atom as a host write even when the neighboring bytes are unchanged, so this introduces a host/GPU data race in the chunk table. Align each independently published table, including the capacity calculation, as staging already does in gpu/vk/vk_texture.cpp, lines 456-463. Add a test of the actual block allocator and flush path with an earlier table pending and a non-coherent atom larger than one chunk. The rule is explicit at https://docs.vulkan.org/refpages/latest/refpages/source/vkFlushMappedMemoryRanges.html.

Measurement and smaller points

  • The displayed frame-rate medians differ by 0.28 fps, against 2.74 fps between the main repeats, so the stated lack of an FPS gain is supported. The 71% GPU-time reduction is from only one profiled run per side. The five and six reports inside those runs are not independent repeat runs. Complete a second interleaved profile pair and report the between-run spread before treating that percentage as a repeatable gain, especially after the rebase.
  • The new fixture executes chunk construction, but models the shader with memcpy and never executes msync_block_get or msync_pass_flush. Its passing result does not cover the lifetime and flush issue above.
  • The branch merges cleanly with main cf605c85. The source-check jobs are blocked, not passing: https://git.magicbean.dev/MagicBeanDev/Longshot/actions/runs/3782.

Not checked

No GPU execution, Windows execution, full build, full suite or new performance measurement here. The non-coherent race is established from the allocator and Vulkan's synchronization rule, not a reproduced visual fault. The default serial replay with the late-page fix needs current-head gameplay and synchronization-validation evidence; the published runs predate that combination. AMD, Intel, Steam Deck, long-session behavior and the underlying timing captures remain unjudged.

Read head `e089b486` against main `cf605c85`: the complete diff, description, empty comments/reviews, and the capture, upload, shader, recorder and allocation paths. Eight compiled fixtures pass with GCC and Clang; the shader and tested-run checks pass too. The new test against main stops at the missing pass before compiling. No game or renderer build was run. ### What's good - The cost exists in the old `mirror_sync`: each capture buffer's regions become a copy command. Reusing the existing chunk format and shader keeps the change small, and the shader consumes the new chunks as intended. - The rebase keeps the shadow copies and the barrier after the late-page fix. The capability and table-space fallbacks retain the old copies. - The description separates profiled GPU time from frame rate and discloses that the current head has not run in the game. The ten results are separate files under `docs/tested/`, not additions to the frozen table. ### Must change 1. **Align each chunk table to `nonCoherentAtomSize`.** In `gpu/vk/vk_memory.cpp`, lines 5278-5279, the new table is written and flushed at `b->fill`, which advances only by its byte count at line 5288. `msync_block_get`, lines 5194-5198, permits appending while earlier readers of the block are still pending. `create_buffer` does not require coherent memory. With a 128-byte atom, two one-chunk tables occupy bytes `[0,32)` and `[32,64)`; flushing the second also covers the first table while its submitted dispatch may still read it. Vulkan counts that whole atom as a host write even when the neighboring bytes are unchanged, so this introduces a host/GPU data race in the chunk table. Align each independently published table, including the capacity calculation, as `staging` already does in `gpu/vk/vk_texture.cpp`, lines 456-463. Add a test of the actual block allocator and flush path with an earlier table pending and a non-coherent atom larger than one chunk. The rule is explicit at https://docs.vulkan.org/refpages/latest/refpages/source/vkFlushMappedMemoryRanges.html. ### Measurement and smaller points - The displayed frame-rate medians differ by 0.28 fps, against 2.74 fps between the main repeats, so the stated lack of an FPS gain is supported. The 71% GPU-time reduction is from only one profiled run per side. The five and six reports inside those runs are not independent repeat runs. Complete a second interleaved profile pair and report the between-run spread before treating that percentage as a repeatable gain, especially after the rebase. - The new fixture executes chunk construction, but models the shader with `memcpy` and never executes `msync_block_get` or `msync_pass_flush`. Its passing result does not cover the lifetime and flush issue above. - The branch merges cleanly with main `cf605c85`. The source-check jobs are blocked, not passing: https://git.magicbean.dev/MagicBeanDev/Longshot/actions/runs/3782. ### Not checked No GPU execution, Windows execution, full build, full suite or new performance measurement here. The non-coherent race is established from the allocator and Vulkan's synchronization rule, not a reproduced visual fault. The default serial replay with the late-page fix needs current-head gameplay and synchronization-validation evidence; the published runs predate that combination. AMD, Intel, Steam Deck, long-session behavior and the underlying timing captures remain unjudged.
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 1/2) (pull_request) Blocked by required conditions
CI / Python source checks (ubuntu-24.04, part 2/2) (pull_request) Blocked by required conditions
CI / SPIR-V fixtures (global narrow load/store, direct surface atomics, F2I immediate) (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Third-party parts / As their notes say (pull_request) Blocked by required conditions
Third-party parts / The same as upstream (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
PR description / Description follows the template (pull_request_target) Successful in 4s
Website / Website on Cloudflare (pull_request_target) Has been skipped
This pull request is marked as a work in progress.
Some workflows are waiting to be reviewed.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u 'perf/mirror-sync-pass:proci-perf/mirror-sync-pass'
git switch 'proci-perf/mirror-sync-pass'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
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!326
No description provided.