feat(vk): AMD Anti-Lag 2 with frame generation #2262

Closed
liska1304 wants to merge 2 commits from liska1304/feat/antilag-framegen into main
liska1304 commented 2026-11-11 12:26:15 +00:00 (Migrated from github.com)

With frame generation on, AMD Anti-Lag 2 turned itself off, so on AMD GPUs the input delay that frame generation adds got no help: the menu row said On, but the log said vk: AMD Anti-Lag 2: off. This keeps it on with frame generation.

Rebased onto main after #2118, and now built the way #2118 gives NVIDIA Reflex its present markers with frame generation.

What changed

  • vk_antilag.cpp: frame generation no longer turns Anti-Lag 2 off. The input stage is where it was (after the game thread's present, with Reflex's sleep). The present stage now gives the index of the frame the present shows, not the latest one: with frame generation, the display thread shows a frame after the game thread has already given the next frame's input stage.
  • vk_antilag.h: antilag_present_frame, a thread_local like reflex_present_frame: kAntiLagThisFrame (the default) is the frame being made, as without frame generation; the display thread sets a set's game frame for its game frame and 0 for a made frame. antilag_frame() is the frame whose input stage was given last, like reflex_frame().
  • vk_framegen.cpp: each set keeps antilag_id = antilag_frame() next to reflex_id in framegen_record, and display_main sets antilag_present_frame with the same choice as reflex_present_frame (timed ? game[k] : k == made) in both the direct path and present ahead. So only the set's game frame gives the present stage, at the same present that gets Reflex's present id and markers; the made frames before it give none. Auto, fixed 2x and up, FSR 3.1 interpolation and DLSS frame generation all go through this path.
  • vk_replay.cpp / vk_framegen.h: a deferred present keeps it in PendingPresent::antilag_frame, and present_commit restores it around present_out, as it does for reflex_frame. present_out still gives the stage right before vkQueuePresentKHR (and before a hand-off to the present thread). Without frame generation nothing changes.
  • A frame from before the row was last turned on gets no present stage (its input stage went to the driver's earlier run).
  • The row's hint now reads like the Reflex row's ("With frame generation too: it paces the game's frames."), and its 13 translations take the Reflex row's second sentence in each language. The README line says it works with frame generation.

How it was tested

  • tests/test_antilag.py compiles the real functions against the Vulkan headers with a recording vkAntiLagUpdateAMD. It checks the stages without frame generation, a game frame's present stage given after the next frame's input stage with its own index, nothing for a made frame (0), nothing for a frame from before the row was turned on again, and Off told once. Its wiring checks cover framegen_record, both present paths in display_main, the deferred present in present_image and present_commit. It fails on main.
  • The present-semaphores and present-source-crop fixtures got the new field and stub.
  • python -I -B tests/run_all.py: 448 passed, 37 skipped, 10 failed on Windows. The same 10 fail on unmodified main on this PC (builder, portable, updater and a few local-environment checks), not from this change.
  • vk_antilag.cpp, vk_framegen.cpp, vk_replay.cpp and vk_menu.cpp compile with llvm-mingw against Vulkan-Headers v1.4.345.
  • In the game, the rebased version (main + this PR, built from source): RX 9070 XT, Adrenalin 26.10.2, Windows 11, Ryzen 9 3900X, 2560x1440 fullscreen, FSR 3.1, frame generation Auto (144 Hz, present ahead). About 10 minutes with the row switched off and on several times: Anti-Lag 2 stays on with frame generation (vk: AMD Anti-Lag 2: on, no off after it), each switch took effect at once, 0 late frames (28,652 shown on time in the longest stretch with it on), about 58-59 fps with it on and off, no hang, no failed submit. The FPS counter's Breakdown shows more "Pacing" with the row on than off at the same frame time (it varies from frame to frame): the driver's wait at the input stage now happens with frame generation too.
  • Before the rebase (v2.1.0 base, the present stage then at each set's first present), the same setup ran about 5 minutes: 41,769 frames on time, 0 late, no hang.

Not tested

  • Latency in milliseconds: no PresentMon or AMD overlay numbers yet, only the Breakdown's Pacing above.
  • Fixed 2x/3x and up were not run separately (the same display-thread path as Auto).
  • DLSS frame generation (NVIDIA only), Linux / RADV, other AMD GPUs.
With frame generation on, AMD Anti-Lag 2 turned itself off, so on AMD GPUs the input delay that frame generation adds got no help: the menu row said On, but the log said `vk: AMD Anti-Lag 2: off`. This keeps it on with frame generation. Rebased onto main after #2118, and now built the way #2118 gives NVIDIA Reflex its present markers with frame generation. ## What changed - `vk_antilag.cpp`: frame generation no longer turns Anti-Lag 2 off. The input stage is where it was (after the game thread's present, with Reflex's sleep). The present stage now gives the index of the frame the present shows, not the latest one: with frame generation, the display thread shows a frame after the game thread has already given the next frame's input stage. - `vk_antilag.h`: `antilag_present_frame`, a `thread_local` like `reflex_present_frame`: `kAntiLagThisFrame` (the default) is the frame being made, as without frame generation; the display thread sets a set's game frame for its game frame and 0 for a made frame. `antilag_frame()` is the frame whose input stage was given last, like `reflex_frame()`. - `vk_framegen.cpp`: each set keeps `antilag_id = antilag_frame()` next to `reflex_id` in `framegen_record`, and `display_main` sets `antilag_present_frame` with the same choice as `reflex_present_frame` (`timed ? game[k] : k == made`) in both the direct path and present ahead. So only the set's game frame gives the present stage, at the same present that gets Reflex's present id and markers; the made frames before it give none. Auto, fixed 2x and up, FSR 3.1 interpolation and DLSS frame generation all go through this path. - `vk_replay.cpp` / `vk_framegen.h`: a deferred present keeps it in `PendingPresent::antilag_frame`, and `present_commit` restores it around `present_out`, as it does for `reflex_frame`. `present_out` still gives the stage right before `vkQueuePresentKHR` (and before a hand-off to the present thread). Without frame generation nothing changes. - A frame from before the row was last turned on gets no present stage (its input stage went to the driver's earlier run). - The row's hint now reads like the Reflex row's ("With frame generation too: it paces the game's frames."), and its 13 translations take the Reflex row's second sentence in each language. The README line says it works with frame generation. ## How it was tested - `tests/test_antilag.py` compiles the real functions against the Vulkan headers with a recording `vkAntiLagUpdateAMD`. It checks the stages without frame generation, a game frame's present stage given after the next frame's input stage with its own index, nothing for a made frame (0), nothing for a frame from before the row was turned on again, and Off told once. Its wiring checks cover `framegen_record`, both present paths in `display_main`, the deferred present in `present_image` and `present_commit`. It fails on `main`. - The present-semaphores and present-source-crop fixtures got the new field and stub. - `python -I -B tests/run_all.py`: 448 passed, 37 skipped, 10 failed on Windows. The same 10 fail on unmodified `main` on this PC (builder, portable, updater and a few local-environment checks), not from this change. - `vk_antilag.cpp`, `vk_framegen.cpp`, `vk_replay.cpp` and `vk_menu.cpp` compile with llvm-mingw against Vulkan-Headers v1.4.345. - In the game, the rebased version (main + this PR, built from source): RX 9070 XT, Adrenalin 26.10.2, Windows 11, Ryzen 9 3900X, 2560x1440 fullscreen, FSR 3.1, frame generation Auto (144 Hz, present ahead). About 10 minutes with the row switched off and on several times: Anti-Lag 2 stays on with frame generation (`vk: AMD Anti-Lag 2: on`, no `off` after it), each switch took effect at once, 0 late frames (28,652 shown on time in the longest stretch with it on), about 58-59 fps with it on and off, no hang, no failed submit. The FPS counter's Breakdown shows more "Pacing" with the row on than off at the same frame time (it varies from frame to frame): the driver's wait at the input stage now happens with frame generation too. - Before the rebase (v2.1.0 base, the present stage then at each set's first present), the same setup ran about 5 minutes: 41,769 frames on time, 0 late, no hang. ## Not tested - Latency in milliseconds: no PresentMon or AMD overlay numbers yet, only the Breakdown's Pacing above. - Fixed 2x/3x and up were not run separately (the same display-thread path as Auto). - DLSS frame generation (NVIDIA only), Linux / RADV, other AMD GPUs.
copilot-pull-request-reviewer[bot] (Migrated from github.com) reviewed 2026-11-11 12:26:24 +00:00
copilot-pull-request-reviewer[bot] (Migrated from github.com) left a comment

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
hckrman101 (Migrated from github.com) approved these changes 2026-11-11 12:35:09 +00:00
hckrman101 (Migrated from github.com) left a comment

Looks good to me. I went through the stage bookkeeping against the call sites:

  • The index is stamped in framegen_queue, which do_present calls before its ReflexSleep destructor gives the next input stage, so a set carries the frame it was made from, not the next one. do_present's own present does the same through antilag_frame(), so without frame generation the order and the indices are what main sends.
  • Present ahead keeps the index in PendingPresent and gives the stage in present_out from present_commit, still right before vkQueuePresentKHR. If the commit fails because the swapchain was made again, that frame simply gets no present stage, which is fine.
  • since correctly drops a set stamped before the row was turned off and on again. All calls stay under a.mu, and nothing takes dev.present_mu while holding it, so the display thread giving the present stage doesn't add a lock-order problem.
  • FSR 3.1 interpolation goes through the same display_main, so it's covered without its own path, as the description says.
  • The test drops the framegen_on cases because the gate is gone, and adds the out-of-order present, the made frame's 0, and the re-enable case, plus wiring checks for both present paths. That's an update, not a weakening. The four source checks only follow the new signatures.

Notes, none blocking:

  • In Auto, k == 0 is often a made frame (game[0] false), so the present stage marks the first interpolated frame toward that game frame, not the game frame itself. The README line says so, and since the Vulkan extension has no frame-generation rule (unlike the DX12 SDK's frame-type calls), I think that's a fair choice. A PresentMon or AMD overlay latency number with the row off and on would show whether the driver's wait still helps when the present comes after the next input stage. Worth a follow-up if you have a minute.
  • #2118 (DLSS frame generation with Reflex) touches the same lines: Job, PendingPresent, both present_image calls in display_main, present_out/present_commit, and test_present_ahead.py/test_present_semaphores.py. Whichever lands second needs a small rebase. Nothing conflicts in what they do.
  • No new menu option ids, and no platform-specific code. Linux/RADV is untested, as stated.

CI is green apart from the AppImage job, which is still pending.

Looks good to me. I went through the stage bookkeeping against the call sites: - The index is stamped in `framegen_queue`, which `do_present` calls before its `ReflexSleep` destructor gives the next input stage, so a set carries the frame it was made from, not the next one. `do_present`'s own present does the same through `antilag_frame()`, so without frame generation the order and the indices are what main sends. - Present ahead keeps the index in `PendingPresent` and gives the stage in `present_out` from `present_commit`, still right before `vkQueuePresentKHR`. If the commit fails because the swapchain was made again, that frame simply gets no present stage, which is fine. - `since` correctly drops a set stamped before the row was turned off and on again. All calls stay under `a.mu`, and nothing takes `dev.present_mu` while holding it, so the display thread giving the present stage doesn't add a lock-order problem. - FSR 3.1 interpolation goes through the same `display_main`, so it's covered without its own path, as the description says. - The test drops the `framegen_on` cases because the gate is gone, and adds the out-of-order present, the made frame's 0, and the re-enable case, plus wiring checks for both present paths. That's an update, not a weakening. The four source checks only follow the new signatures. Notes, none blocking: - In Auto, `k == 0` is often a made frame (`game[0]` false), so the present stage marks the first interpolated frame toward that game frame, not the game frame itself. The README line says so, and since the Vulkan extension has no frame-generation rule (unlike the DX12 SDK's frame-type calls), I think that's a fair choice. A PresentMon or AMD overlay latency number with the row off and on would show whether the driver's wait still helps when the present comes after the next input stage. Worth a follow-up if you have a minute. - #2118 (DLSS frame generation with Reflex) touches the same lines: `Job`, `PendingPresent`, both `present_image` calls in `display_main`, `present_out`/`present_commit`, and `test_present_ahead.py`/`test_present_semaphores.py`. Whichever lands second needs a small rebase. Nothing conflicts in what they do. - No new menu option ids, and no platform-specific code. Linux/RADV is untested, as stated. CI is green apart from the AppImage job, which is still pending.
@ -389,7 +389,8 @@ HDR peak brightness show only while HDR is on.
detail. `RX_VK_UPSCALE_LOD_BIAS=0` turns it off.
hckrman101 (Migrated from github.com) commented 2026-11-11 12:35:09 +00:00

Nit: "Unlike Reflex" is true on main today, but it goes stale if #2118 lands (Reflex with DLSS frame generation). Something like "It works with Frame generation too: ..." would avoid that.

Nit: "Unlike Reflex" is true on main today, but it goes stale if #2118 lands (Reflex with DLSS frame generation). Something like "It works with Frame generation too: ..." would avoid that.
liska1304 (Migrated from github.com) reviewed 2026-11-11 12:43:26 +00:00
@ -389,7 +389,8 @@ HDR peak brightness show only while HDR is on.
detail. `RX_VK_UPSCALE_LOD_BIAS=0` turns it off.
liska1304 (Migrated from github.com) commented 2026-11-11 12:43:26 +00:00

Done, thanks!

Done, thanks!
hckrman101 (Migrated from github.com) approved these changes 2026-11-11 16:28:02 +00:00
hckrman101 (Migrated from github.com) left a comment

Still good. The only change since my last review is the README line, which now reads "It works with Frame generation too" without the Reflex comparison, so it holds whether or not #2118 lands. The code is unchanged from a603d66, so my earlier notes still apply as notes, not blockers: a PresentMon or AMD overlay latency comparison would be nice to have, and whichever of this and #2118 merges second will need a rebase on PendingPresent/present_out and the shared tests.

CI: everything passes except appimage, which timed out on a network fetch. Main's Linux AppImage run fails the same way right now, so that is not from this PR.

Still good. The only change since my last review is the README line, which now reads "It works with Frame generation too" without the Reflex comparison, so it holds whether or not #2118 lands. The code is unchanged from a603d66, so my earlier notes still apply as notes, not blockers: a PresentMon or AMD overlay latency comparison would be nice to have, and whichever of this and #2118 merges second will need a rebase on `PendingPresent`/`present_out` and the shared tests. CI: everything passes except `appimage`, which timed out on a network fetch. Main's Linux AppImage run fails the same way right now, so that is not from this PR.
Joey-Magnus (Migrated from github.com) reviewed 2026-11-11 21:25:20 +00:00
Joey-Magnus (Migrated from github.com) left a comment

Needs a rebase after #2118: it conflicts in vk_framegen.cpp/.h, vk_replay.cpp, vk_menu.cpp, test_antilag.py, test_present_semaphores.py and tested-configurations.md, around PendingPresent/present_out. The change is still needed (main still has wanted() && !framegen_on() at vk_antilag.cpp:58). After the rebase, the present-stage index needs re-checking through #2118's present path. A latency measurement would also show the driver's wait helps when the present comes after the next frame's input stage.

Needs a rebase after #2118: it conflicts in `vk_framegen.cpp/.h`, `vk_replay.cpp`, `vk_menu.cpp`, `test_antilag.py`, `test_present_semaphores.py` and `tested-configurations.md`, around `PendingPresent`/`present_out`. The change is still needed (main still has `wanted() && !framegen_on()` at `vk_antilag.cpp:58`). After the rebase, the present-stage index needs re-checking through #2118's present path. A latency measurement would also show the driver's wait helps when the present comes after the next frame's input stage.
github-sync closed this pull request 2026-11-12 16:54:57 +00:00
Owner

@liska1304 commented on GitHub · 2026-11-12 15:52 UTC · original

Rebased onto main after #2118 (now one commit, 8d16615). The present stage follows #2118's Reflex path: antilag_present_frame works like reflex_present_frame, each set keeps antilag_id next to reflex_id, and only the set's game frame gives the present stage (also through present ahead's PendingPresent). So the earlier note about k == 0 being a made frame no longer applies.

Retested in the game with the rebased build (main + this PR, built from source): RX 9070 XT, Adrenalin 26.10.2, Windows 11, 2560x1440 fullscreen, FSR 3.1, frame generation Auto (144 Hz, present ahead).

  • Anti-Lag 2 stays on with frame generation (vk: AMD Anti-Lag 2: on, no off after it); switching the row off and on took effect at once each time.
  • About 10 minutes: 0 late frames (28,652 shown on time in the longest stretch with it on), about 58-59 fps with it on and off, no hang, no failed submit.
  • The FPS counter's Breakdown shows more "Pacing" with the row on than off at the same frame time (it varies frame to frame): the driver's wait at the input stage now happens with frame generation too.

No latency number in milliseconds yet (no PresentMon run). The description is updated.

> [@liska1304](https://github.com/liska1304) commented on GitHub · 2026-11-12 15:52 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2262#issuecomment-6828480160) Rebased onto main after #2118 (now one commit, 8d16615). The present stage follows #2118's Reflex path: `antilag_present_frame` works like `reflex_present_frame`, each set keeps `antilag_id` next to `reflex_id`, and only the set's game frame gives the present stage (also through present ahead's `PendingPresent`). So the earlier note about `k == 0` being a made frame no longer applies. Retested in the game with the rebased build (main + this PR, built from source): RX 9070 XT, Adrenalin 26.10.2, Windows 11, 2560x1440 fullscreen, FSR 3.1, frame generation Auto (144 Hz, present ahead). - Anti-Lag 2 stays on with frame generation (`vk: AMD Anti-Lag 2: on`, no `off` after it); switching the row off and on took effect at once each time. - About 10 minutes: 0 late frames (28,652 shown on time in the longest stretch with it on), about 58-59 fps with it on and off, no hang, no failed submit. - The FPS counter's Breakdown shows more "Pacing" with the row on than off at the same frame time (it varies frame to frame): the driver's wait at the input stage now happens with frame generation too. No latency number in milliseconds yet (no PresentMon run). The description is updated. <!-- github-sync:c:6828480160 -->
Owner

@hckrman101 approved on GitHub · 2026-11-12 16:02 UTC · original

Still good after the rebase onto #2118. I went through it again against the new present path:

  • Each set now keeps antilag_id = antilag_frame() next to reflex_id in framegen_record, which runs inside do_present before the ReflexSleep destructor gives the next input stage, so the id is the frame the set was made from.
  • display_main picks the frame the same way as Reflex in both the direct path and present ahead (timed ? game[k] : k == made), so only the set's game frame gives the present stage. Made frames give 0. That settles my earlier k == 0 note.
  • The deferred present keeps the frame in PendingPresent::antilag_frame, and present_commit puts it back around present_out and restores it afterwards, the same way it handles reflex_present_frame. The stage still goes out before vkQueuePresentKHR and before a hand-off to the present thread.
  • since drops sets stamped before the row was turned off and on again. Without frame generation the thread-local stays at kAntiLagThisFrame, so the order and the indices are what main sends.
  • Without the extension, or on non-AMD GPUs, ready is false, so antilag_frame() returns 0 and the display thread gives nothing. Lock order is unchanged: a.mu is only ever taken inside dev.present_mu, never the other way around.
  • There are no new option ids and the default stays Off. The test covers the out-of-order present, the made frame's 0, the re-enable case, and the wiring for both present paths and the commit.

On Joey-Magnus's review: the rebase is done and the PR merges cleanly. The present-stage index follows #2118's Reflex path, as checked above. A millisecond latency number is still missing. The in-game run on the RX 9070 XT shows the driver's wait now happens with frame generation (more Pacing in the Breakdown, frame rate unchanged, 0 late frames). The row is opt-in and stays Off by default, so I'm fine landing it without that number. A PresentMon or AMD overlay comparison with the row off and on, with frame generation, would still be welcome as a follow-up.

CI is green on the new head, including the AppImage job.

> [@hckrman101](https://github.com/hckrman101) approved on GitHub · 2026-11-12 16:02 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2262#pullrequestreview-5877724407) Still good after the rebase onto #2118. I went through it again against the new present path: - Each set now keeps `antilag_id = antilag_frame()` next to `reflex_id` in `framegen_record`, which runs inside `do_present` before the `ReflexSleep` destructor gives the next input stage, so the id is the frame the set was made from. - `display_main` picks the frame the same way as Reflex in both the direct path and present ahead (`timed ? game[k] : k == made`), so only the set's game frame gives the present stage. Made frames give 0. That settles my earlier `k == 0` note. - The deferred present keeps the frame in `PendingPresent::antilag_frame`, and `present_commit` puts it back around `present_out` and restores it afterwards, the same way it handles `reflex_present_frame`. The stage still goes out before `vkQueuePresentKHR` and before a hand-off to the present thread. - `since` drops sets stamped before the row was turned off and on again. Without frame generation the thread-local stays at `kAntiLagThisFrame`, so the order and the indices are what main sends. - Without the extension, or on non-AMD GPUs, `ready` is false, so `antilag_frame()` returns 0 and the display thread gives nothing. Lock order is unchanged: `a.mu` is only ever taken inside `dev.present_mu`, never the other way around. - There are no new option ids and the default stays Off. The test covers the out-of-order present, the made frame's 0, the re-enable case, and the wiring for both present paths and the commit. On Joey-Magnus's review: the rebase is done and the PR merges cleanly. The present-stage index follows #2118's Reflex path, as checked above. A millisecond latency number is still missing. The in-game run on the RX 9070 XT shows the driver's wait now happens with frame generation (more Pacing in the Breakdown, frame rate unchanged, 0 late frames). The row is opt-in and stays Off by default, so I'm fine landing it without that number. A PresentMon or AMD overlay comparison with the row off and on, with frame generation, would still be welcome as a follow-up. CI is green on the new head, including the AppImage job. <!-- github-sync:r:5877724407 -->
Commenting is not possible because the repository is archived.
No description provided.