fix(framegen): the game keeps its own frame rate with frame generation on #2531

Open
NotRexed wants to merge 1 commit from fix/framegen-native-kept into main
NotRexed commented 2026-11-11 22:21:01 +00:00 (Migrated from github.com)

The problem. Frame generation made the game slower. From the owner's log (3440x1440, 180 Hz, mailbox, 8x): about 200–240 frames shown a second, nearly all of them late, so the game ran at ~25–30 fps instead of 60.

Why. At 8x, every game frame gets 7 made frames. Each one is a full-size pass on the game's GPU queue plus its own present, but a 180 Hz display can show only 3 frames per game frame at 60 fps. The display thread couldn't get the presents out fast enough (~220 a second), the game's three frame sets stayed busy, and the game waited for them (framegen_record's wait on busy).

The fix (gpu/vk/vk_framegen.cpp):

  • A fixed multiplier makes no more frames than the display can show (fixed_frames: the display's refresh rate divided by the game's frame rate, capped at the chosen multiplier). For example:

    • 8x at 60 fps on a 180 Hz display: 3x (180 shown).
    • On a 60 Hz display: the game's own frame alone.
    • 30 fps on 240 Hz: all 8x.

    A log line is written whenever the cap changes. RX_VK_FRAMEGEN_UNCAPPED=1 turns the cap off. DLSS Frame Generation keeps its own multiplier.

  • The display thread never holds the game back. When the game's next frame is already waiting, a made frame is left out; the set's game frame is always shown. The 5-second frame interpolation log line counts the frames left out. This doesn't apply with present ahead, where the copies are already submitted.

Tests: tests/test_framegen_auto.py checks fixed_frames (180/60/144/240 Hz, unknown refresh). The frame generation and present tests pass, and vk_framegen.cpp compiles (clang, -fsyntax-only).

Not yet verified in-game: the A/B is still to do (native fps with frame generation off vs 8x, and the flash check). The PC is in use for the owner's 2.2.0 test. This replaces the draft #2332, which capped the multiplier only to keep the game above 30 fps.

🤖 Generated with Claude Code

**The problem.** Frame generation made the game slower. From the owner's log (3440x1440, 180 Hz, mailbox, 8x): about 200–240 frames shown a second, nearly all of them late, so the game ran at ~25–30 fps instead of 60. **Why.** At 8x, every game frame gets 7 made frames. Each one is a full-size pass on the game's GPU queue plus its own present, but a 180 Hz display can show only 3 frames per game frame at 60 fps. The display thread couldn't get the presents out fast enough (~220 a second), the game's three frame sets stayed busy, and the game waited for them (`framegen_record`'s wait on `busy`). **The fix** (`gpu/vk/vk_framegen.cpp`): - **A fixed multiplier makes no more frames than the display can show** (`fixed_frames`: the display's refresh rate divided by the game's frame rate, capped at the chosen multiplier). For example: - 8x at 60 fps on a 180 Hz display: 3x (180 shown). - On a 60 Hz display: the game's own frame alone. - 30 fps on 240 Hz: all 8x. A log line is written whenever the cap changes. `RX_VK_FRAMEGEN_UNCAPPED=1` turns the cap off. DLSS Frame Generation keeps its own multiplier. - **The display thread never holds the game back.** When the game's next frame is already waiting, a made frame is left out; the set's game frame is always shown. The 5-second `frame interpolation` log line counts the frames left out. This doesn't apply with present ahead, where the copies are already submitted. **Tests:** `tests/test_framegen_auto.py` checks `fixed_frames` (180/60/144/240 Hz, unknown refresh). The frame generation and present tests pass, and `vk_framegen.cpp` compiles (clang, `-fsyntax-only`). **Not yet verified in-game:** the A/B is still to do (native fps with frame generation off vs 8x, and the flash check). The PC is in use for the owner's 2.2.0 test. This replaces the draft #2332, which capped the multiplier only to keep the game above 30 fps. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
github-sync changed title from WIP: fix(framegen): the game keeps its own frame rate with frame generation on to fix(framegen): the game keeps its own frame rate with frame generation on 2026-11-12 19:01:36 +00:00
Owner

@Jiro131 commented on GitHub · 2026-11-12 18:42 UTC · original

In-game test on Windows: this fixes the 4x hold on a 144 Hz display. The game settles at 2x and 60 fps with every frame on time. It reaches that state only after about 15-20 s of start-up at 3x and 48 fps, because fixed_frames measures the rate it is itself holding.

Setup

  • Hardware: i9-13900K, RTX 4090 (driver 32.0.16.2101), Windows 11, Samsung G93SC at 144 Hz (G-Sync Compatible).
  • Builds: main 51da0464 (base) against 51da0464 plus this PR's commit 9cf12c18, cherry-picked cleanly. Both built release with RX_VK_ONLY=1 and the NGX SDK. Only hookshot_vk.dll was swapped.
  • Settings: Frame generation 4x, Frame generation by Auto, Frame pacing on, cap 60, NVIDIA Reflex on, DLAA, windowed.
  • DLSS-G's runtime (310.9.16) makes 1 frame at most, so at 4x the renderer's own frames are used. V-Sync Auto then keeps its mailbox (#2695's fix), and present ahead is on with 6 images.

Base (51da0464), 5 launches

  • V-Sync Auto (mailbox):
    • 4 launches held the game at 36.0 fps (144 / 4), with 144 presents a second;
    • the frame interpolation line read 0 frames shown on time, 720 late ... <7 ms 540, ..., over 25 180 in nearly every 5 s window;
    • the fifth launch ran at 60 fps with every frame on time; I could not find what made it differ.
  • An earlier build (ba005ca4) also held at 36 with the same settings.
  • In the same launch, switching V-Sync to Off (immediate) gave 60 fps with every frame on time, about 240 presents a second. Switching back to Auto (a new mailbox swapchain) gave 36 again.
  • Resizing or snapping the window did not change it. Fullscreen with V-Sync Auto (mailbox) and with V-Sync On (FIFO) also held the game at 36. So NVIDIA's mailbox here behaves like FIFO: one image a refresh. A set of 4 then takes 4 refreshes, and the game waits on its sets.

This PR, one launch

Phase Log Game fps Frame interpolation (per 5 s)
Start 4x asked, 3x made: the display's 144 Hz over the game's 48 fps 48 0-23 on time, 700+ late; gaps 17-25 ms: 221-240
4x asked, 2x made: ... 53 fps 53-60 457 on time, 135 late
4x asked, 3x made: ... 48 fps (back) ~48 280 on time, 189 late
Settled 4x asked, 2x made: ... 60 fps 60.0 594-602 on time, 0-1 late; 7-9 ms apart: 554-602
V-Sync Off (immediate) 60.0 600-602 on time, 0 late
Back to V-Sync Auto (mailbox) 60.0 599-602 on time, 0 late

0 made ones left out throughout, as expected with present ahead.

The start-up

  • fixed_frames uses fg.game_ns, the game's measured frame time. While the display thread holds the game, that time is the held one.
  • At 3x on 144 Hz the hold is 144 / 3 = 48 fps, and fixed_frames(4, 144, 20.8 ms) = (int)(3.0 + 0.2) = 3. So 3x at 48 fps keeps itself going.
  • It left that state only when the game's rate rose by chance (53 fps gives 2), fell back once, then settled.
  • The same applies to 4x at 36: (int)(4.0 + 0.2) = 4. Here the first measurement apparently came from before the hold had formed, so it started at 3x.
  • One way out may be to use the game's own target (the frame rate cap, when the game reaches it) instead of the measured time, or to round down while the sets are busy. I have not tried either.

The user played and judged the feel; the figures come from hookshot_log.txt. Happy to test a revision on the same setup.

Assisted-by: Claude Code

🤖 Generated with Claude Code

> [@Jiro131](https://github.com/Jiro131) commented on GitHub · 2026-11-12 18:42 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2531#issuecomment-6832664875) In-game test on Windows: this fixes the 4x hold on a 144 Hz display. The game settles at 2x and 60 fps with every frame on time. It reaches that state only after about 15-20 s of start-up at 3x and 48 fps, because `fixed_frames` measures the rate it is itself holding. **Setup** - Hardware: i9-13900K, RTX 4090 (driver 32.0.16.2101), Windows 11, Samsung G93SC at 144 Hz (G-Sync Compatible). - Builds: `main` 51da0464 (base) against 51da0464 plus this PR's commit 9cf12c18, cherry-picked cleanly. Both built release with `RX_VK_ONLY=1` and the NGX SDK. Only `hookshot_vk.dll` was swapped. - Settings: Frame generation 4x, Frame generation by Auto, Frame pacing on, cap 60, NVIDIA Reflex on, DLAA, windowed. - DLSS-G's runtime (310.9.16) makes 1 frame at most, so at 4x the renderer's own frames are used. V-Sync Auto then keeps its mailbox (#2695's fix), and present ahead is on with 6 images. **Base (51da0464), 5 launches** - V-Sync Auto (mailbox): - 4 launches held the game at 36.0 fps (144 / 4), with 144 presents a second; - the `frame interpolation` line read `0 frames shown on time, 720 late ... <7 ms 540, ..., over 25 180` in nearly every 5 s window; - the fifth launch ran at 60 fps with every frame on time; I could not find what made it differ. - An earlier build (ba005ca4) also held at 36 with the same settings. - In the same launch, switching V-Sync to Off (immediate) gave 60 fps with every frame on time, about 240 presents a second. Switching back to Auto (a new mailbox swapchain) gave 36 again. - Resizing or snapping the window did not change it. Fullscreen with V-Sync Auto (mailbox) and with V-Sync On (FIFO) also held the game at 36. So NVIDIA's mailbox here behaves like FIFO: one image a refresh. A set of 4 then takes 4 refreshes, and the game waits on its sets. **This PR, one launch** | Phase | Log | Game fps | Frame interpolation (per 5 s) | |---|---|---|---| | Start | `4x asked, 3x made: the display's 144 Hz over the game's 48 fps` | 48 | 0-23 on time, 700+ late; gaps 17-25 ms: 221-240 | | | `4x asked, 2x made: ... 53 fps` | 53-60 | 457 on time, 135 late | | | `4x asked, 3x made: ... 48 fps` (back) | ~48 | 280 on time, 189 late | | Settled | `4x asked, 2x made: ... 60 fps` | 60.0 | 594-602 on time, 0-1 late; 7-9 ms apart: 554-602 | | V-Sync Off (immediate) | | 60.0 | 600-602 on time, 0 late | | Back to V-Sync Auto (mailbox) | | 60.0 | 599-602 on time, 0 late | `0 made ones left out` throughout, as expected with present ahead. **The start-up** - `fixed_frames` uses `fg.game_ns`, the game's measured frame time. While the display thread holds the game, that time is the held one. - At 3x on 144 Hz the hold is 144 / 3 = 48 fps, and `fixed_frames(4, 144, 20.8 ms)` = (int)(3.0 + 0.2) = 3. So 3x at 48 fps keeps itself going. - It left that state only when the game's rate rose by chance (53 fps gives 2), fell back once, then settled. - The same applies to 4x at 36: (int)(4.0 + 0.2) = 4. Here the first measurement apparently came from before the hold had formed, so it started at 3x. - One way out may be to use the game's own target (the frame rate cap, when the game reaches it) instead of the measured time, or to round down while the sets are busy. I have not tried either. The user played and judged the feel; the figures come from `hookshot_log.txt`. Happy to test a revision on the same setup. Assisted-by: Claude Code 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- github-sync:c:6832664875 -->
Commenting is not possible because the repository is archived.
No description provided.