WIP: feat(menu): Don't wait for the GPU, a row for the game's present poll answered at once (off by default) #656
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!656
Loading…
Reference in a new issue
No description provided.
Delete branch "mooki/Longshot:fix/present-nowait-menu"
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: feat · Risk: low · Tested on: macOS 26.7.1, Apple M4 Max: game-free checks only, no game run · Needs: Windows, Linux (both untested), and a game run with the row On on any platform
Why
After the game hands a frame's work to the GPU, its present waits until the GPU has finished that frame before the game
starts the next one. Where the GPU is the slower part, the GPU then sits idle while the CPU builds the next frame, and a
frame costs the GPU's time plus the CPU's instead of the larger of the two. On an Apple M4 Max each 33 ms frame in Hyrule
Field was about 23 ms of GPU work plus about 10 ms in which the GPU had nothing to do.
HOS_PRESENT_NOWAIT=1already lets the game go on without that wait (the render thread, Threaded rendering On, does thesame as its rule), but only as a switch for testing. This PR puts it in the Insert menu as Don't wait for the GPU, off
by default, on every platform: a player whose GPU is the limit can trade up to one frame more input delay for more frames
per second. Nothing changes for anyone who leaves it off.
The issue tracker was searched (
forge.py issue list --state all, 188 issues): none is about the present's wait. #410and #402 are low frame rates where the CPU is the limit, which this row does not help.
What changed
A Graphics row, Don't wait for the GPU (Off, On), in Frame generation and latency after the Game frame rate cap, kept
in
longshot_options.iniaspresent_nowait=besideframe_rate_cap=. On, the game's present poll is answered at oncewithout the render thread too, exactly as
HOS_PRESENT_NOWAIT=1answered it; Off, the default, the poll is as before. Therow applies in play, and
HOS_PRESENT_NOWAIT, when set, still decides for a run, asHOS_FPS_MAXdoes over the cap.present_nowait()inhost/nvn_native.c(new;nvnEventGetValueasks it at OoT's two poll sites, as the oldblock did).
HOS_PRESENT_NOWAITset: its answer for the run. Unset: at once while the render thread runs(
HOS_OPT_RENDER_THREAD, as before); otherwise the row, asked at each poll through the renderer(
HOS_OPT_PRESENT_NOWAIT), and the options file'spresent_nowait=until the renderer answers, ashost/nn_vi.cfps_refreshreads the cap.opts.present_nowait(gpu/vk/vk_menu.h, 0); the row ingpu/vk/vk_menu.cpp(tx_onoff, changes at once, nopreset sets it);
ini_keysaves it aspresent_nowait. Its help says the trade: "more frames per second where the GPUholds the game back, for up to one frame more input delay".
HOS_OPT_PRESENT_NOWAIT = 70ingpu/vk/vk_api.h(56 and 59 to 69 are taken by open pull requests),answered by the existing option callback in
gpu/vk/vk_replay.cpp: no newMkwVkHost/MkwVkRenderermember, soMKWVK_API_VERSIONis unchanged.gpu/vk/menu_i18n.h(machine-made, as the file says; theuntranslated row name "Threaded rendering" is kept in English where the help names it), counts updated.
tests/test_present_nowait.py(new);tests/test_threaded_rendering.pyreads the poll's decision frompresent_nowait()now;tests/test_menu_sections.pylists the row in its section;tests/test_audio_output_menu.pyand
tests/test_stick_dead_zone.pygetpresent_nowaitin theirOptionsstand-ins (they compileini_key).HOS_PRESENT_NOWAITindocs/development.md's switches; a line indocs/graphics-and-controls.md.Measurements
Verdict: not measured yet: no game was run for this PR. With the row Off (the default) the present poll gets the same
answer as on main, which
tests/test_present_nowait.pychecks; what On gains in frames per second and costs in inputdelay still has to be measured in the game (Testing, Must be tested). The game-free checks:
Hardware: macOS 26.7.1, Apple M4 Max
Method:
tools/ls-suite.py --full(tests/run_all.py --jobs 6) on each tree, compared by test nameScene: none in the game for the checks below (no game ran); the game measurement still owed is Hyrule Field by day,
standing still, Threaded rendering Off, Game frame rate cap Uncapped, FPS counter at Breakdown (Must be tested).
How to reproduce:
python -I -B tests/run_all.pyin a checkout of main and of this PR, compared by test name (heretools/ls-suite.py --full, 6 at once);python -I -B tests/test_present_nowait.pyalone for the new check.6c9edffede74a101tests/run_all.pytests/test_present_nowait.pytests/test_present_nowait.pyFlags and settings
A new menu row and its ini key;
HOS_PRESENT_NOWAITnow also goes over that row.present_nowait=inlongshot_options.ini)HOS_PRESENT_NOWAIT=1or0Testing
Tested:
tests/test_present_nowait.py(new) fails on main6c9edffeand passes here. It compiles the menu's realini_key,save()andmenu_init's reading of a line into the rows, and saves the row and reads it back besideframe_rate_cap=(Off without a line, On and Off again, values past the ends, two lines: the last). It compileshost/nvn_native.c'spresent_nowaitand runs it with no switch, withpresent_nowait=1, with the row turned On andOff in play, with the render thread, and with
HOS_PRESENT_NOWAIT=1over the row Off and=0over the row On, thefile's line and the render thread.
tests/test_threaded_rendering.py,tests/test_menu_sections.py,tests/test_menu_i18n.py(it checks the new translations: texts the menu's own, sorted, counts),tests/test_readme_menu_rows.py,tests/test_frame_rate_cap_step.py,tests/test_menu_env_overrides.py,tests/test_preset_exempt.py,tests/test_upscaler_rows.py,tests/test_bug_report_defaults.py, everytests/test_menu_*.pyandtests/test_nvn_*.py,tests/test_hle_stats.py,tests/test_present_trace.py, andtests/test_audio_output_menu.pyandtests/test_stick_dead_zone.pywith their stand-ins updated (a full run of thefirst commit showed them failing on the new
ini_keyline).6c9edffe, compared by test name: the same 32 checks fail on both, each with the sameerror lines as on main (compared with temporary paths taken out); the only difference is the new
tests/test_present_nowait.py, which passes. The 32 (on this Mac; why each fails here is not this PR's):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.gpu/vk/vk_menu.cppandgpu/vk/vk_replay.cpppassclang++ -fsyntax-onlywith the renderer's build defines, withthe same two Linux-only errors in
vk_menu.cppas main has on a Mac (posix_spawn_file_actions_addclosefrom_np,sigtimedwait).aa95d35f, at0c01c1fa(de74a101before 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, once the end commit gave
test_present_nowait's fixture main's #736 names(
gpu_device_cards,gpu_saved_card,gpu_card_missing);python -I -B tests/run_all.pyon wave S1's merge11a5b4e7, which carries this 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:
and its translations on screen.
compiled only inside the test above. The test requests T-E4TU (Windows) and T-9WPL (Linux) ask for the build, the
frames per second and the input delay.
Must be tested:
0c01c1fa) and mainaa95d35fthe same way; onboth, Threaded rendering Off, the Game frame rate cap at Uncapped, the FPS counter at Breakdown, standing still in Hyrule
Field by day. Read the frames per second a minute each on main, then on this PR with the row Off, On and Off again, and
film screen and controller at 240 fps to count the frames from pressing B to the sword swing, 10 presses each on main,
the row Off and the row On, expecting Off as main, On at least as fast where the GPU is the limit with the same picture
and no hang or stall when it is switched in play, and On's delay at most one frame more (by hand: test request T-E4TU).
test request T-9WPL).
(each 33 ms frame here was about 23 ms of GPU work), the same picture and no hang or stall when it is switched (by hand,
me).
and help to fit on screen, not cut off (by hand).
Risks
What could break: only with the row On or
HOS_PRESENT_NOWAIT=1. The game goes on while the GPU still draws the lastframe, so input reaches the screen up to a frame later, and GPU results the game reads without waiting for them can be a
frame older. With the row Off (the default) the poll gets the answer main gives. The poll asks the renderer's option
callback once per poll at OoT's two poll sites, where main read a cached value.
Turning it off: the row Off (the default), or
HOS_PRESENT_NOWAIT=0for a run; reverting the commit is enough.Compatibility:
longshot_options.inigets apresent_nowait=0line at the next save; a file without it loads Off,and an older build keeps the line as one it does not know. No saves, caches or builder tools change, and the vk ABI is
unchanged (
MKWVK_API_VERSION). A renderer from before this PR answers the new option with 0: the row's line is readuntil the renderer is up, then Off.
Checklist
Everyone
type(scope): description) of the status line's type.main: onaa95d35f(#761, 2026-11-15), head0c01c1fa, with two conflicts:docs/development.md(#488'sHOS_MOVIE_PACErow and this PR's row, both kept) andgpu/vk/menu_i18n.h'sper-language counts (main's after #735/#736, plus this PR's 2); and an end commit:
test_present_nowaitcompiles themenu's real
save(), which since #736 writes the GPU card, so its fixture gets main'sgpu_device_cards/gpu_saved_card/gpu_card_missingstand-ins; this PR's own commits, old → new:de74a101→78b7d65a; end commits added by the rebases:0c01c1fa; the commit names elsewhere in this description are frombefore the rebase.
python -I -B tests/run_all.pypasses (any failure that also fails onmainis named). After the rebase onto mainaa95d35f(head0c01c1fa): 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.Mac measurements and the two requests' runs)
for its Mac measurements: the row on screen)
docs/tested/for each new measured run (format indocs/tested-configurations.md). (waits for its Macmeasurements)
mainand passing here.tests/that reads changed source updated in this PR, not weakened.T-9WPL)
de74a1017388f59e339c88f59e339c0c01c1fafdView command line instructions
Checkout
From your project repository, check out a new branch and test the changes.