Repository navigation
feat(dlss): DLSS Frame Generation through NGX, evenly paced at 3x and up - #2118
Conversation
…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>
|
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.
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. |
|
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
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 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:
So
|
…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>
|
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 (
On the 5090 (5120x2160, DLAA, Reflex On + Boost, same spot), Reflex's own timings (
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. |
|
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 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.
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:
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
If this is the cause, |
…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>
|
Thanks for the review. The four inline points are fixed in a new commit, 90c4683 From #1831's side:
Agreed that the choice between this and #1831 is @NotRexed's. |
…-ngx # Conflicts: # gpu/vk/vk_replay.cpp
…-ngx # Conflicts: # gpu/vk/vk_menu.cpp
…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>
|
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
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>
… 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>
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.
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)
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).feat(dlss): DLSS-G through NGX (vk_dlss.cpp, beside DLSS on the same NGX session, no Streamline):nvngx_dlssg.dllis looked for where DLSS's runtime is.RX_VK_DLSS_FG=0/1overrides it for a run.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.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).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
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(aNameErrorinmake_game.py; it passes on current main).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.cppandtest_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
VkLatencySubmissionPresentIdNVshould fix it; I'll do that as its own PR.🤖 Generated with Claude Code