Skip to content

Insert menu: Frame rate limit (30, 40, 45, 50 fps) - #895

Closed
HenriqueSBueno wants to merge 1 commit into
NotRexed:mainfrom
HenriqueSBueno:fps-limit
Closed

HenriqueSBueno wants to merge 1 commit into
NotRexed:mainfrom
HenriqueSBueno:fps-limit

Conversation

@HenriqueSBueno

Copy link
Copy Markdown
Contributor

Problem

On a CPU where the replay is the limit, the game's own rate swings with the view. On an i5-10400F + RTX 3080 (driver 591.x, Windows 11, v2.0.5), standing at the same spot in Kokiri Forest, the native rate goes between ~31 and ~46 fps as the camera turns (1800 to 2900 draws a frame; the replay thread's submit is 13-25 ms, the GPU 10-15 ms). Frame generation passes that swing on to the screen: the shown intervals spread over the 7-25 ms buckets, and with fullscreen on a 60 Hz screen the game is held to 60 ÷ the multiplier anyway (#755), so a frame that misses 33 ms waits for the next vsync.

There is no way to ask for a steady lower rate: HOS_FPS_TARGET only raises the game's own limiter (MKW), and HOS_GAME_INTERVAL follows OoT's controller, which snaps to 33/50/67 ms.

Change

A Frame rate limit row in the Insert menu's Graphics tab: Off (default), 30, 40, 45, 50 fps, saved as frame_rate_limit in hookshot_options.ini. HOS_FPS_LIMIT=<fps> (15-59) wins over the menu.

  • host/nn_vi.c: frame_period() is the longer of the display interval and 1/limit; the presents are paced by it. With a limit, the acquire waits one period after the last present instead of letting the display queue run two frames ahead (two limited frames of extra input delay otherwise).
  • Game time keeps following the clock (realtime_step measures the real frame time), so Real time speed is unchanged.
  • gpu/vk: the row, its labels, menu_fps_limit() and host option 18.
  • Off: fps_limit() is 0, the period and the acquire are exactly as before.

With Frame generation 2x and Frame pacing, a 30 fps limit is an even 60 on a 60 Hz screen on a PC that keeps 30 but not 60 (see the results below for a spot where this one does not).

Testing

  • tests/test_fps_limit.py (new, in run_all.py with --cc gcc): compiles fps_limit() and frame_period() out of nn_vi.c with stand-ins for getenv and rx_option and checks off, 30, 45, the env over the menu, a longer game interval kept, 60 and <15 as off; reads the menu row, labels, table and option 18 from the sources.
  • python -I -B tests/run_all.py fps_limit report_words text_speed: pass (Windows, llvm-mingw).
  • In game, same save point, windowed, Frame generation 2x + pacing:

Heavy Kokiri view (~2440 draws), Frame generation 2x + Frame pacing, windowed, 60 s each:

limit native fps (median, min-max) replay submit GPU shown intervals 12-17 ms / 17-25 ms / over 25 ms (3 windows)
Off 24.6 (23.2-32.5) 27.3 ms 22.6 ms 65 / 681 / 7
30 28.4 (24.9-29.8) 24.3 ms 16.4 ms 374 / 478 / 11
  • With the limit the shown frames move toward the 60 Hz interval (12-17 ms: 65 → 374), and the GPU time a frame drops (22.6 → 16.4 ms): the display queue no longer runs frames ahead.
  • At this spot the machine does not keep a full 30 with frame generation on (frame generation costs the replay thread too), so it is not an even 60 here; in lighter views (~46 native fps walking) it is.
  • Flash detector (RX_VK_FLASH, every frame captured) walking with the limit at 30: 0 flash events, 0 white blowouts, 0 black frames over ~1800 frames.
  • Off: same code path as before (fps_limit() is 0).

Related

A PC whose replay makes 35-45 fps swung between those rates, and frame
generation passed the swing on to the screen. The new Graphics row (and
HOS_FPS_LIMIT=<fps>, 15-59, over it) paces the game's frames to at most
that rate in host/nn_vi.c: the present period becomes 1/limit, and the
acquire waits one period after the last present instead of letting the
display queue run two limited frames ahead. Game time keeps following the
clock (realtime_step). Off by default: the pacing is unchanged then.

tests/test_fps_limit.py compiles fps_limit() and frame_period() out of
nn_vi.c and checks the menu row, labels, table and option 18.
@HenriqueSBueno

Copy link
Copy Markdown
Contributor Author

Follow-up on the same PC, fullscreen at 3840x2160 / 60 Hz, Frame generation 2x + Frame pacing, Output resolution 3840x2160 with Resolution 67% (#849 on top), interleaved runs:

limit native fps (median) shown 12-17 ms / 17-25 ms / over 25 ms (5 s windows)
Off 32.1, 34.3 258/51/0, 321/1/0, 330/0/0, 337/0/0
30 29.1, 29.3 151/140/1, 100/182/4, 192/100/2, 237/63/0

In fullscreen the presents already hold the shown rate at 60 (#755), and there the limit makes the shown intervals less even, not more. The windowed gain above still holds, but this is not the improvement I described for fullscreen, so I'm moving this to draft until the pacer and the limit agree (the display thread's interval estimate divided by the multiplier, against a game held at exactly 33.3 ms).

@HenriqueSBueno
HenriqueSBueno marked this pull request as draft November 9, 2026 02:06
@NotRexed

NotRexed commented Nov 9, 2026

Copy link
Copy Markdown
Owner

Closing to keep the queue clear — please rebase on current main and reopen; thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants