Skip to content

feat(dlss): DLSS Frame Generation through NGX, evenly paced at 3x and up - #2118

Merged
hckrman101 merged 9 commits into
NotRexed:mainfrom
pmwork-personal:feat/dlss-frame-gen-ngx
Nov 11, 2026
Merged

hckrman101 merged 9 commits into
NotRexed:mainfrom
pmwork-personal:feat/dlss-frame-gen-ngx

Conversation

@pmwork-personal

@pmwork-personal pmwork-personal commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

On an RTX 5090 at 5120x2160 the built-in frame generation couldn't follow motion. Its block search covers ±32 px per game frame, so a camera pan's vectors fell to zero and every made frame was a cross-fade of two game frames: smoother counters, same stepping motion. This PR makes those frames with NVIDIA's DLSS Frame Generation through NGX, from the temporal upscaler's own depth and motion. It also fixes two pacing problems that made 3x feel uneven even once the frames were right.

What changed (five commits, each on its own)

  1. fix(vk): present 3x and 4x sets ahead too. Frame generation 2x: frames reach the screen 5.7 / 11 ms apart instead of 8.3 (VRR flicker), with cause and a patch #1702's present-ahead (a set's copies go to the GPU together, and only the presents wait for their times) ran for 2x only. At 3x and 4x each made frame's copy still queued behind the game's next frame and reached the screen unevenly. It now runs for any paced set the swapchain can hold, and at 3x/4x the swapchain asks for one spare image per frame (5 and 6 images on NVIDIA).
  2. feat(dlss): DLSS-G through NGX (vk_dlss.cpp, beside DLSS on the same NGX session, no Streamline):
    • Inputs. The chain's depth and motion (actor motion included) are copied at its evaluation, with the camera. The scene's projection is now kept beside its view-projection (camera block 0x70).
    • Output. At the present, DLSS-G writes the frame generation row's made frames into the renderer's own set, which the display thread shows and paces as before.
    • Fallback. A set it doesn't make is made by the existing methods, and the log says every 600 presents how many sets DLSS-G made and why it didn't make the rest.
    • Motion units. The motion goes to NGX in render pixels (MvecScale 1), the same as to DLSS. The header's "[-1, 1]" is Streamline's convention. With 1/render size, DLSS-G saw no motion and made cross-fades again; I captured frames to confirm, and it was 0.2/255 off a pure blend at t = 1/3 and 2/3.
    • Exit. The feature is let go with the queue idle.
    • Runtime. nvngx_dlssg.dll is looked for where DLSS's runtime is.
    • Menu. A Frame generation by row: Auto (DLSS-G where it runs, otherwise the built-in one) or Built-in. Both frame generation rows now sit under the Upscaler, as asked in #dev-work. The row changes at once, with no restart. RX_VK_DLSS_FG=0/1 overrides it for a run.
  3. fix(vk): V-Sync Auto presents FIFO with DLSS Frame Generation at 2x and up. Auto was mailbox, which on NVIDIA is Fast Sync: below the refresh rate (~135 shown on 165 Hz) frames land on the refresh grid, so evenly timed presents still judder. With DLSS-G making the sets, Auto now asks for FIFO, which a VRR display shows as presented. The built-in frame generation and other GPUs keep Auto as before (from review, item 5). On and Off are unchanged. The swapchain is rebuilt when this changes, as it already is for V-Sync.
  4. feat(vk): NVIDIA Reflex with frame generation (added after review). Reflex was off whenever frame generation was on, so its held-back frame added its whole delay (nullifyshot measured +12 to +18 ms at 4x on an RTX 5080). The game's frame calls stay on: the sleep, the simulation markers and render submission. Each set keeps the game frame it was made at, and only that frame is presented with its present id and present markers; made frames get neither. Present ids only go up. Without frame generation nothing changes. On the 5090 at 3x, Reflex's own timings now appear under DLSS-G: simulation start to present end ~41 ms, against ~16 ms with frame generation off (details in the comments).
  5. fix(dlss): DLSS-G only where it runs (from review). NGX isn't asked for frame generation's device extensions, or started at a present, on a device without NVIDIA's extensions. V-Sync Auto's FIFO is for DLSS-G only. Frame generation at Off or Auto lets DLSS-G's feature go. A barrier follows a refused frame before the built-in passes write the same images.

How it was tested

  • RTX 5090, driver 621.14, Windows 11, 5120x2160 @ 165 Hz with G-SYNC, DLAA, frame generation 3x, Kokiri Forest:
  • tests/run_all.py (Windows, at 90c4683): 340 passed, 28 skipped, 3 failed. The same 3 also fail on untouched main at this branch's base here:
    • test_recorder_sync's exit case;
    • test_precompile_compute_guard;
    • test_finish_output (a NameError in make_game.py; it passes on current main).
  • Updated tests:
    • test_present_ahead.py: 3x/4x with and without enough spare images.
    • test_vsync_row.py: the present-mode choice with DLSS-G and with the built-in frame generation.
    • test_upscaler_rows.py: the new row order.
    • present_source_crop_test.cpp and test_present_semaphores.py: stubs for the new calls.
    • test_recorder_sync.py: knows NGX's DLSS-G create/evaluate calls as direct recordings with their B_DIRECT boundary.

Not tested / known

🤖 Generated with Claude Code

pmwork-personal and others added 3 commits November 10, 2026 22:17
…ge for each frame

Present ahead (NotRexed#1702) submits a paced set's copies into the window together and keeps only the presents on
their times; it ran for 2x only, so at 3x and 4x each made frame's copy still queued behind the game's next
frame on the same queue and reached the screen unevenly (frame generation at 3x felt less even than
Streamline's at the same rates). It now runs for any paced set the swapchain can hold (a spare image per
frame), and the swapchain asks for that many over the surface's least when the menu is at 3x or 4x at the
start (NVIDIA: 5 and 6 images). A set the chain cannot hold is shown one at a time as before.

test_present_ahead.py: 3x and 4x with enough spare images, not with fewer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…er's depth and motion

Frame generation at 5120x2160 made its frames from the finished picture alone: the block search covers
+-32 px a game frame, so a camera pan's motion fell to zero and every made frame was a cross-fade of the
two game frames (motion stepped at the game's rate). On RTX 40 and newer this makes them with NVIDIA's
DLSS Frame Generation through NGX (no Streamline), from the temporal Upscaler's own depth and motion
(actor motion included) and its camera.

- vk_dlss.cpp: DLSS-G beside DLSS, on the same NGX session. The chain's depth and motion are copied at its
  evaluation (dlss_fg_inputs) with the camera (the scene's projection is now kept beside its view-projection,
  camera block 0x70); at the present the made frames of the Frame generation row's set are evaluated into
  the renderer's own set (dlss_fg_frames), which the display thread shows and paces as before. A set it
  does not make (no Upscaler this frame, a gap, more frames than the GPU makes, a refusal) is made by the
  renderer's own methods as before, and the log counts which and why every 600 presents.
- The motion goes to NGX in render pixels (MvecScale 1), as to DLSS. With 1 / render size (the [-1, 1]
  nvsdk_ngx_defs_dlssg.h describes, which is Streamline's convention) DLSS-G saw no motion and every made
  frame was a cross-fade again: captured at 5120x2160, t = 1/3 and 2/3 and 0.2 of 255 off a pure blend;
  with 1 the same scenes came out as moved pictures between the game's.
- Insert menu: a "Frame generation by" row, Auto (DLSS-G where it runs, else the built-in one) or
  Built-in, and the Frame generation and Frame pacing rows moved under the Upscaler's (asked for in #dev-work). Its device
  extensions are asked of NGX whenever DLSS may run and the chain's images are always copyable, so the row
  changes at once; turned off, the feature is let go. RX_VK_DLSS_FG=0 / 1 over the row for a run.
- At the exit the feature is let go with the queue idle (as PR 1944's Streamline path needed).
- nvngx_dlssg.dll is looked for where DLSS's runtime is (the game folder's ngx first).

Played on an RTX 5090 (driver 621.14, Windows 11), 5120x2160 DLAA, frame generation 3x: DLSS-G made every
set in gameplay (600 / 600), ~135 frames shown from ~45 game fps; clean exits.

Tests: present_source_crop_test.cpp stubs dlss_fg_wanted / dlss_fg_frames; test_recorder_sync.py knows
NGX's frame generation calls (NGX_VK_*_DLSSG) as recordings with their B_DIRECT boundary;
test_upscaler_rows.py checks the new order (frame generation between the Render resolution and the window
rows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With V-Sync Auto the swapchain is mailbox where the driver has it: NVIDIA's Fast Sync, which puts frames on
the refresh grid whenever they come slower than the display. With frame generation at 3x (~135 shown on a
165 Hz screen) the display thread's evenly timed presents still reached the screen unevenly. Auto now asks
for FIFO when frame generation is at 2x and up, which a VRR display shows as presented; On and Off are as
before. The swapchain is made again when frame generation's multiplier changes, as for V-Sync itself
(swapchain_framegen), so its present mode and its spare images (present ahead) follow the row.

Tests: test_vsync_row.py's present-mode choice with frame generation (Auto FIFO with or without mailbox,
On and Off as before); test_present_semaphores.py stubs swapchain_framegen.

Played on an RTX 5090, 165 Hz G-SYNC, 5120x2160, DLSS Frame Generation 3x: even pacing (as with Streamline's
DLSS-G in NotRexed#1944), and the game ~45 fps instead of ~42.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Arkex-ai

Arkex-ai commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Tested exact head ec27f1f on an RTX 5070 Ti / Windows 11, 3440x1440 output, DLSS Quality 2294x960, game cap 60, FG 2x, pacing on. Fresh guest generation and matching API 14 host/backend build. NGX loaded the pinned official SR runtime and the DLSS-G runtime supplied with Streamline 2.14.16 from an isolated ngx directory; no Streamline interposer was loaded for this path.

NGX created its feature and logged repeated 600/600 sets made by DLSS-G, with zero refused and no fallback in those windows. Three controls used the same host/backend, copied checkpoint and active window. The controller reported 3440x1440 at 159 Hz; all three swapchains logged FIFO (V-Sync Auto). PresentMon 2.6 recorded actual display timestamps, separately from the native-present trace; no pixel capture during timing.

Phase Off displayed/s Built-in 2x NGX 2x
Gameplay before pause 60.03 119.88 120.05
Inventory 60.03 119.88 120.05
First second after return 59.47 119.46 118.71
Stabilized 59.98 120.23 120.14

Native rates stayed around 60 in all modes. Displayed P95 in the first return second: 18.78 ms off, 12.51 ms built-in, 12.52 ms NGX. There was no missing display feedback in those measurement windows. Normal exit from inventory returned 0 with NGX active, and both controls also exited normally. Presented hearts/instructions and inventory text were legible in the frames inspected after timing.

This confirms the NGX path runs on this RTX 50 setup. It is one run per mode in the initial interior scene, not a latency, motion-quality, 3x/4x, fixed-refresh, or campaign test. With the game capped at 60, both FG methods reach about 120 displayed/s here; these measurements do not establish an overall winner. PresentMon marked frames Application, so individual generated-frame identity comes from neither that label nor the UI screenshots; the generator attribution above comes from the NGX set log.

@nullifyshot

Copy link
Copy Markdown
Contributor

I tested this on an RTX 5080 (1920x1080 at 360 Hz, G-SYNC, driver V-Sync on, in-game V-Sync Off) at 4x, measuring input latency with DLSS-G against frame generation off. DLSS-G worked well: it made every set in gameplay ("600 made by DLSS-G" in each window). It adds a fair amount of latency, though, and part of that is because NVIDIA Reflex is off while it runs.

How I measured. The game pressed its own buttons through HOS_INPUT_LIVE. A local log line (not part of this PR) recorded when each press was due, on the QPC clock. Desktop Duplication timed the first composed frame that showed the reaction, using LastPresentTime on the same clock. That covers the game's input read through to the frame on the desktop. It leaves out the controller and the monitor. I used 20 presses per action, with Link idle in the same spot each time.

Action DLSS-G 4x Frame generation off Difference
Camera flick (right stick) 37.8 ms 25.7 ms +12.1 ms
Sword (Y) 62.5 ms 45.1 ms +17.4 ms
Walk (left stick) 64.3 ms 46.6 ms +17.7 ms

These are medians. The 10–90% range was about ±5 ms around each, and the two modes don't overlap. The game's own frame rate was about 77 fps with DLSS-G and about 99 fps with frame generation off.

Reflex is off whenever frame generation is on, DLSS-G included.

  • The code: on() in gpu/vk/vk_reflex.cpp requires !framegen_on(). That check gates the sleep, every marker and the timing log. Anti-Lag 2 has the same check. This PR doesn't touch either file, and vk_dlss.cpp never calls into Reflex.
  • In game: with Reflex set to On + Boost and RX_VK_REFLEX_LOG=1, a DLSS-G session wrote no Reflex timing report in over 10,000 frames. The log writes one every 300 frames while Reflex runs.
  • One quirk: NVIDIA Reflex: on + boost is still logged once after each swapchain rebuild. It looks like the sleep mode is re-applied when the latency state is reset. So the driver may keep Boost even though nothing is paced.

The frame-generation-off run had Reflex On + Boost and the DLSS-G run had none, so the gap above includes both frame generation's held-back frame and Reflex's pacing. NVIDIA's own integrations (Streamline) require Reflex while DLSS-G is on, to win back exactly this latency, so it seems worth having here too.

A possible fix. As far as I can tell, Reflex is off under frame generation because its per-frame calls assume one present per game frame. Its calls fall into two groups, and only the second actually conflicts:

  • Game-frame calls, all on the presentation thread in do_present:

    • SIMULATION_END and RENDERSUBMIT_START (reflex_present_begins, vk_replay.cpp:2940)
    • the latency sleep after it, followed by SIMULATION_START and INPUT_SAMPLE (ReflexSleep, vk_replay.cpp:2941)
    • RENDERSUBMIT_END (reflex_submit_ends, vk_replay.cpp:3678)

    These still describe the game's real frames with frame generation on, and the sleep that paces them is where the latency gets won back.

  • Present calls: the present id and PRESENT_START/PRESENT_END (reflex_present_id, vk_replay.cpp:3704, and reflex_presented, :3724). These run in the present path the display thread also uses for made frames, so they would mark made frames as if they were game frames. This is the part that clashes with frame generation.

So on() could be split into two checks:

  • A game-frame check: the row is on, the device is ready and the swapchain has latency mode. It no longer depends on framegen_on(), and it gates the sleep and the simulation and render-submit markers.
  • A present check: the same, plus "this present carries the game's real frame". It's always true without frame generation. With frame generation it holds only for the present of the set's real frame, and made frames get no present id or present markers. It gates reflex_present_id and reflex_presented.

RX_VK_REFLEX_LOG=1 would show it working: timing reports with frame generation on. Rerunning the measurement above would show what it wins back. Anti-Lag 2 could probably get the same split. I haven't tried any of this, so take it as a direction, not a tested patch.

…d, made frames without present ids

Reflex was off whenever frame generation was on (on() required !framegen_on()), so its held-back frame
added its whole delay: on an RTX 5080 at 4x DLSS-G measured +12 to +18 ms from input to the screen against
frame generation off with Reflex on (PR 2118's review, breakthetargets, who also laid out this split).
NVIDIA's own integrations keep Reflex on with DLSS-G for this reason.

The calls that describe the game's frame stay on with frame generation: the latency sleep, the simulation
and input markers after it, the simulation end and submission start at do_present, and the submission's end
(reflex_game_submitted, once the set's recording is submitted). The present calls run in the present path
the display thread also uses for made frames, so they now know which frame a present shows:
- framegen_record keeps the game frame the set was made at (reflex_frame) with the set;
- the display thread sets reflex_present_frame per present: the set's game frame gets that frame's present
  id and its PRESENT_START/END markers, made frames get neither (a deferred present keeps it in its
  PendingPresent until present_commit);
- present ids only go up: one that would not (frame generation turned on or off with sets in flight) is
  left off; a new swapchain starts again;
- the display thread's copies are not the game's render submission.
Without frame generation nothing changes: reflex_present_frame is kReflexThisFrame, the frame being made.
The NVIDIA Reflex row's help says it now works with frame generation.

Tests: test_reflex_swapchain_lifetime.py: a wait that turns frame generation on still marks the frame; with
frame generation the game frame's markers, a made frame without present id or markers, the set's game frame
with its own id, an id that does not go up left off. present_source_crop_test.cpp stubs reflex_frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pmwork-personal

Copy link
Copy Markdown
Contributor Author

Thanks @Arkex-ai for the 5070 Ti run, and @breakthetargets for the latency numbers and the Reflex write-up. I pushed your split as a new commit here (feat(vk): NVIDIA Reflex with frame generation):

  • Kept with frame generation: the calls that describe the game's frame. That's the latency sleep, the simulation/input markers after it, simulation end + render-submit start at do_present, and render-submit end once the set's recording is submitted (reflex_game_submitted).
  • Present calls: each set keeps the game frame it was made at (reflex_frame()). On the display thread only the set's real frame is presented with that present id and PRESENT_START/END. Made frames get neither. A deferred (present-ahead) present carries it in its PendingPresent.
  • Present ids only go up. One that wouldn't (frame generation toggled with sets in flight) is left off, and a new swapchain starts again.
  • Without frame generation nothing changes. test_reflex_swapchain_lifetime.py covers the new cases.

On the 5090 (5120x2160, DLAA, Reflex On + Boost, same spot), Reflex's own timings (RX_VK_REFLEX_LOG=1) now show up under DLSS-G. Before, it wrote none:

game fps sim start → present end
frame generation off ~61 15–16.5 ms
DLSS-G 3x ~54 41–42 ms

That's Reflex's view, so it can't show what turning Reflex on under frame generation wins back: with Reflex off there's no timing to compare against. @breakthetargets, could you rerun your input-to-screen measurement on the new head? That's the comparison that counts.

Most of the remaining gap is the interpolation itself: the real frame is shown after its set's made frames, about ⅔ of a game frame later at 3x.

@nullifyshot

Copy link
Copy Markdown
Contributor

Thanks for the quick follow-up. I reran the same test on 3a39e3f, with the same PC, settings, spot and method as my comment above. This time RX_VK_REFLEX_LOG=1 was set in both modes.

Reflex now runs with DLSS-G. The DLSS-G run wrote 59 Reflex timing reports while DLSS-G made every set. Before this commit it wrote none.

Action DLSS-G 4x + Reflex (3a39e3f) Frame generation off, Reflex on Gap Gap before 3a39e3f
Camera flick 38.3 ms 25.3 ms +13.0 ms +12.1 ms
Sword (Y) 62.9 ms 47.6 ms +15.3 ms +17.4 ms
Walk 61.6 ms 48.9 ms +12.7 ms +17.7 ms

These are medians of 20 presses each. The walk gap shrank by about 5 ms and the sword gap by about 2 ms, but part of that may be run-to-run noise. Overall, the latency barely changed. Reflex's own timings show why:

Simulation start → present end Simulation Game's own frames/s
Frame generation off 10.1 ms 9.5 ms ~97
DLSS-G 4x 20.9 ms 12.0 ms ~77
  • Off: a frame reaches its present about 0.6 ms after its simulation ends. This scene isn't GPU-bound, so there's no queue for Reflex's sleep to drain, with or without frame generation.
  • DLSS-G: the extra ~11 ms is the game frame held back while the set's made frames are shown first. That's built into interpolation, and Reflex can't remove it. Each game frame also takes longer (12.0 vs 9.5 ms of simulation, ~77 vs ~97 fps). If the presentation thread is waiting on frame generation's work, that may be worth a look too.

So the change looks right. It should pay off in GPU-bound scenes, where frames do queue up: higher resolution, heavier settings or a slower card.

One thing looks off: the GPU-end timing with frame generation. With frame generation on, Reflex's "to the GPU's end" reads 243–442 ms, next to about 21 ms from simulation start to present end. With frame generation off it reads a sensible 12.1 ms. It looks like the driver is pinning the GPU's end of a frame on later work.

A possible fix (untested, just a direction). Hookshot never chains VkLatencySubmissionPresentIdNV to its queue submits. That's VK_NV_low_latency2's way of telling the driver which frame a submission's GPU work belongs to. Without frame generation the driver seems to infer it correctly. With frame generation, the display thread's copy and DLSS-G submits sit between the game's frames, and it probably gets them wrong. What I'd try is tagging every submit through one helper, much like reflex_present_id, for example reflex_submit_id(const void* next):

  • The game's rendering: submit_recording, including the batched path through queue_flush_locked, and the parallel recording jobs' submit (vk_replay.cpp around line 1555). Chain VkLatencySubmissionPresentIdNV with the frame being made (r.id while frame_marked, between its simulation start and its render-submit end).
  • present_image's submit (around line 3682): use the same rule as the present id. That's r.id without frame generation, the set's game frame (reflex_present_frame) for the present that carries it, and no tag for made frames. That way the made frames' copy and DLSS-G work isn't counted as the next game frame's rendering.

If this is the cause, RX_VK_REFLEX_LOG=1 should show "to the GPU's end" back below "simulation start to present end" with frame generation on, as it is with frame generation off. The driver would then also be pacing on correct GPU timings. I haven't tried this, so it's a lead to check, not a confirmed diagnosis.

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this. It's focused, it builds alone, CI is green, and it has been tested on three RTX 50 cards (Arkex-ai's 5070 Ti run, nullifyshot's latency numbers, and the Reflex follow-up you added after them).

Overlap: #1831 adds the same feature (DLSS-G through NGX, on DLSS's session). Only one of them should land, so the two need to converge. Your description already says you're open to folding this into either.

Compared against #1831's diff, this PR:

  • feeds DLSS-G the temporal chain's motion, actor motion included. #1831 uses FSR 3.1's assembled inputs, which have camera motion only.
  • reads MultiFrameCountMax and leaves sets it can't make to the built-in method. #1831 hard-codes 3 made frames and quietly makes a 4x set at 5x to 8x.
  • waits for the queue to go idle before releasing the feature and defers freeing its images. #1831 releases both straight away on a resize.
  • presents 3x/4x sets ahead and keeps Reflex on under frame generation. Neither is in #1831.
  • builds alone at about an eighth of the size. #1831 needs #1833 and #1834.

What #1831 does better, and could be adopted here:

  • Its DLSS-G doesn't need a temporal upscaler. Its inputs are made at the gameplay upscale "whatever upscales" (its own comment), so it isn't tied to the Upscaler row. Here it needs a temporal Upscaler on.
  • Its method choice is a small pure header (framegen_methods.h) with a unit test. The new Frame generation by row here has no test of its own beyond the row order.
  • It asks NGX for frame generation's device extensions only when frame generation is wanted (see the inline note on gating).
  • Its single/pair/no-weight actor-motion layouts. You suggested those as their own PR, and I agree.

Inline notes below. Still open from the thread: nullifyshot saw Reflex's "to the GPU's end" read 243 to 442 ms with frame generation on, and suggested tagging submits with VkLatencySubmissionPresentIdNV. That's fine as a follow-up, but please say so in the description so it isn't lost.

The decision on which of the two lands stays with @NotRexed.

Comment thread gpu/vk/vk_dlss.cpp

bool dlss_fg_wanted() { return fg_asked(); }

void dlss_fg_device_extensions(std::vector<const char*>& want, const std::vector<VkExtensionProperties>& all) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This calls NVSDK_NGX_VULKAN_GetFeatureDeviceExtensionRequirements at every device creation in a DLSS build, on AMD and Intel GPUs too and with frame generation off. Main's dlss_device_extensions and dlss_probe avoid NGX unless the device has the NVX extensions (DLSS_NEEDS_RTX). Could this get the same gate, i.e. skip when VK_NVX_binary_import/VK_NVX_image_view_handle aren't in all? fg_start could also return early when dlss_why() says the device isn't NVIDIA's, so the first 2x present on an AMD card doesn't run ngx_init.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 90c4683 (fix(dlss): DLSS-G only where it runs). dlss_fg_device_extensions now returns before NGX when the device lacks VK_NVX_binary_import or VK_NVX_image_view_handle, the same check as dlss_device_extensions. fg_start returns before ngx_init when dlss_why() isn't DLSS_OK, so the first 2x present on an AMD card doesn't start NGX. On an NVIDIA device NGX is still asked with frame generation off, so the Frame generation by row works without a restart.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed in 90c4683, thanks: both the extension query and fg_start are now gated.

Comment thread gpu/vk/vk_main.cpp Outdated
// (hookshot) V-Sync Auto with frame generation at 2x and up: FIFO. Mailbox, NVIDIA's Fast Sync, puts the made frames on the
// refresh grid whenever they come slower than the display (DLSS Frame Gen 3x: ~135 shown on a 165 Hz screen), so evenly
// timed presents still reached the screen unevenly; FIFO on a VRR display shows each one when it is presented
const bool fg_fifo = vsync == 0 && swapchain_framegen() >= 2;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This changes V-Sync Auto for every GPU and for the built-in frame generation too, not only DLSS-G. On Windows it also skips the immediate fallback that the comment below keeps for AMD ("FIFO held anything under 60 fps at 30"). The description lists fixed-refresh displays as untested. Could this be limited to where it was measured (DLSS-G running, or at least NVIDIA), or else tested at 2x on a fixed-refresh/AMD setup?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Limited to DLSS-G, where it was measured (90c4683). swapchain_dlssg() is true only at a fixed multiplier with DLSS-G asked for, on a device it runs on, and NGX not having said no. The built-in frame generation and other GPUs keep Auto as on main, including the Windows immediate fallback for AMD. The swapchain is made again when that changes. test_vsync_row now covers both cases. A fixed-refresh display is still untested, as the description says.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed: FIFO is now for DLSS-G only, and the built-in method and AMD keep main's Auto. Thanks.

Comment thread gpu/vk/vk_dlss.cpp
static int told = 0;
if (told++ < 8) logf("vk: DLSS Frame Gen (NGX): frame %d of %d was refused (%#x)", k, count, (unsigned)r);
g.last_sequence = 0;
return g.refused++, 0; // (a half-made set: the renderer's own makes all of it)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: when frame k>1 is refused, frames 1..k-1 have already been written by NGX into out[], and the built-in passes then write the same images in the same command buffer. No barrier is recorded on this path, so that's a write-after-write without a dependency. A fg_barrier(cmd) before returning would cover it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 90c4683: fg_barrier(cmd) before returning when frame k > 1 is refused.

Comment thread gpu/vk/vk_framegen.cpp
// (hookshot) NVIDIA DLSS Frame Generation through NGX where it runs and the Frame generation by row asks for it (vk_dlss.cpp):
// every made frame of the set, from the scene's own depth, motion and camera. A set it does not make goes on below as before
// (asked every present: turned off, it lets its feature go)
if (m > 1) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the feature is only let go when the Frame generation by row changes. With Frame generation set to Off or Auto, dlss_fg_frames isn't called anymore (m > 1), so DLSS-G's feature (large at 5K) stays allocated for the rest of the run. Releasing it when the multiplier drops below 2 would match the row's "changes at once".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 90c4683: dlss_fg_idle() runs at each present without a fixed multiplier (Off or Auto) and lets the feature go if one is held. When there's nothing to release it's one atomic load.

…let go at Off or Auto

From the review:
- NGX is not asked for frame generation's device extensions on a device
  without NVIDIA's NVX extensions (AMD, Intel), and fg_start does not start
  NGX where dlss_why() says DLSS does not run.
- V-Sync Auto's FIFO under frame generation is limited to DLSS-G making the
  sets (swapchain_dlssg), where it was measured; the renderer's own methods
  and other GPUs keep Auto's mailbox or immediate. The swapchain is made
  again when that changes.
- A frame NGX refuses after the first: a barrier before the renderer's own
  passes write the same images.
- Frame generation at Off or Auto lets DLSS-G's feature go (dlss_fg_idle),
  as a change of the Frame generation by row does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pmwork-personal

Copy link
Copy Markdown
Contributor Author

Thanks for the review. The four inline points are fixed in a new commit, 90c4683 fix(dlss): DLSS-G only where it runs (replies in each thread). The Reflex submit tagging (VkLatencySubmissionPresentIdNV) is now in the description as a follow-up.

From #1831's side:

  • Method choice as a pure header with a unit test: I can move the Frame generation by choice into a small header with a test here, if that helps the review.
  • DLSS-G without a temporal upscaler: here its inputs come from the temporal chain, because that's where the actor motion is. DLAA (the Upscaler's Native) covers the no-upscaling case. Decoupling it could be a follow-up.
  • Single/pair/no-weight actor-motion layouts: I'll open them as their own PR next, with credit.

Agreed that the choice between this and #1831 is @NotRexed's.

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, re-reviewed at fb09afc (90c4683 plus a merge of main).

All four earlier points are fixed:

  • NGX isn't asked for frame generation's device extensions without VK_NVX_binary_import/VK_NVX_image_view_handle, and fg_start stops before ngx_init when dlss_why() isn't DLSS_OK.
  • V-Sync Auto's FIFO is now for DLSS-G only (swapchain_dlssg()), so the built-in frame generation and AMD keep main's behaviour, including the Windows immediate fallback. test_vsync_row covers both cases, and the swapchain is rebuilt when it flips.
  • A refused frame k > 1 now gets fg_barrier(cmd) before the built-in passes write the same images.
  • dlss_fg_idle() lets the feature go at Off/Auto. It runs on the presentation thread, where dlss_fg_frames also runs, and takes g.mu then present_mu in the same order, so I don't see a lock-order problem.

The Reflex submit tagging (VkLatencySubmissionPresentIdNV, nullifyshot's GPU-end reading) is now in the description as a follow-up, which is what I asked for.

One nit, inline: after three failed feature creations, dlss_fg_runs() still says yes, so V-Sync Auto stays FIFO while the built-in method makes the sets. That's an edge case and doesn't block.

From #1831, worth adopting. #1831 is being closed in favour of this PR. These are FIocker's ideas, and none of them blocks this one. They would be good follow-ups, with credit:

  • DLSS-G without a temporal upscaler. #1831 makes camera-only motion from depth (vk_camera_motion), so its DLSS-G runs with the Upscaler off. Here DLSS-G needs DLSS, DLAA or FSR. You've said this could be a follow-up, and that's fine.
  • The method choice as a small pure header with a unit test (framegen_methods.h). In #1831, an unknown saved value reads as Auto, and the row only offers methods that run on this PC. Here, a saved out-of-range value clamps to Built-in, and an AMD card sees Auto and Built-in, which do the same thing. You offered to do this, and I'd take it, here or in the follow-up.
  • Actor motion. This PR is already ahead of #1831 here: its DLSS-G gets the chain's actor motion, while #1831's gets camera motion only. What #1831 adds is wider skinning coverage in dlss_actor.vert/bones_used (the single, pair and no-weight layouts: Link's shield and hair, rigid props). That helps DLSS and DLSS-G alike, and you plan it as its own PR.
  • Already done, nothing to adopt: sharing DLSS's NGX session (this PR does it too) and asking for NGX's frame generation extensions only on NVIDIA devices (90c4683).

CI on fb09afc was still queued when I reviewed, so please merge once it's green. Approving.

Comment thread gpu/vk/vk_dlss.cpp

bool dlss_fg_wanted() { return fg_asked(); }

bool dlss_fg_runs() { return fg_asked() && dlss_why() == DLSS_OK && !fgs().start_failed.load(std::memory_order_relaxed); }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: after create_failures >= 3, dlss_fg_frames refuses every set, but this still returns true, so V-Sync Auto stays FIFO while the built-in method makes the sets. Setting start_failed (or a separate flag) once creation gives up would hand Auto back to main's mode.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7c13821: once creation gives up (create_failures >= 3), start_failed is set, so dlss_fg_runs() says no and the swapchain goes back to V-Sync Auto's mode without DLSS-G. Main is merged in again too (b371359), for the conflict with #2042's menu rows.

pmwork-personal and others added 2 commits November 11, 2026 11:39
…back to its mode

From review: after create_failures >= 3 dlss_fg_frames refuses every set,
but dlss_fg_runs still said yes, so V-Sync Auto stayed FIFO while the
built-in method made the sets. Giving up now sets start_failed, which
dlss_fg_runs reads; present_image makes the swapchain again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pmwork-personal

Copy link
Copy Markdown
Contributor Author

Thanks for the approval. Main is merged in again (#2042 touched the Reflex row), and the nit is fixed in 7c13821. I'll take the method choice as a small header with a unit test in the follow-up, together with DLSS-G without a temporal upscaler, and the single/pair/no-weight actor-motion layouts as their own PR, with credit to FIocker.

…-ngx

# Conflicts:
#	gpu/vk/vk_framegen.h
#	gpu/vk/vk_internal.h
#	gpu/vk/vk_main.cpp
#	gpu/vk/vk_replay.cpp

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-checked at fd6aaa5. The only code change since my approval is 7c13821 (the rest are main merges). Setting start_failed once creation gives up at three failures makes dlss_fg_runs() say no, so V-Sync Auto goes back to its normal mode. That's what my nit asked for, and it matches the existing "off" log line (it was already permanent after three failures). Thanks for taking the #1831 ideas on as follow-ups.

CI is still running on this head after the main merges (#2042 touched the Reflex row), so it's worth waiting for green before merging.

@hckrman101
hckrman101 merged commit c51df0b into NotRexed:main Nov 11, 2026
11 of 12 checks passed
unixman69 added a commit to unixman69/Hookshot that referenced this pull request Nov 11, 2026
NotRexed#2118's DLSS Frame Generation (NGX) copies the chain's depth each frame and
told NGX it was not reversed. With FSR 3.1 on the chain the depth is 1 - d
(dlss_prep.comp bit 3), so generated frames under FSR 3.1 would have had
their occlusion backwards. The frame's depth_reversed now goes with the
copied inputs (Fg::depth_reversed) and into
NVSDK_NGX_DLSSG_Opt_Eval_Params::depthInverted. DLSS and FSR 4 are
unchanged (their depth is not reversed).

test_upscaler_rows checks both lines; it fails with the hard-coded false.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
…ters' motion (#2305)

AMD FSR 3.1 makes its own inputs (`vk_fsr3_inputs.cpp`), and the only
motion it gets is the camera's. So anything that
moves differently from the camera is given the motion of what's behind
it: Link while he walks, his swinging arm, his
shield, and the people around him. DLSS and FSR 4 already get those
characters' own motion on `vk_temporal.cpp`'s chain.
This PR puts FSR 3.1 on that chain too, as a third engine, so all three
temporal upscalers share the same inputs.

**What changed**
- `vk_fsr3.cpp/.h`: `kChainEngine`, FSR 3.1 as a `TemporalEngine` (slot
2).
- It takes the chain's colour, depth, motion (with the actor pass drawn
over it), jitter and jitter phase count.
  - It builds FSR's depth constants from the unjittered projection.
- Its picture is copied into the chain's output and squared back by
`dlss_out.comp` as before.
- `fsr3_chosen()` is "the row or `RX_VK_FSR=1` picks FSR 3.1".
`fsr3_wanted()` now means "FSR 3.1 on its own inputs",
    which is false while it's on the chain.
- `fsr3::prepare_sizes` and `fsr3::release` let the chain size and free
FSR's images.
- `vk_temporal.cpp/.h`:
- `temporal_mode` returns `FSR3` for the row's FSR 3.1 before it falls
back to the game's own; `engine_of` gets a third
    slot.
  - Two new `TemporalEngine` fields:
- `fsr_depth`: the chain reverses depth for FSR's passes (built with
`INVERTED_DEPTH`) where the projection puts the
      near plane at 0.
- `retry`: a failed evaluation hands that frame to the network instead
of turning the engine off for good. FSR 3.1's
constants come from the frame's stream, which can run out for one frame.
- `TemporalFrame` carries the unjittered projection and the jitter phase
count.
  - The chain's output image gets `TRANSFER_DST`.
  - `RX_VK_FSR_JITTER=0` still turns FSR 3.1's jitter off on the chain.
- `shaders/dlss_prep.comp`:
  - Flag bit 3 reverses depth (`1 - d`).
- A NaN in the scene colour is written as black, following
`fsr3_prep.comp`'s rule, because one NaN spreads through an
    upscaler's history.
- The header was regenerated with `glslangValidator -V --target-env
vulkan1.3 --vn DLSS_PREP_COMP_SPV`, the command
    `test_dlss_actor.py` checks.
- `RX_VK_FSR3_CHAIN=0` gives FSR 3.1 its own inputs, as before, for
comparisons (`docs/development.md`). The readme's FSR 3.1
section describes the new inputs, and says what still trails: things
that aren't skinned, such as Navi's body.
- Tests:
- `temporal_jitter_test.cpp` covers FSR 3.1's slot and its failure gate,
and a new run (`fsr-jitter-off`) where only
`RX_VK_FSR_JITTER=0` is set: FSR 3.1 on the chain gets no jitter, and
DLSS and FSR 4 keep theirs.
- `test_upscaler_rows.py` checks that the row's FSR 3.1 runs on the
chain before the fallback to the game's own, that
`fsr3_wanted` is off while it does, and that `engine_of` has its slot.
  - Both fail on main. A mutated jitter gate fails `fsr-jitter-off`.

**Testing**
- `python -I -B tests/run_all.py`: 363 passed, 0 failed, 11 skipped
(Linux; the skips are Windows-only, need a Vulkan 1.3
  device, or need dotnet).
- In game: RX 9070 XT 16 GB, Mesa RADV 26.2.6, Gentoo Linux (kernel
7.3.3), KDE Plasma Wayland, windowed, 2560x1440
  output, 60 fps cap.
- Host v2.1.0-main-local (source fc2ed0d0). Renderer: main 49f75dc plus
this branch.
- "Own" means the same build with `RX_VK_FSR3_CHAIN=0`, which is what
main does.
- Every chain run logged `vk: fsr3 on the chain: 600 frames drawn ...
depth reversed for FSR by the chain`, with no
    evaluation failures.

| FSR 3.1 | Kokiri Forest, 28 s camera turn (4 runs each) | Castle Town
market at noon (3 runs each) |
  |---|---|---|
  | Own inputs (as main) | 59.2 fps, 1% low 46.4 | 53.1 fps |
  | On the chain | 59.3 fps, 1% low 48.1 | 52.5 fps |

Market runs vary by about 0.9 fps between runs, so the chain costs
nothing measurable.
- Picture: consecutive 2560x1440 shots at Kokiri's lost-woods path,
standing and walking, compared on the same build.
Edge flicker is the mean frame-to-frame change of edge pixels, 0-255,
while standing.

| FSR 3.1 | Edge flicker, standing | 1-px outlines per MP | Halos per MP
|
  |---|---|---|---|
  | 100%, own | 2.17 | 32 | 45 |
  | 100%, chain | 2.20 | 29 | 50 |
  | Quality (67%), own | 3.01 | 12 | 25 |
  | Quality (67%), chain | 1.67 | 2 | 9 |

Four independent reviewers of the walking sheets saw no visible
difference at 100%. At 67%, they found the chain
slightly better: smoother edges on Link's shield and tunic hem, and
slightly softer shield texture. No new artefacts.
The clearest evidence is the motion field itself (below). On main, Link
isn't in it at all.

**Not tested:**
- other GPUs and drivers;
- Windows;
- Steam Deck, where #2110 would make FSR 3.1 the default;
- frame interpolation (`RX_VK_FSR_FG=1`), which keeps using
`vk_fsr3_inputs.cpp`;
- switching upscalers repeatedly while playing (release and remake are
code-reviewed, not exercised in game);
- HDR: the chain still clamps FSR 3.1's colour at the paper white, as
main does (see below).

**Screenshots** (from the game's output image before the present path,
2560x1440)

The motion vectors the upscaler gets while Link walks
(`RX_VK_DLSS_DEBUG=motion`). Left: camera motion only
(`RX_VK_DLSS_ACTORS=0`, which is what FSR 3.1 got on main). Right: on
the chain, with Link's body, arm and shield carrying
their own motion:

![motion
vectors](https://raw.githubusercontent.com/unixman69/Hookshot/pr-screenshots/fsr3-chain/motion-vectors.png)

Link walking: own inputs vs the chain, at 100% and at 67% (2x zoom
crops):


![walking](https://raw.githubusercontent.com/unixman69/Hookshot/pr-screenshots/fsr3-chain/link-walking-own-vs-chain.png)

**Merge notes** (from test merges against every open PR that touches
these files; textual conflicts show up in git, semantic ones don't)
- **Depth is reversed for FSR 3.1 on the chain.**
`TemporalFrame::depth_reversed` says so. Anything that reads the chain's
  depth must check it rather than assume plain depth:
- #2118 (DLSS frame generation through NGX, now on main): done in this
PR. Commit ca2e302 passes `depth_reversed` to
    NGX's `depthInverted`, and `test_upscaler_rows` checks it.
- #1944 (DLSS frame generation through Streamline) needs the same:
`depthInverted` from `depth_reversed`, not a
    hard-coded false.
- #1831/#1833: `nr_pre` passes `s.depth` on, and the NGX backend's
`DepthInverted` should follow it too.
- **#2145** adds `bool native_aa` to `TemporalEngine::prepare`.
`chain_prepare` in `vk_fsr3.cpp` needs the same
parameter, or the build breaks. git merges the two cleanly. FSR 3.1
never has native AA.
- **#1653** (reactive mask) is on main now and merged into this branch.
Commit 7ceae45 makes the mask in `chain_evaluate`
the same way DLSS does on the chain, and `test_reactive_mask.py` checks
it. Checked in game with `RX_VK_REACTIVE=1`: the mask is
  made, and FSR 3.1 draws all 600 frames on the chain.
- **#2243** (DLSS HDR, linear light) is on main now (in #2342's squash,
a0dfd28) and is merged into this branch.
- `dlss_prep.comp` scrubs NaN first, then applies #2243's
`max`/`kSquare`/`min`, and keeps the `flags & 8` depth flip.
The header was regenerated; glslang 16.4.0 gives the same SPIR-V words
as CI's 16.6.0 for `dlss_prep` and `dlss_out`.
- `TemporalEngine` is ordered `memory`, `hdr`, `fsr_depth`, `retry`:
DLSS's and FSR 4's engines are positional,
    and `kChainEngine` is designated.
- FSR 3.1 doesn't report `hdr` yet, so it keeps the LDR pair. Giving it
HDR colour on #2243's mechanism is a follow-up.
- **#1831**:
- It reads `fsr3_wanted()` to ask whether FSR 3.1 is chosen. After this
PR that question is `fsr3_chosen()`. Its
`fsr4 = !fsr3_wanted() && fsr4_wanted()` guard and its jitter guard
should use `fsr3_chosen()`. The merge is
    textually clean, so git won't flag it.
- In `vk_temporal.cpp`, the output image's usage should become
`TRANSFER_DST | TRANSFER_SRC`, and its `proj[16]` and
    this PR's `projection[16]` should become one field.
- **#2166** only matters with `RX_VK_FSR3_CHAIN=0`.
- `docs/tested-configurations.md` and `docs/development.md`: rows next
to other PRs' rows; keep both.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
notquiteog added a commit to notquiteog/Hookshot that referenced this pull request Nov 11, 2026
… the fork's side

Upstream's 73 commits since the last merge, with its NVIDIA DLSS Frame
Generation through NGX, taken as it is: the fork's own vk_dlssg.cpp and
dlssg_math.h are gone. Upstream measured in the game that NGX takes motion
in render pixels, which the fork's version had wrong from the headers.

Frame generation: upstream's Frame generation by row (Auto, Built-in)
keeps its key and values, and the fork adds AMD FSR 4 (vk_ffx.cpp) and
AMD FSR 3.1 after them where they run. Neither is ever Auto's: both are
given the picture with the HUD in it. A set a method does not make whole
is the renderer's own.

Neural rendering: runs once for each of the game's frames, before frame
generation, on the picture's part of the window image alone (it was run
over all 2560x1440 of a 2048x1152 picture), and is left out of a native
HDR picture. Two rows the RenoDX DLSS add-on has are new: Passes (1 to
10, a feature or a Streamline viewport a pass, local tone on the first
alone) and Resolution scale (50 to 100%: the model on a smaller copy,
its detail added back at full size, shaders/nr_detail.comp).

Every fork backend that hands the command buffer to someone else's code
goes through the recorder (B_DIRECT before, note_work after), and
tests/test_recorder_sync.py checks each with a control.

Run in the game on an RTX 5060 Ti, Linux (docs/upscaling.md): each
upscaler that exists there, DLSS Frame Generation at 2x to 4x, Built-in,
AMD FSR 3.1's interpolation, and the open neural rendering network with
and without frame generation and at 50% scale. Windows-only code (AMD
FSR 4, Streamline) and NVIDIA's neural rendering model, which has no
Linux runtime, are compiled for Windows and not run.

Tests: 413 passed, 12 skipped (Windows-only or missing tools).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
The Graphics tab is one list of 24 settings, with the window, HDR,
upscaling, frame generation and GPU options mixed
together, and about ten more are on their way in open PRs. This puts its
rows under section titles, so the upscaler, frame
generation or HDR can be found without scrolling the whole list. It is
the first step of a menu organization laid out below
for discussion.

**What changed**
- The Graphics rows in six sections, under heading rows like the Debug
tab's Developer (a gold title, never selected,
  nothing to click):
  - Quality preset (first: it sets rows in every section)
- **Resolution and upscaling:** Output resolution, Profile, Upscaler,
DLSS model, Render resolution, Sharpness
- **Frame generation and latency:** Frame generation, Frame pacing, Game
frame rate cap, NVIDIA Reflex, AMD Anti-Lag 2
- **Screen:** Fullscreen, V-Sync, Window scaling filter, Aspect ratio
(#2119), Ultrawide menus
- **HDR:** HDR, HDR paper white, HDR peak brightness, HDR contrast
(#2342), HDR tone mapper (#2481)
  - **Detail:** Anisotropic filtering
- **GPU and memory:** GPU device, VRAM budget (#1859), Mesh shaders
(Robust buffers is on Debug ▸ Developer since #2042)
- Moves only: every row's definition is unchanged, word for word; no row
is renamed and no value or saved key changes, so
options files carry over, and #1864's tables (keyed by the English text)
still find every row. They need the six
  headings.
- No new drawing or input code: the menu already draws heading rows on
any tab, and the keys, the pad and the mouse step
  over them.
- `readme.md`: a sentence on the sections, and the option table in the
new order. (Ultrawide menus and Mesh shaders have
no line yet; #2105 adds them.) The **Frame generation by** row from
#2118 gets its line in the table here too: main's
readme has none yet. So do #1859's **VRAM budget** (under GPU and
memory) and #2342's **HDR contrast** (under HDR).
- `tests/test_upscaler_rows.py`: its order list follows the picture's
path (output, upscaler, render size, sharpening,
then the frame rows, then the window). New
`tests/test_menu_sections.py`: the Graphics sections and which rows they
hold,
and on every tab no section that `row_shown` could leave empty (a title
over nothing on a GPU without Reflex, say).

**The plan this is step 1 of (for discussion)**

Each tab answers one question; a section groups the rows you set
together; a row that depends on another follows it; vendor
rows sit beside their counterpart and hide where they can't run (no
vendor sections, so nothing is left empty on AMD); rows
to touch only when something is wrong go last; moves only, no renames.

| Tab | Holds | Sections |
|---|---|---|
| **Game** (was Gameplay; first, where the menu opens) | how the game
behaves and reads, and Restart and Quit, as a pause menu | flat; Restart
and Quit last |
| Graphics | how the picture is made, and what it costs | Quality preset
· Resolution and upscaling · Frame generation and latency · Detail · GPU
and memory |
| **Display** (new) | how it gets to your screen, and what is drawn over
it | Screen · HDR · Overlays (FPS counter, Menu size, from Game) |
| Audio | sound out and in | + Microphone (from Game: it picks the input
device) |
| Controls | how you play | Camera · Controller · Buttons (optional) |
| Debug | tools and developer switches | Robust buffers in Developer
(#2042, merged) |

Next PRs, one topic each: (2) a Display tab, taking the Screen and HDR
sections over whole and adding Overlays; (3) the
Gameplay tab titled Game, first in the strip, where the menu opens; (4)
Microphone to Audio; (5) optional, sections on
Controls. If a sixth tab is a no, this PR stands on its own.

Open questions:
1. A sixth tab (Display)?
2. V-Sync on Display (the PC convention) or with frame generation (it
changes with it)?
3. Sharpness with the upscaling rows (where players look for it) or with
the window filter (where it runs)?
4. DLSS 5 (#1590) and #1834's Neural rows as a page opened from a
Graphics row, like Motion, rather than a seventh tab?

<details><summary>Where open PRs' rows would go</summary>

- Resolution and upscaling: Sharpening method (#1979), DLSS 5 page
(#1590), Native 3D resolution (#2017)
- Frame generation and latency: Frame generation by (#2118, merged: in
this section now), Frame generation method (#1834)
- Detail: Shadow resolution (#1892), Screen filter and Filter strength
(#1488)
- GPU and memory: Texture streaming cache (#2017), Refresh memory
(#888); VRAM budget (#1859, merged) is in this section now
- Game (was Gameplay): Game language and Menu language (#1864), Skip
title screen (#1560), Amiibo (#1521)
- Controls: Camera speed (#1945)
- #2042 (merged) moved Robust buffers to Debug ▸ Developer.
- #2105: its readme check would read the heading rows as rows; it needs
to skip `{TAB_..., "...", nullptr, 0, 0, 0, ...,
  true}` headings, or this PR adds that if it lands first.

Whichever PR lands second moves one row on rebase; happy to do it for
anyone.

</details>

**Testing**
- `tests/run_all.py` on Windows 11 (llvm-mingw clang, 8 at once) after
merging main at 2dfe901 (with #2119's Aspect ratio, #2481's HDR tone
mapper, #2118's Frame generation by,
#2342's HDR contrast and #1859's VRAM budget): 422 passed, 33 skipped, 2
failed, both outside this PR:
`test_vram_budget_row.py` (fails the same on main's own sources here)
and `test_portable_builder_startup.py` (its
`dotnet publish` needs packages this PC does not have). A build of the
branch compiles. All 81 of main's rows keep
main's definitions word for word; the only additions are the six
headings.
`test_menu_sections.py` fails on main as it stands (no sections) and
passes here; a layout with a section of Reflex and
  Anti-Lag 2 alone fails it.
- A build of this branch (Windows 11, RTX 5090), headless with scripted
menu keys at Small, Medium and Large: 12 Downs
from the Quality preset land on V-Sync and 13 Ups come back past it to
the last row, so every heading is stepped over
both ways and Up from the first row wraps to the last; the help box
always shows the selected row (at the first head,
when Robust buffers was the last row). Again at this head (25feb16,
after #2042, #1859 and #2119): Up from the Quality preset
wraps to Mesh shaders, the last row now, under VRAM budget (the bottom
screenshot).

**Not tested**: in a window with a pad, the mouse wheel and a click just
under a heading (the headless runs drive
the keys); values kept across a restart (no row's saved key changes);
AMD and Intel (their rows hide through `row_shown` as before; the new
check makes sure no section can end up
empty), Linux.

**Screenshots**

The Graphics tab at 1920x1080, Medium menu size: the top, then scrolled
with the keys to V-Sync and to the last row
(the selection steps over every heading; Up from the first row wraps to
the last row). The bottom one is retaken at this
head (25feb16, a 16:9 picture: `HOS_ASPECT=1.7778`, as on a 16:9
monitor); the others are from the first head (634ef65).

![Graphics tab, top: Quality preset, then Resolution and upscaling and
Frame generation and
latency](https://raw.githubusercontent.com/pmwork-personal/Hookshot-pr/pr-screenshots/menu-sections/graphics-sections-medium-1.webp)

![Graphics tab, middle: Frame generation and latency, then Screen,
V-Sync
selected](https://raw.githubusercontent.com/pmwork-personal/Hookshot-pr/pr-screenshots/menu-sections/graphics-sections-medium-2.webp)

![Graphics tab, bottom: Screen with Aspect ratio, HDR, Detail, then GPU
and memory with GPU device, VRAM budget and Mesh shaders, Mesh shaders
selected](https://raw.githubusercontent.com/pmwork-personal/Hookshot-pr/pr-screenshots/menu-sections/graphics-sections-medium-3c.webp)

Small and Large:

![Graphics tab at Small menu
size](https://raw.githubusercontent.com/pmwork-personal/Hookshot-pr/pr-screenshots/menu-sections/graphics-sections-small.webp)

![Graphics tab at Large menu
size](https://raw.githubusercontent.com/pmwork-personal/Hookshot-pr/pr-screenshots/menu-sections/graphics-sections-large.webp)

_Taken from headless runs of this branch (`HOS_HEADLESS=1`,
`RX_VK_SHOT_MENU=1`, the keys from `RX_VK_MENU_SCRIPT`), RTX
5090. Headless, the list never scrolls on main either:
`menu_record_image` leaves `ease_step` at 0, so the build behind
the scrolled shots had one more line there (`ease_step = 1.0f`, hence
"-dirty" in the first head's build label), not part of
this PR.
The windowed menu eases as before._

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
… its GPU work belongs to (#2499)

Follows @nullifyshot's lead on #2118
([comment](#2118 (comment))).
Under DLSS frame generation, Reflex's own "to the GPU's end" read
243–442 ms there (RTX 5080, 4x), next to ~21 ms from simulation start to
present end. Hookshot chained no `VkLatencySubmissionPresentIdNV` to its
queue submits, so the driver had to guess which frame each submit's GPU
work belongs to. Without frame generation it guesses right. With it, the
display thread's copies and DLSS-G's work sit between the game's frames
and get counted as the next game frame's rendering.

**What changes**

`reflex_submit_id(next, frame)` (vk_reflex.cpp) chains a
`VkLatencySubmissionPresentIdNV` to a `VkSubmitInfo2` (vk.xml:
`structextends="VkSubmitInfo,VkSubmitInfo2"`). It is used at the four
submit sites in vk_replay.cpp:

- **`submit_recording`'s direct submit and the parallel segment jobs'
submit:** the frame being made.
- **A batch (`queue_flush_locked`):** the frame of the batch's first
command buffer, kept in `pend.reflex` when the batch starts.
- **`present_image`'s copy:** the frame it shows, by the same rule as
the present id: the set's game frame (`reflex_present_frame`), no tag
for a made frame, and the frame being made without frame generation.

There is no tag when Reflex is off, when no frame is marked, or when
that present id was presented already (present ids only go up). Each tag
is taken right before its `vkQueueSubmit2`, after any flush, and that
flush tags its own batch. `RX_VK_REFLEX_SUBMIT_IDS=0` leaves the tags
off, for an A/B comparison.

**In game:** RTX 5090, 5120x2160 swapchain, DLAA and DLSS-G 3x at
4416x1864 (600 of 600 presents made by DLSS-G), V-Sync Auto, Reflex On +
Boost, `RX_VK_REFLEX_LOG=1`, Kokiri Forest. Same build both runs; run 2
only adds `RX_VK_REFLEX_SUBMIT_IDS=0`.

| | Simulation start → present end | To the GPU's end | Game fps |
Reports |
|---|---|---|---|---|
| Tags on | 40–51 ms | **46–56 ms** | ~44–47 | 13 |
| Tags off (`RX_VK_REFLEX_SUBMIT_IDS=0`) | 44–59 ms | **425–1060 ms** |
~42–44 | 5 |

With the tags, "to the GPU's end" lands a few ms after present end, the
same pattern as without frame generation (12.1 vs 10.1 ms in
nullifyshot's run), so the driver now paces on real GPU timings. The
tags-off run also felt worse to play. Its simulation-to-present numbers
are a bit higher too, but it was a short run (5 reports), so I wouldn't
read much into that gap yet.

What this doesn't measure: input to screen. @nullifyshot, if you have
time, your `HOS_INPUT_LIVE` method on this head would show what it's
actually worth. The ~⅔ of a game frame that interpolation holds the real
frame back stays as it was; Reflex can't remove that.

**Tests:** `test_reflex_swapchain_lifetime.py` copies `reflex_submit_id`
and gains 8 checks (the frame being made, a kept frame, frame 0, no
frame marked, one already presented, Reflex off, retired before the
lock). `draw_stream_visibility_test.cpp`,
`fsr_image_retirement_test.cpp` and `parallel_init_order_test.cpp` get
stand-ins. `tests/run_all.py` (Windows, llvm-mingw clang++): 391 passed,
31 skipped, 1 failed: `test_vram_budget_row`, which reads only
`vk_menu.cpp`, `vk_menu.h` and `vk_memory.cpp` (not touched here) and
fails the same way on main.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
liska1304 added a commit to liska1304/Hookshot that referenced this pull request Nov 12, 2026
Frame generation no longer turns Anti-Lag 2 off. As NVIDIA Reflex's
markers with frame generation (NotRexed#2118), each set keeps the game frame it
was made at (antilag_frame), and only the set's game frame gives that
frame's present stage (antilag_present_frame, kept by a deferred present
until its commit); the made frames' presents give none. Without frame
generation nothing changes. The row's hint takes the Reflex row's
wording and translations.
hckrman101 pushed a commit that referenced this pull request Nov 12, 2026
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.
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.

4 participants