fix(vk): the replay's own buffer writes stay in command order when unaligned or split #440
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!440
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:fix/replay-write-order"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type: fix · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max (game-free checks, arm64 and x86_64 object compiles; no game or GPU run) · Needs: Windows or Linux, an NVIDIA, AMD or Intel GPU
Why
The replay writes guest memory itself in three places: an event signal, the stream's uniform updates, and
nvnCommandBufferClearBuffer. Those writes go on the GPU in command order only when the bytes are 4-byte aligned and one
import holds them. Otherwise they were written on the CPU when the replay recorded them, which is before the GPU ran the
draws recorded ahead of them. Those draws could then read the new value, and an earlier GPU write of the same bytes could
land over it. That happens to an unaligned write and to a range that crosses from one game memory pool into the next. It
affects every GPU. Nobody has measured how often the game does this.
How to reproduce
Seen on: found by reading the code; not seen in a game. The game-free test shows it on macOS 26.7.1, Apple M4 Max,
against main 6c9edffe's
gpu/vksources. The replay's writes are the same code on every OS and GPU.Settings and switches: defaults.
Steps: game-free, from this branch's checkout:
python -I -B tests/test_replay_write_order.py --cxx clang++ --headers <Vulkan headers> --source <a tree of main>. In the game: an event signal, a uniform update or anvnCommandBufferClearBuffer that is unaligned or crosses two game pools, after a draw that reads those bytes.
Expected: the draw recorded before the write reads the old bytes, and the later write lands last.
Actual: on main the write is made on the CPU at record time, ahead of the GPU: the draw recorded before it reads the
new bytes, and an earlier GPU write can land over it. The test fails 10 of its 18 cases on main.
What changed
The replay's own writes now go on the GPU in command order also when they are unaligned or cross two game pools: a copy
from the recording's stream for unaligned bytes, and one update or fill per import. Only bytes outside every pool are
still written on the CPU, which is exact there.
gpu_piece_bind,gpu_put,gpu_writeandgpu_fillinvk_replay.cpp, thengpu_pool_pieceinvk_memory.cpp.gpu_writeingpu/vk/vk_replay.cpp(event signals andflush_ub_updates).vkCmdUpdateBuffer. Unaligned bytes go into the recording's stream (stream_copy) and are copied into place withvkCmdCopyBuffer, which has no alignment rule (gpu_put).gpu_piece_bind). The pieces end at each gamepool's end, because
pool_lookupimports a range inside one pool only. For a virtual pool's address they also endwhere the host's mapping ends.
nor writes them. A pool whose import fails, or a stream with no room left, also falls back to the CPU, as before.
NOP_CLEAR_BUFinreplay()now callsgpu_fill(vk_replay.cpp).vkCmdFillBuffer, and stamped as the GPU's as before. Theup to 3 bytes at either end are copied from the stream, with the value's byte order kept across a pool's end.
RX_VK_CLEAR_CPU_MAX, 256 bytes by default) are unchanged: they are written on the CPU in streamorder first. Their GPU part now also covers one that is unaligned or crosses two pools, as it already did one inside
a pool.
RX_VK_WATCH_RANGEclear line now says "NOT all on the GPU" where it said "NOT bound".gpu_pool_pieceingpu/vk/vk_memory.cpp(declared invk_internal.h). It returns how much of a range from anaddress one import can hold: up to the end of the game pool holding it, or, outside the pools, up to the next pool. It
uses the same "last pool starting at or below" rule as
pool_lookup. Only the fallback path calls it.tests/test_replay_write_order.py(new).gpu_write, theNOP_CLEAR_BUFcase and their helpers from the source and runs them against a stand-inthat records the Vulkan commands. The stand-in runs them in order at the submit, after the CPU writes made at record
time.
pool's mapping across two of them.
rules, and every write is checked for an earlier access of the same bytes with no barrier between.
tests/test_inline_write_order.pynow cutsgpu_writewith its two helpers, and its stand-in takesvkCmdCopyBuffer. An unaligned write now becomes an ordered copy where it was counted as a CPU fallback. Its checksthat a missing barrier or a missing work count fails are unchanged.
in that submission, and a CPU write there is exact.
across two pools run on the CPU, and a skipped B2T or T2B copy. Each needs reads gathered from several imports, which
is a separate pull request.
Measurements
Verdict: fixed in the game-free test: it fails 10 of its 18 cases on main and passes all 18 here. The full game-free
suite changes only by the new test. An ordering fix: nothing timed (no GPU run).
Commits: main
6c9edffe5d(2026-11-14); this PR fix/replay-write-order @fa9b6338a3.Hardware: Apple M4 Max (16 CPUs), macOS 26.7.1, Apple clang 21. Game-free: no GPU or game involved
Scene: none (an ordering fix; its proof is the test, as CONTRIBUTING's
fixrow allows)Method:
tests/test_replay_write_order.pyagainst main'sgpu/vksources (6c9edffe) and against this PR;the full game-free suite on both
How to reproduce:
python -I -B tests/test_replay_write_order.py --cxx clang++ --headers <Vulkan headers>(add--source <a tree of main>for main's side)6c9edffefa9b6338tests/test_replay_write_order.pytests/run_all.py --jobs 6)Screenshots: none; nothing on screen in the game-free tests.
Flags and settings
None.
Testing
Tested:
tests/test_replay_write_order.pyfails on main (10 of 18 cases) and passes here.tests/test_inline_write_order.pypasses here, and its two negative controls (no barrier, no work count) still failas they should.
tests/test_wb_diff_cpu_writes.pypasses.tests/test_threaded_rendering.py(main's #588, a render thread for the replay) passes on this head, in the suite run.test_replay_write_order,absent on main, passes here.
requests, some are checks main added or changed since
5e072fc8):test_appimage_package_retry,test_appimage_player_build,test_collect_wide,test_control,test_cpm_discovery,test_crash_dialog,test_crash_record,test_env_on,test_game_layout,test_lift_cleanup_error,test_linux_preflight,test_longshot_ctl,test_low_memory_notice,test_make_game_cache,test_mem_gate,test_mem_gate_children,test_mirror_trim,test_oot_state_tap,test_pcache_header_read,test_pipeline_cache_file,test_pipeline_cache_save,test_pipeline_play_cache,test_pipeline_tour,test_prof_symbols,test_program_cache_cleanup,test_program_cache_one_variant,test_readback_late,test_report_run,test_split_giants,test_vk_pgo,test_vram_budget_row,test_walk_key.recorder_sync_test.exe-2026-11-14-180744.ips).
gpu/vk/vk_replay.cppandgpu/vk/vk_memory.cppwith the renderer's flags (nx2/build.py),Apple clang 21, arm64 and x86_64.
vk_shader.cppandpipeline_cache_file.hneed (environ,posix_fallocate). Before the rebase they also built with that prelude.-Wall -Wextrasyntax-only compile showed no warning on the changed lines of either file.aa95d35f, at3156b569(fa9b6338before the rebases ontoe090cc48andaa95d35f):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.pyon wave S1's merge11a5b4e7, which carriesthis head: 568/22/17 (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:
vkCmdCopyBufferfrom the recording's stream (made withTRANSFER_SRC) into a pool buffer or its VRAM mirror (already a transfer destination for updates, fills and B2Bcopies), and more than one fill or update per range. Nobody has run them on a driver.
measured.
stream_copy,vkCmdCopyBuffer,vkCmdFillBuffer,gpu_buffer_bind) and the standard library.Must be tested:
3156b569) and mainaa95d35fthe sameway and play the same few minutes on each with
RX_VK_VALIDATION=1(the Khronos validation layer), expecting novalidation error and no log line on this PR that main does not show, and the same frame rate as main (by hand: test
request T-2J27).
Risks
What could break:
Each writes the same bytes the CPU wrote, but in command order. With VRAM mirrors those bytes now go to the mirror,
as an aligned write's do.
the CPU as before.
main6c9edffe(2026-11-14) without a conflict. Main changedvk_replay.cppin six commitssince this branch's base, among them #588 (a render thread for the replay, behind Threaded rendering): the writes this
PR moves are recorded where they were (
gpu_write, andNOP_CLEAR_BUFinreplay), into the recording's commandbuffer and stream, and
tests/test_threaded_rendering.pypasses on this head (Testing). No conflict with the othermacOS-port PRs rebased on
6c9edffe(git merge-treeof the new heads, 19:49 CST).Turning it off: revert the commit. There is no switch.
Compatibility: Nothing changes:
vk_replay.cpp,vk_memory.cppandvk_internal.hare not in the translator stamp(
nx2/translator_files.py), and saves, the options file, the shader and pipeline caches and the vk ABI are unchanged.Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), head3156b569, without a conflict; this PR's owncommits, old → new:
fa9b6338→3156b569; the commit names elsewhere in this description are from before therebase.
python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). On macOS: 532 passed, 17skipped, and 32 failures that also fail on main
6c9edffeon this Mac, named under Testing. After the rebase onto mainaa95d35f(head3156b569): wave S1's merge11a5b4e7, 568/22/17 against main aa95d35f's 543/33/17, no new failure.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.229bc0e62ffa9b6338a3WIP: fix(vk): the replay's own buffer writes stay in command order when unaligned or splitto fix(vk): the replay's own buffer writes stay in command order when unaligned or splitfa9b6338a3c307a843b0c307a843b03156b56989View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.