WIP: perf(nvn): the game's frame limiter waits for the frame before last on the GPU, so heavy scenes run 11 to 15 % faster #916

Draft
GummiNaomi wants to merge 1 commit from GummiNaomi/Longshot:perf/frame-limiter-ahead into main
Member

Type: perf · Risk: medium · Tested on: Windows 11, RTX 4070 SUPER (NVIDIA 617.42) · Needs: Linux; AMD; Intel; a Steam Deck

Why

In heavy scenes the game holds itself back. Once a frame, Ocarina of Time's frame limiter waits, with no timeout, until
the GPU has finished the frame before. On the console that wait is short. In Longshot the replay sits between the game
and the GPU, so the frame before ends later, and at Castle Town's south gate the wait took 4.2 ms of a 14.9 ms frame
while the GPU (75 % busy) and the render thread both had time to spare. Every PC pays this, whatever its GPU.

See #995.

What changed

The limiter's one wait (the nvnSyncWait that returns to 0x8042cde0, in f_8042cd20) now waits for the GPU only up
to the frame before last: the game runs at most two frames ahead of the GPU instead of one. It also waits until the
replay has replayed the frame before, because what that wait really guarded on the console is the game's per-frame
memory: it fills frame N + 2's scene buffer in the one frame N used (two in turn). Here the replay is what reads that
memory, so the replay must be done with it. A first version without that second wait gave the same frame rate but
drew Link and the scene dark for single frames in Hyrule Field (3 and 5 times in two 100 s runs).

  • nvn: NV(nvnSyncWait) in host/nvn_native.c: the limiter's call site only; every other sync wait the game makes
    (those before it reads GPU results back) waits for its own fence as before. HOS_FRAME_AHEAD=0: as before.
  • vk: replayed_wait in gpu/vk/vk_replay.cpp (MkwVkRenderer::nvn_replayed_wait, MKWVK_API_VERSION 24): waits until
    the render thread has replayed the hand-over a fence value belongs to (q_done against the hand-over number kept in
    handed_values). Without the render thread a submit is replayed where it is made, so it answers at once.
  • host: rx_nvn_replayed_wait in host/rx_backend.c (1 at once on the WebGPU back end).
  • tests: tests/test_frame_ahead.py builds the real NV(nvnSyncWait) with a fake GPU and replay: the limiter waits for
    the GPU two frames back and the replay one frame back, other callers for their own fence, and HOS_FRAME_AHEAD=0
    restores the old wait. It fails on main.
  • docs: the switch in docs/development.md.

Measurements

Verdict: faster in every round taken: Hyrule Field +12 % (67.9 -> 76.2 fps, six rounds), Castle Town's heavy scene
+15 % (66.2 -> 76.3 fps, two rounds). The slowest run with the change is faster than the fastest without it. No dark
frames in these 16 runs. With the render thread off (what PCs under 12 logical processors get) +4 % (67.3 -> 70.2
fps), and the dark-Link frames of #366 on that path fall from 13-25 a run to 0-1: the game no longer races the replay
there either. #769 fixes those frames at their cause; this change does not replace it.

Hardware: Windows 11 Pro 26200, RTX 4070 SUPER 12 GB (NVIDIA 617.42), Ryzen 9 9950X3D (16 cores, SMT off), 32 GB
Scene: Castle Town's south gate into Hyrule Field (save set "First Step into Hyrule Town"); 2560x1440, DLAA, no frame
generation, uncapped; render thread on (Auto on this PC)
Method: headless runs on a wall-clock script, the change off (HOS_FRAME_AHEAD=0) and on in turn, the same build; the
frame rate is the median of the log's 120-present fps lines with 2,000+ draws (the heavy scene), dark frames counted by a
local counter in the build (frames drawn with the scene buffer's light grid empty, the dark-Link frames of #366; not in
this change)
How to reproduce: the save set above; walk out of the south gate to the drawbridge and into the field; compare
HOS_FRAME_AHEAD=0 against the default

Before: HOS_FRAME_AHEAD=0, this PR @ 14e165ab After: this PR @ 14e165ab
Hyrule Field from the gate, heavy-scene fps, six rounds 68.2, 67.8, 67.7, 67.3, 68.0, 68.3 (median 67.9) 74.8, 76.1, 76.7, 76.2, 76.3, 75.5 (median 76.2)
Castle Town's south gate, standing, heavy-scene fps, two rounds 66.3, 66.1 (median 66.2) 76.2, 76.3 (median 76.3)
Dark-Link frames in those 16 runs 0 0
The game's sync waits that blocked (field, a run) 330-334 4-191
Hyrule Field from the gate, render thread off (RX_VK_ASYNC=0), heavy-scene fps, three rounds 66.8, 67.3, 68.0 (median 67.3) 70.2, 70.9, 69.5 (median 70.2)
Dark-Link frames in those runs (render thread off) 13, 25, 24 0, 1, 0

Baseline: the same build with the change switched off, because the dark-frame count needs the local counter in the build.
Earlier runs of the same build add one flash (two frames) in 20 runs with the change, during a 50 ms render-thread stall,
against none in 10 runs without; the first version (no replay wait) had 8 flashes in 2 field runs.

fac-a01  HOS_FRAME_AHEAD=0  fps heavy: median 68.1 | dark frames: 0
fac-a11  (default)          fps heavy: median 74.8 | dark frames: 0
... (the 22 rows are in docs/tested/2026-11-15-frame-ahead-gumminaomi-*.md)

Flags and settings

One new switch, on by default.

Flag What it is Before this PR After this PR Effect Helps / doesn't help / untested Recommendation
HOS_FRAME_AHEAD 0: the game's frame limiter waits for the frame before on the GPU, as on the console (no switch: the frame before) on (unset): the frame before last on the GPU, the frame before on the replay +11-15 % in heavy scenes here helps: RTX 4070 SUPER, render thread on; helps, render thread off: +4 %, and fewer dark frames; untested: AMD, Intel, Linux, Steam Deck on for everyone

Testing

Tested: Windows 11, RTX 4070 SUPER, NVIDIA 617.42: the 22 runs above (Hyrule Field six rounds, Castle Town's gate two, the field with the render thread off three,
each side in turn), plus 20 more runs of the same build with the change on (field and town, dark frames counted);
tests/test_frame_ahead.py passes here and fails on main; python -I -B tests/run_all.py: 331 passed, 79 failed, 189 skipped; main 772fe283 on the same PC the same hour: 329
passed, 80 failed, 189 skipped. Every failure here also fails on main (this PC's toolchain: Windows line endings in
test_tested_configurations.py's pipe test, the fixtures that compile with -Werror or need gcc; the list is below).

The 79 checks that fail here, all of them on main too
  • tests/test_alt_enter.py
  • tests/test_appimage_player_build.py
  • tests/test_audio_deadline.py
  • tests/test_audio_output_errors.py
  • tests/test_audio_wave_buffer_release.py
  • tests/test_bc_levels.py
  • tests/test_bind_memo.py
  • tests/test_capture_copy_helpers.py
  • tests/test_collect_wide.py
  • tests/test_crash_dialog.py
  • tests/test_dlss_actor.py
  • tests/test_dlss_checks.py
  • tests/test_dlss_math.py
  • tests/test_draw_binds.py
  • tests/test_env_file.py
  • tests/test_env_on.py
  • tests/test_fps_overlay_order.py
  • tests/test_fps_rate.py
  • tests/test_fps_stats.py
  • tests/test_frame_whole.py
  • tests/test_framegen_auto.py
  • tests/test_framegen_gate.py
  • tests/test_fsr3_math.py
  • tests/test_game_font.py
  • tests/test_gpu_choice.py
  • tests/test_hd_rumble.py
  • tests/test_high_fps_fixes.py
  • tests/test_import_span.py
  • tests/test_jobs_wait_runs_queued.py
  • tests/test_lift_cleanup_error.py
  • tests/test_linux_preflight.py
  • tests/test_make_game_cache.py
  • tests/test_make_game_prepare.py
  • tests/test_menu_mouse_scroll.py
  • tests/test_mirror_inherit.py
  • tests/test_missing_nup.py
  • tests/test_movie_upscale_policy.py
  • tests/test_oot_jitter.py
  • tests/test_options_budget_once.py
  • tests/test_pass_hazard.py
  • tests/test_pool_span.py
  • tests/test_precompile_compute_guard.py
  • tests/test_present_ahead.py
  • tests/test_present_thread.py
  • tests/test_prof_clock.py
  • tests/test_program_cache_cleanup.py
  • tests/test_prompt_auto_face.py
  • tests/test_psychov35_default.py
  • tests/test_psychov35_dlss.py
  • tests/test_push_state.py
  • tests/test_quick_pipelines.py
  • tests/test_readback_late.py
  • tests/test_report_words.py
  • tests/test_rumble_mapping.py
  • tests/test_rumble_menu.py
  • tests/test_rumble_refresh.py
  • tests/test_run_all_junit.py
  • tests/test_same_bind.py
  • tests/test_same_draw.py
  • tests/test_same_dyn.py
  • tests/test_seed_reader.py
  • tests/test_shader_module_guard.py
  • tests/test_skip_code.py
  • tests/test_status_line.py
  • tests/test_submitted_page.py
  • tests/test_table_track.py
  • tests/test_task_mesh_fallback_devices.py
  • tests/test_tested_configurations.py
  • tests/test_tex_spans_once_a_present.py
  • tests/test_texture_layout_sizes.py
  • tests/test_ub_gap.py
  • tests/test_upscale_independent.py
  • tests/test_upscale_label.py
  • tests/test_utf8_paths.py
  • tests/test_vertex_bind.py
  • tests/test_vk_occlusion_queries.py
  • tests/test_vk_pgo.py
  • tests/test_walk_key.py
  • tests/test_wb_pages.py

Not tested: AMD and Intel GPUs, Linux, a Steam Deck (no such hardware here); a 60 fps cap (where the gain is room
under the cap, not a higher number); other areas than Castle Town and the field.

Must be tested:

  • Linux, any GPU: Castle Town's south gate out to Hyrule Field and back for a few minutes, the FPS counter on, then the
    same with HOS_FRAME_AHEAD=0, expecting a higher frame rate in the heavy scenes with the default and no frame where
    Link or the scene goes dark (Test Farm: @Beanstalk linux, the gate route with and without HOS_FRAME_AHEAD=0)
  • AMD, Windows or Linux: the same, expecting the same (Test Farm: @Beanstalk amd, the gate route with and without HOS_FRAME_AHEAD=0)
  • Steam Deck: the same at its default settings, expecting no dark frame and the frame rate no lower than with
    HOS_FRAME_AHEAD=0 (Test Farm: @Beanstalk steamdeck)

Risks

What could break: a frame drawn with data the game has already overwritten for a later frame (a dark or wrong
frame), if some per-frame memory is read later than the replay's own pass over it. The replay wait covers what the
dark-frame counter found; other data was not watched. Input lag: the game may be one frame further ahead of the screen
(about 13 ms at 76 fps), which matters most uncapped.

Turning it off: HOS_FRAME_AHEAD=0.

Compatibility: MKWVK_API_VERSION goes to 24: the game and longshot_vk.dll come from the same build as always.
Saves, options and caches unchanged.

Issues

See #995. Related: #656 (a menu row for the present poll answered at once) is a different wait, the Presentation thread's poll of its frame's event; this change is the main thread's sync wait in the frame limiter. The two do not touch the same code.

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.
  • Rebased on current main.
  • python -I -B tests/run_all.py passes (any failure that also fails on main is named).
  • 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.
  • Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported.
  • Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value.
  • A file in docs/tested/ for each new measured run (format in docs/tested-configurations.md).
  • MKWVK_API_VERSION bumped (gpu/vk/vk_api.h's MkwVkHost or MkwVkRenderer changed).
  • A game-free test added for the fix, failing on main and passing here.
  • A check under tests/ that reads changed source updated in this PR, not weakened. (None needed a change: every check that reads these files passes as on main.)
  • Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Built on Windows only; no Linux build
    here: the host change is plain C with the calls the Linux build already has, InterlockedIncrement64 as the file uses
    elsewhere. Asked of the Test Farm's Linux run above.)
  • Docs updated.

🤖 Generated with Claude Code

**Type:** perf · **Risk:** medium · **Tested on:** Windows 11, RTX 4070 SUPER (NVIDIA 617.42) · **Needs:** Linux; AMD; Intel; a Steam Deck ## Why In heavy scenes the game holds itself back. Once a frame, Ocarina of Time's frame limiter waits, with no timeout, until the GPU has finished the frame before. On the console that wait is short. In Longshot the replay sits between the game and the GPU, so the frame before ends later, and at Castle Town's south gate the wait took 4.2 ms of a 14.9 ms frame while the GPU (75 % busy) and the render thread both had time to spare. Every PC pays this, whatever its GPU. See #995. ## What changed The limiter's one wait (the `nvnSyncWait` that returns to `0x8042cde0`, in `f_8042cd20`) now waits for the GPU only up to the frame before last: the game runs at most two frames ahead of the GPU instead of one. It also waits until the replay has replayed the frame before, because what that wait really guarded on the console is the game's per-frame memory: it fills frame N + 2's scene buffer in the one frame N used (two in turn). Here the replay is what reads that memory, so the replay must be done with it. A first version without that second wait gave the same frame rate but drew Link and the scene dark for single frames in Hyrule Field (3 and 5 times in two 100 s runs). - nvn: `NV(nvnSyncWait)` in `host/nvn_native.c`: the limiter's call site only; every other sync wait the game makes (those before it reads GPU results back) waits for its own fence as before. `HOS_FRAME_AHEAD=0`: as before. - vk: `replayed_wait` in `gpu/vk/vk_replay.cpp` (`MkwVkRenderer::nvn_replayed_wait`, `MKWVK_API_VERSION` 24): waits until the render thread has replayed the hand-over a fence value belongs to (`q_done` against the hand-over number kept in `handed_values`). Without the render thread a submit is replayed where it is made, so it answers at once. - host: `rx_nvn_replayed_wait` in `host/rx_backend.c` (1 at once on the WebGPU back end). - tests: `tests/test_frame_ahead.py` builds the real `NV(nvnSyncWait)` with a fake GPU and replay: the limiter waits for the GPU two frames back and the replay one frame back, other callers for their own fence, and `HOS_FRAME_AHEAD=0` restores the old wait. It fails on main. - docs: the switch in `docs/development.md`. ## Measurements **Verdict:** faster in every round taken: Hyrule Field +12 % (67.9 -> 76.2 fps, six rounds), Castle Town's heavy scene +15 % (66.2 -> 76.3 fps, two rounds). The slowest run with the change is faster than the fastest without it. No dark frames in these 16 runs. With the render thread off (what PCs under 12 logical processors get) +4 % (67.3 -> 70.2 fps), and the dark-Link frames of #366 on that path fall from 13-25 a run to 0-1: the game no longer races the replay there either. #769 fixes those frames at their cause; this change does not replace it. Hardware: Windows 11 Pro 26200, RTX 4070 SUPER 12 GB (NVIDIA 617.42), Ryzen 9 9950X3D (16 cores, SMT off), 32 GB Scene: Castle Town's south gate into Hyrule Field (save set "First Step into Hyrule Town"); 2560x1440, DLAA, no frame generation, uncapped; render thread on (Auto on this PC) Method: headless runs on a wall-clock script, the change off (`HOS_FRAME_AHEAD=0`) and on in turn, the same build; the frame rate is the median of the log's 120-present fps lines with 2,000+ draws (the heavy scene), dark frames counted by a local counter in the build (frames drawn with the scene buffer's light grid empty, the dark-Link frames of #366; not in this change) How to reproduce: the save set above; walk out of the south gate to the drawbridge and into the field; compare `HOS_FRAME_AHEAD=0` against the default | | Before: `HOS_FRAME_AHEAD=0`, this PR @ 14e165ab | After: this PR @ 14e165ab | |---|---|---| | Hyrule Field from the gate, heavy-scene fps, six rounds | 68.2, 67.8, 67.7, 67.3, 68.0, 68.3 (median 67.9) | 74.8, 76.1, 76.7, 76.2, 76.3, 75.5 (median 76.2) | | Castle Town's south gate, standing, heavy-scene fps, two rounds | 66.3, 66.1 (median 66.2) | 76.2, 76.3 (median 76.3) | | Dark-Link frames in those 16 runs | 0 | 0 | | The game's sync waits that blocked (field, a run) | 330-334 | 4-191 | | Hyrule Field from the gate, render thread off (`RX_VK_ASYNC=0`), heavy-scene fps, three rounds | 66.8, 67.3, 68.0 (median 67.3) | 70.2, 70.9, 69.5 (median 70.2) | | Dark-Link frames in those runs (render thread off) | 13, 25, 24 | 0, 1, 0 | Baseline: the same build with the change switched off, because the dark-frame count needs the local counter in the build. Earlier runs of the same build add one flash (two frames) in 20 runs with the change, during a 50 ms render-thread stall, against none in 10 runs without; the first version (no replay wait) had 8 flashes in 2 field runs. ``` fac-a01 HOS_FRAME_AHEAD=0 fps heavy: median 68.1 | dark frames: 0 fac-a11 (default) fps heavy: median 74.8 | dark frames: 0 ... (the 22 rows are in docs/tested/2026-11-15-frame-ahead-gumminaomi-*.md) ``` ### Flags and settings One new switch, on by default. | Flag | What it is | Before this PR | After this PR | Effect | Helps / doesn't help / untested | Recommendation | |---|---|---|---|---|---|---| | `HOS_FRAME_AHEAD` | `0`: the game's frame limiter waits for the frame before on the GPU, as on the console | (no switch: the frame before) | on (unset): the frame before last on the GPU, the frame before on the replay | +11-15 % in heavy scenes here | helps: RTX 4070 SUPER, render thread on; helps, render thread off: +4 %, and fewer dark frames; untested: AMD, Intel, Linux, Steam Deck | on for everyone | ## Testing **Tested:** Windows 11, RTX 4070 SUPER, NVIDIA 617.42: the 22 runs above (Hyrule Field six rounds, Castle Town's gate two, the field with the render thread off three, each side in turn), plus 20 more runs of the same build with the change on (field and town, dark frames counted); `tests/test_frame_ahead.py` passes here and fails on main; `python -I -B tests/run_all.py`: 331 passed, 79 failed, 189 skipped; main 772fe283 on the same PC the same hour: 329 passed, 80 failed, 189 skipped. Every failure here also fails on main (this PC's toolchain: Windows line endings in `test_tested_configurations.py`'s pipe test, the fixtures that compile with `-Werror` or need gcc; the list is below). <details><summary>The 79 checks that fail here, all of them on main too</summary> - `tests/test_alt_enter.py` - `tests/test_appimage_player_build.py` - `tests/test_audio_deadline.py` - `tests/test_audio_output_errors.py` - `tests/test_audio_wave_buffer_release.py` - `tests/test_bc_levels.py` - `tests/test_bind_memo.py` - `tests/test_capture_copy_helpers.py` - `tests/test_collect_wide.py` - `tests/test_crash_dialog.py` - `tests/test_dlss_actor.py` - `tests/test_dlss_checks.py` - `tests/test_dlss_math.py` - `tests/test_draw_binds.py` - `tests/test_env_file.py` - `tests/test_env_on.py` - `tests/test_fps_overlay_order.py` - `tests/test_fps_rate.py` - `tests/test_fps_stats.py` - `tests/test_frame_whole.py` - `tests/test_framegen_auto.py` - `tests/test_framegen_gate.py` - `tests/test_fsr3_math.py` - `tests/test_game_font.py` - `tests/test_gpu_choice.py` - `tests/test_hd_rumble.py` - `tests/test_high_fps_fixes.py` - `tests/test_import_span.py` - `tests/test_jobs_wait_runs_queued.py` - `tests/test_lift_cleanup_error.py` - `tests/test_linux_preflight.py` - `tests/test_make_game_cache.py` - `tests/test_make_game_prepare.py` - `tests/test_menu_mouse_scroll.py` - `tests/test_mirror_inherit.py` - `tests/test_missing_nup.py` - `tests/test_movie_upscale_policy.py` - `tests/test_oot_jitter.py` - `tests/test_options_budget_once.py` - `tests/test_pass_hazard.py` - `tests/test_pool_span.py` - `tests/test_precompile_compute_guard.py` - `tests/test_present_ahead.py` - `tests/test_present_thread.py` - `tests/test_prof_clock.py` - `tests/test_program_cache_cleanup.py` - `tests/test_prompt_auto_face.py` - `tests/test_psychov35_default.py` - `tests/test_psychov35_dlss.py` - `tests/test_push_state.py` - `tests/test_quick_pipelines.py` - `tests/test_readback_late.py` - `tests/test_report_words.py` - `tests/test_rumble_mapping.py` - `tests/test_rumble_menu.py` - `tests/test_rumble_refresh.py` - `tests/test_run_all_junit.py` - `tests/test_same_bind.py` - `tests/test_same_draw.py` - `tests/test_same_dyn.py` - `tests/test_seed_reader.py` - `tests/test_shader_module_guard.py` - `tests/test_skip_code.py` - `tests/test_status_line.py` - `tests/test_submitted_page.py` - `tests/test_table_track.py` - `tests/test_task_mesh_fallback_devices.py` - `tests/test_tested_configurations.py` - `tests/test_tex_spans_once_a_present.py` - `tests/test_texture_layout_sizes.py` - `tests/test_ub_gap.py` - `tests/test_upscale_independent.py` - `tests/test_upscale_label.py` - `tests/test_utf8_paths.py` - `tests/test_vertex_bind.py` - `tests/test_vk_occlusion_queries.py` - `tests/test_vk_pgo.py` - `tests/test_walk_key.py` - `tests/test_wb_pages.py` </details> **Not tested:** AMD and Intel GPUs, Linux, a Steam Deck (no such hardware here); a 60 fps cap (where the gain is room under the cap, not a higher number); other areas than Castle Town and the field. **Must be tested:** - Linux, any GPU: Castle Town's south gate out to Hyrule Field and back for a few minutes, the FPS counter on, then the same with `HOS_FRAME_AHEAD=0`, expecting a higher frame rate in the heavy scenes with the default and no frame where Link or the scene goes dark (Test Farm: @Beanstalk linux, the gate route with and without HOS_FRAME_AHEAD=0) - AMD, Windows or Linux: the same, expecting the same (Test Farm: @Beanstalk amd, the gate route with and without HOS_FRAME_AHEAD=0) - Steam Deck: the same at its default settings, expecting no dark frame and the frame rate no lower than with `HOS_FRAME_AHEAD=0` (Test Farm: @Beanstalk steamdeck) ## Risks **What could break:** a frame drawn with data the game has already overwritten for a later frame (a dark or wrong frame), if some per-frame memory is read later than the replay's own pass over it. The replay wait covers what the dark-frame counter found; other data was not watched. Input lag: the game may be one frame further ahead of the screen (about 13 ms at 76 fps), which matters most uncapped. **Turning it off:** `HOS_FRAME_AHEAD=0`. **Compatibility:** `MKWVK_API_VERSION` goes to 24: the game and `longshot_vk.dll` come from the same build as always. Saves, options and caches unchanged. ## Issues See #995. Related: #656 (a menu row for the present poll answered at once) is a different wait, the Presentation thread's poll of its frame's event; this change is the main thread's sync wait in the frame limiter. The two do not touch the same code. ## 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. - [x] Rebased on current `main`. - [x] `python -I -B tests/run_all.py` passes (any failure that also fails on `main` is named). - [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. - [x] Numbers from the same PC, scene and settings, at least 2 interleaved runs each, the median reported. - [x] Switches documented (docs/development.md; the README for a menu row) and shipped at their recommended value. - [x] A file in `docs/tested/` for each new measured run (format in `docs/tested-configurations.md`). - [x] `MKWVK_API_VERSION` bumped (`gpu/vk/vk_api.h`'s `MkwVkHost` or `MkwVkRenderer` changed). - [x] A game-free test added for the fix, failing on `main` and passing here. - [x] A check under `tests/` that reads changed source updated in this PR, not weakened. (None needed a change: every check that reads these files passes as on main.) - [ ] Host code builds on both Windows (llvm-mingw) and Linux, with UTF-8 paths. (Built on Windows only; no Linux build here: the host change is plain C with the calls the Linux build already has, `InterlockedIncrement64` as the file uses elsewhere. Asked of the Test Farm's Linux run above.) - [x] Docs updated. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
perf(nvn): the game's frame limiter waits for the frame before last on the GPU, so heavy scenes run 11 to 15 % faster
Some checks are pending
CI / Conventional Commit title (pull_request) Blocked by required conditions
CI / Workflow lint (actionlint) (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request) Blocked by required conditions
Website / Website on Cloudflare (pull_request_target) Has been skipped
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
Guide screenshots / Guide images and local UI source review (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
PR description / Description follows the template (pull_request_target) Successful in 4s
beanstalk/approval approved by futondestroyer (reviewed)
beanstalk/steamdeck 0/1 pass, 0 running, 1 queued
14e165ab6b
Once a frame Ocarina of Time's frame limiter (f_8042cd20, the nvnSyncWait
returning to 0x8042cde0) waits with no timeout until the GPU has finished the
frame before. With the replay between the game and the GPU that frame ends
later than on the console: at Castle Town's south gate the wait was 4.2 ms of a
14.9 ms frame while the GPU and the render thread both had time to spare.

That one wait now waits for the GPU only up to the frame before last, and for
the replay to have replayed the frame before (MkwVkRenderer::nvn_replayed_wait,
MKWVK_API_VERSION 24): the game fills frame N + 2's scene buffer in the one
frame N used, and here the replay is what reads it. Without the replay wait a
first version drew Link and the scene dark for single frames in Hyrule Field.
Every other sync wait is as before. HOS_FRAME_AHEAD=0: as before.

Measured on Windows 11, RTX 4070 SUPER, Ryzen 9 9950X3D, 2560x1440 DLAA,
uncapped, the change off and on in turn in one build: Hyrule Field from the
gate 67.9 -> 76.2 fps (six rounds), Castle Town's gate 66.2 -> 76.3 (two);
with the render thread off 67.3 -> 70.2 (three), where the dark-Link frames of
#366 fell from 13-25 a run to 0-1. No dark frames with the render thread on in
those runs. Not run on Linux, AMD, Intel or a Steam Deck.

Tests: tests/test_frame_ahead.py (the real NV(nvnSyncWait) with a fake GPU and
replay; fails on main). The switch is in docs/development.md; the measured runs
in docs/tested/.

See #995

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Author
Member

@Beanstalk linux, any GPU: the build itself (host code on Linux) and Castle Town's south gate (a save at the gate) out to the drawbridge and into Hyrule Field and back, a few minutes, uncapped, the FPS counter on; then the same with HOS_FRAME_AHEAD=0. Expect a higher frame rate in the heavy scenes with the default, and no single frame where Link or the scene goes dark. Please post both frame rates and whether any dark frame was seen.

@Beanstalk linux, any GPU: the build itself (host code on Linux) and Castle Town's south gate (a save at the gate) out to the drawbridge and into Hyrule Field and back, a few minutes, uncapped, the FPS counter on; then the same with HOS_FRAME_AHEAD=0. Expect a higher frame rate in the heavy scenes with the default, and no single frame where Link or the scene goes dark. Please post both frame rates and whether any dark frame was seen.
Author
Member

@Beanstalk amd, Windows or Linux: Castle Town's south gate (a save at the gate) out to the drawbridge and into Hyrule Field and back, a few minutes, uncapped, the FPS counter on; then the same with HOS_FRAME_AHEAD=0. Expect a higher frame rate in the heavy scenes with the default, and no single frame where Link or the scene goes dark. Please post both frame rates and whether any dark frame was seen.

@Beanstalk amd, Windows or Linux: Castle Town's south gate (a save at the gate) out to the drawbridge and into Hyrule Field and back, a few minutes, uncapped, the FPS counter on; then the same with HOS_FRAME_AHEAD=0. Expect a higher frame rate in the heavy scenes with the default, and no single frame where Link or the scene goes dark. Please post both frame rates and whether any dark frame was seen.
Member

@GummiNaomi only members of BigBeans can start the farm. One of them can approve this request by reacting 👍 to it.

@GummiNaomi only members of `BigBeans` can start the farm. One of them can approve this request by reacting 👍 to it.
Author
Member

@Beanstalk steamdeck, its default settings: Castle Town's south gate (a save at the gate) out to the drawbridge and into Hyrule Field and back, a few minutes, uncapped, the FPS counter on; then the same with HOS_FRAME_AHEAD=0. Expect a higher frame rate in the heavy scenes with the default, and no single frame where Link or the scene goes dark. Please post both frame rates and whether any dark frame was seen.

@Beanstalk steamdeck, its default settings: Castle Town's south gate (a save at the gate) out to the drawbridge and into Hyrule Field and back, a few minutes, uncapped, the FPS counter on; then the same with HOS_FRAME_AHEAD=0. Expect a higher frame rate in the heavy scenes with the default, and no single frame where Link or the scene goes dark. Please post both frame rates and whether any dark frame was seen.
Member

@GummiNaomi only members of BigBeans can start the farm. One of them can approve this request by reacting 👍 to it.

@GummiNaomi only members of `BigBeans` can start the farm. One of them can approve this request by reacting 👍 to it.
Member

@GummiNaomi only members of BigBeans can start the farm. One of them can approve this request by reacting 👍 to it.

@GummiNaomi only members of `BigBeans` can start the farm. One of them can approve this request by reacting 👍 to it.
futondestroyer left a comment

Readiness assessment at 14e165ab6b51151a20bedb2881eaa823826912d0; this is not a source-code review or approval. I read the description, test evidence, current checks and farm-request discussion. No build, tests or game run were performed for this assessment.

The reported Windows/NVIDIA gains make this worth prioritizing, but it is not ready for final approval:

  • The Linux host build checkbox is still unticked, and the Linux, AMD and Steam Deck runs explicitly listed under Must be tested have no results yet.
  • Beanstalk says those requests require a BigBeans member's approval. Please react thumbs-up to the existing Linux, AMD and Steam Deck request comments rather than create duplicate requests:
  • CI checks are pending; a passing description check alone does not establish build/runtime readiness.

Please incorporate the resulting exact-head build and paired default/HOS_FRAME_AHEAD=0 runtime evidence into the description, including any dark frames, test duration and limitations. Keep the disclosed earlier flashes and extra-frame latency risk visible. Once the required coverage and checks are settled, update the checklist and mark the PR ready for code review under the repository workflow.

No new correctness finding is asserted here; this comment records the outstanding readiness gates, not a verdict on the synchronization implementation.

Readiness assessment at `14e165ab6b51151a20bedb2881eaa823826912d0`; this is not a source-code review or approval. I read the description, test evidence, current checks and farm-request discussion. No build, tests or game run were performed for this assessment. The reported Windows/NVIDIA gains make this worth prioritizing, but it is not ready for final approval: - The Linux host build checkbox is still unticked, and the Linux, AMD and Steam Deck runs explicitly listed under Must be tested have no results yet. - Beanstalk says those requests require a BigBeans member's approval. Please react thumbs-up to the existing Linux, AMD and Steam Deck request comments rather than create duplicate requests: - Linux: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/916#issuecomment-29363 - AMD: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/916#issuecomment-29364 - Steam Deck: https://git.magicbean.dev/MagicBeanDev/Longshot/pulls/916#issuecomment-29366 - CI checks are pending; a passing description check alone does not establish build/runtime readiness. Please incorporate the resulting exact-head build and paired default/HOS_FRAME_AHEAD=0 runtime evidence into the description, including any dark frames, test duration and limitations. Keep the disclosed earlier flashes and extra-frame latency risk visible. Once the required coverage and checks are settled, update the checklist and mark the PR ready for code review under the repository workflow. No new correctness finding is asserted here; this comment records the outstanding readiness gates, not a verdict on the synchronization implementation.
Member

Beanstalk test farm

Commit 14e165a · tier reviewed · approved by futondestroyer (👍 on GummiNaomi's request) · dashboard

What will be tested · AI plan (gpt-6.1-terra)

This plan waits for a Steam Deck tester; none is currently online. In roughly 25 minutes it compares uncapped frame rates and sampled images in the Market and Hyrule Field, with frame-ahead enabled and disabled because the switch controls the changed limiter. The gate save is unavailable, so teleports substitute for the requested gate-to-drawbridge route. A crash, a frame-rate drop over 10%, or a substantial sampled image difference fails the plan; the sampled images cannot establish whether any single dark frame occurred. No tester online now matches steamdeck: that target waits for one.

  • Where: steamdeck (1 report).
  • Matrix: 2 cells, each on main and the PR: HOS_FRAME_AHEAD 1 or 0 (PR only).
  • market-field: from a new save (settings fps_counter=1, frame_rate_cap=6), then teleport to Market, measure market for 30 s (after 5 s warm-up), take the picture market to compare with main, teleport to Hyrule Field, move the stick, measure field for 60 s (after 5 s warm-up), take the picture field to compare with main; 1 run on each side for each of 2 cells (2 game starts a run).
  • Measured: crashes, draws and frame rate, frame times, stutters and hitches, screenshots against main.
  • The PR fails if: it crashes; or frame rate is more than 10% below main; or a picture differs from main by more than 15%.
  • Time: about 31 min of game runs per tester (4 game starts), plus the two builds.

Started.

Target Reports Crash Frame p99 (ms) vs main VRAM peak (MB) vs main Result
steamdeck 0/1 — — — queued
Scenario · matrix cell Target Frame p99 (ms) vs main Result
market-field · HOS_FRAME_AHEAD=1 (PR only) steamdeck — —
market-field · HOS_FRAME_AHEAD=0 (PR only) steamdeck — —
<!-- beanstalk:sticky --> ### Beanstalk test farm Commit `14e165a` · tier **reviewed** · approved by futondestroyer (👍 on GummiNaomi's request) · [dashboard](https://beanstalk.magicbean.dev/prs/916) **What will be tested** · AI plan (`gpt-6.1-terra`) This plan waits for a Steam Deck tester; none is currently online. In roughly 25 minutes it compares uncapped frame rates and sampled images in the Market and Hyrule Field, with frame-ahead enabled and disabled because the switch controls the changed limiter. The gate save is unavailable, so teleports substitute for the requested gate-to-drawbridge route. A crash, a frame-rate drop over 10%, or a substantial sampled image difference fails the plan; the sampled images cannot establish whether any single dark frame occurred. No tester online now matches steamdeck: that target waits for one. - **Where:** steamdeck (1 report). - **Matrix:** 2 cells, each on main and the PR: HOS_FRAME_AHEAD `1` or `0` (PR only). - **market-field:** from a new save (settings fps_counter=1, frame_rate_cap=6), then teleport to Market, measure market for 30 s (after 5 s warm-up), take the picture market to compare with main, teleport to Hyrule Field, move the stick, measure field for 60 s (after 5 s warm-up), take the picture field to compare with main; 1 run on each side for each of 2 cells (2 game starts a run). - **Measured:** crashes, draws and frame rate, frame times, stutters and hitches, screenshots against main. - **The PR fails if:** it crashes; or frame rate is more than 10% below main; or a picture differs from main by more than 15%. - **Time:** about 31 min of game runs per tester (4 game starts), plus the two builds. Started. | Target | Reports | Crash | Frame p99 (ms) vs main | VRAM peak (MB) vs main | Result | | --- | --- | --- | --- | --- | --- | | steamdeck | 0/1 | — | — | — | queued | | Scenario · matrix cell | Target | Frame p99 (ms) vs main | Result | | --- | --- | --- | --- | | market-field · `HOS_FRAME_AHEAD=1 (PR only)` | steamdeck | — | — | | market-field · `HOS_FRAME_AHEAD=0 (PR only)` | steamdeck | — | — |
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
Guide screenshots / Guide images and local UI source review (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
Website / Website on Cloudflare (pull_request_target) Has been skipped
PR description / Description follows the template (pull_request_target) Successful in 4s
beanstalk/approval approved by futondestroyer (reviewed)
beanstalk/steamdeck 0/1 pass, 0 running, 1 queued
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/frame-limiter-ahead:GummiNaomi-perf/frame-limiter-ahead'
git switch 'GummiNaomi-perf/frame-limiter-ahead'
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
3 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!916
No description provided.