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
No reviewers
Labels
No labels
area/accessibility
area/audio
area/input
area/install
area/menu
area/renderer
area/saves
area/tooling
area/upscaling
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
risk
high
risk
low
risk
medium
status
blocked
status
confirmed
status
duplicate
status
invalid
status
needs-info
status
wontfix
testing wanted
ci: approved
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Longshot!916
Loading…
Reference in a new issue
No description provided.
Delete branch "GummiNaomi/Longshot:perf/frame-limiter-ahead"
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: 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
nvnSyncWaitthat returns to0x8042cde0, inf_8042cd20) now waits for the GPU only upto 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).
NV(nvnSyncWait)inhost/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.replayed_waitingpu/vk/vk_replay.cpp(MkwVkRenderer::nvn_replayed_wait,MKWVK_API_VERSION24): waits untilthe render thread has replayed the hand-over a fence value belongs to (
q_doneagainst the hand-over number kept inhanded_values). Without the render thread a submit is replayed where it is made, so it answers at once.rx_nvn_replayed_waitinhost/rx_backend.c(1 at once on the WebGPU back end).tests/test_frame_ahead.pybuilds the realNV(nvnSyncWait)with a fake GPU and replay: the limiter waits forthe GPU two frames back and the replay one frame back, other callers for their own fence, and
HOS_FRAME_AHEAD=0restores the old wait. It fails on main.
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; theframe 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=0against the defaultHOS_FRAME_AHEAD=0, this PR @14e165ab14e165abRX_VK_ASYNC=0), heavy-scene fps, three roundsBaseline: 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.
Flags and settings
One new switch, on by default.
HOS_FRAME_AHEAD0: the game's frame limiter waits for the frame before on the GPU, as on the consoleTesting
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.pypasses here and fails on main;python -I -B tests/run_all.py: 331 passed, 79 failed, 189 skipped; main772fe283on the same PC the same hour: 329passed, 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-Werroror need gcc; the list is below).The 79 checks that fail here, all of them on main too
tests/test_alt_enter.pytests/test_appimage_player_build.pytests/test_audio_deadline.pytests/test_audio_output_errors.pytests/test_audio_wave_buffer_release.pytests/test_bc_levels.pytests/test_bind_memo.pytests/test_capture_copy_helpers.pytests/test_collect_wide.pytests/test_crash_dialog.pytests/test_dlss_actor.pytests/test_dlss_checks.pytests/test_dlss_math.pytests/test_draw_binds.pytests/test_env_file.pytests/test_env_on.pytests/test_fps_overlay_order.pytests/test_fps_rate.pytests/test_fps_stats.pytests/test_frame_whole.pytests/test_framegen_auto.pytests/test_framegen_gate.pytests/test_fsr3_math.pytests/test_game_font.pytests/test_gpu_choice.pytests/test_hd_rumble.pytests/test_high_fps_fixes.pytests/test_import_span.pytests/test_jobs_wait_runs_queued.pytests/test_lift_cleanup_error.pytests/test_linux_preflight.pytests/test_make_game_cache.pytests/test_make_game_prepare.pytests/test_menu_mouse_scroll.pytests/test_mirror_inherit.pytests/test_missing_nup.pytests/test_movie_upscale_policy.pytests/test_oot_jitter.pytests/test_options_budget_once.pytests/test_pass_hazard.pytests/test_pool_span.pytests/test_precompile_compute_guard.pytests/test_present_ahead.pytests/test_present_thread.pytests/test_prof_clock.pytests/test_program_cache_cleanup.pytests/test_prompt_auto_face.pytests/test_psychov35_default.pytests/test_psychov35_dlss.pytests/test_push_state.pytests/test_quick_pipelines.pytests/test_readback_late.pytests/test_report_words.pytests/test_rumble_mapping.pytests/test_rumble_menu.pytests/test_rumble_refresh.pytests/test_run_all_junit.pytests/test_same_bind.pytests/test_same_draw.pytests/test_same_dyn.pytests/test_seed_reader.pytests/test_shader_module_guard.pytests/test_skip_code.pytests/test_status_line.pytests/test_submitted_page.pytests/test_table_track.pytests/test_task_mesh_fallback_devices.pytests/test_tested_configurations.pytests/test_tex_spans_once_a_present.pytests/test_texture_layout_sizes.pytests/test_ub_gap.pytests/test_upscale_independent.pytests/test_upscale_label.pytests/test_utf8_paths.pytests/test_vertex_bind.pytests/test_vk_occlusion_queries.pytests/test_vk_pgo.pytests/test_walk_key.pytests/test_wb_pages.pyNot 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:
same with
HOS_FRAME_AHEAD=0, expecting a higher frame rate in the heavy scenes with the default and no frame whereLink or the scene goes dark (Test Farm: @Beanstalk linux, the gate route with and without HOS_FRAME_AHEAD=0)
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_VERSIONgoes to 24: the game andlongshot_vk.dllcome 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
type(scope): description) of the status line's type.main.python -I -B tests/run_all.pypasses (any failure that also fails onmainis named).third_party/untouched (a new folder with aPROVENANCE.mdinstead).When it applies
Assisted-by:/Co-Authored-By:trailers.docs/tested/for each new measured run (format indocs/tested-configurations.md).MKWVK_API_VERSIONbumped (gpu/vk/vk_api.h'sMkwVkHostorMkwVkRendererchanged).mainand passing here.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.)here: the host change is plain C with the calls the Linux build already has,
InterlockedIncrement64as the file useselsewhere. Asked of the Test Farm's Linux run above.)
🤖 Generated with Claude Code
@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 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.
@GummiNaomi only members of
BigBeanscan start the farm. One of them can approve this request by reacting 👍 to it.@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.
@GummiNaomi only members of
BigBeanscan start the farm. One of them can approve this request by reacting 👍 to it.@GummiNaomi only members of
BigBeanscan start the farm. One of them can approve this request by reacting 👍 to it.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:
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.
Beanstalk test farm
Commit
14e165a· tier reviewed · approved by futondestroyer (👍 on GummiNaomi's request) · dashboardWhat 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.
1or0(PR only).Started.
HOS_FRAME_AHEAD=1 (PR only)HOS_FRAME_AHEAD=0 (PR only)View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.