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

Closed
HenriqueSBueno wants to merge 1 commit from fps-limit into main
HenriqueSBueno commented 2026-11-09 00:30:52 +00:00 (Migrated from github.com)

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).
  • #340 (draft) is the other direction, native rates above 60 with a committed game clock; both touch nn_vi.c's pacing, this one only below 60.
  • #755: fullscreen + frame generation already holds the game to 60 ÷ multiplier through the presents; the limit makes that rate explicit, windowed too, without the display queue's two frames ahead.
## 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 - #340 (draft) is the other direction, native rates above 60 with a committed game clock; both touch `nn_vi.c`'s pacing, this one only below 60. - #755: fullscreen + frame generation already holds the game to 60 ÷ multiplier through the presents; the limit makes that rate explicit, windowed too, without the display queue's two frames ahead.
HenriqueSBueno commented 2026-11-09 02:06:01 +00:00 (Migrated from github.com)

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).

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).
NotRexed commented 2026-11-09 05:30:59 +00:00 (Migrated from github.com)

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

Closing to keep the queue clear — please rebase on current main and reopen; thanks!
Commenting is not possible because the repository is archived.
No description provided.