Skip to content

feat(dlss): NVIDIA DLSS Frame Generation through Streamline (Vulkan, manual hooking, Reflex) - #1944

Closed
j64esatxzdrhfg wants to merge 1 commit into
NotRexed:mainfrom
j64esatxzdrhfg:feat/dlss-frame-generation
Closed

j64esatxzdrhfg wants to merge 1 commit into
NotRexed:mainfrom
j64esatxzdrhfg:feat/dlss-frame-generation

Conversation

@j64esatxzdrhfg

@j64esatxzdrhfg j64esatxzdrhfg commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

On RTX 40/50 cards, DLSS Frame Generation gives smoother motion than the renderer's own interpolation, at lower latency. NVIDIA's network makes the frames and Streamline's Reflex paces them. This overlaps #1831, which does frame generation through NGX directly and keeps a Streamline path switched off. This is offered as an alternative: happy to close it if you prefer the NGX route.

Since the review (rebased on main d7a7306, still one commit)

  • Exit and Restart crash: Streamline is now shut down from mkwvk_close_asked, which the window's close, Quit and Restart all go through before the host's ExitProcess. dlssg_shutdown:
    • stops letting dlssg_* calls in;
    • waits for a Reflex sleep under way (dlssg_sleep drops the state lock during it) without holding the present lock;
    • takes the present lock and the queue's lock, 2 s at most each. Past that it leaves Streamline as it is, so a stuck present can't hang the exit;
    • waits for the device to go idle (device_wait_idle_locked), turns DLSS-G off, then calls slShutdown.
    • It runs once. Every dlssg_* call does nothing afterwards, and both locks stay held until the process ends, so nothing reaches Streamline's hooks after it shut down.
    • A --prepare-shaders run (no window, so no close) no longer starts Streamline.
  • 0xbad00004 with nvngx_dlss.dll only in ngx\: NGX starts once per process and keeps the folder list from that first start. With Streamline, the first start is sl.common's when the device is made. sl.common passes NGX the plugins' folder plus every entry of Preferences::pathsToPlugins (commonEntry.cpp in the 2.14.16 source). So pathsToPlugins now lists, after bin\ (the first folder with plugins still wins for the plugins), the same folders vk_dlss.cpp gives NGX: RX_VK_DLSS_DIR, the game folder's ngx, %LOCALAPPDATA%\Hookshot\ngx. Nothing is copied beside the exe. With Streamline running, vk_dlss.cpp now logs this case as NGX not finding the runtime in its folders instead of blaming the GPU. The game folder is also taken wide now, not through the ANSI code page.
  • RX_VK_DLSSG is applied over opts.dlssg in menu_init, next to RX_VK_FRAMEGEN_MULT, so it also turns frame generation on for that run.
  • motionVectorsInvalidValue is left at Streamline's default (INVALID_FLOAT).
  • tests/upscale_independent_test.cpp gets the dlssg_active() stub.
  • Nits:
    • ini_key has an explicit dlss_frame_gen entry, which saved_dlssg reads.
    • The row's help says the game's own rate is held to the shown-frame limit divided by the multiple (about 39 fps at 4x on 165 Hz), and that DLAA at a 4K output can overload even an RTX 4080.
    • The log has a line when the NVIDIA app's DLSS override makes Streamline use NVIDIA's plugins from ProgramData.
    • --accept-nvidia-license's help names the licenses for each fetch.
    • CONTRIBUTING notes that a future Builder tick needs the license text the DLSS tick has.

What changed

  • gpu/vk/vk_dlssg.cpp/.h: Streamline 2.14 with manual hooking. With the DLSS Frame Gen row set, sl.interposer.dll's vkGetInstanceProcAddr replaces vulkan-1.dll in load_global, after NVIDIA's signature is checked. The row is read from hookshot_options.ini before Vulkan starts; RX_VK_DLSSG overrides it.
  • Per frame:
    • the chain's depth and motion are tagged;
    • constants come from the scene camera;
    • PCL markers and Reflex sleep go through Streamline;
    • FG is on for game frames, off while the Insert menu is open, and off after 60 frames without inputs;
    • a Reflex limit on shown frames of refresh − refresh²/3600 (116 at 120 Hz).
  • The FPS counter shows made and game frames.
  • Inputs come from the temporal upscaler's frame chain (vk_temporal.cpp: DLSS, DLAA, FSR 4) after its engine drew. The chain keeps the scene camera's projection. Main's present-ahead split is respected: the hook sits right before present_out.
  • vk_reflex.cpp, vk_framegen.cpp, vk_fsr3fi.cpp: the renderer's own Reflex and frame generation stand aside while it runs.
  • vk_main.cpp: mkwvk_close_asked ends with dlssg_shutdown(). vk_dlss.cpp: the 0xbad00004 log line with Streamline running.
  • Menu: DLSS Frame Gen (Off, 2x–4x; restart). It is shown only in a build with Streamline on an NVIDIA GPU, and its help says why it isn't running when set.
  • Build:
    • nx2/build.py: HOOKSHOT_SL_SDK (Windows) builds vk_dlssg.cpp with Streamline's headers and links wintrust/crypt32.
    • nx2/make_game.py: copies the seven runtime DLLs beside hookshot.exe; --fetch-sl-sdk fetches the SDK first. The DLSS runtime stays in ngx\.
    • scripts/sl_sdk.py: downloads the pinned v2.14.16 release zip after NVIDIA's licenses are accepted, and checks the zip and every extracted file by SHA-256. NVIDIA's files are never committed or shipped.
  • Tests:
    • test_dlssg.py, test_sl_sdk.py, test_make_game_sl.py. test_dlssg.py now also covers the exit. It cuts State, dlssg_marker, dlssg_sleep and dlssg_shutdown out of vk_dlssg.cpp and runs them against a stand-in SDK whose Reflex sleep blocks:

      • shutdown waits for the sleep with the present lock free;
      • no marker and no new sleep gets in after shutdown begins;
      • the order is idle, DLSS-G off, slShutdown;
      • a second close does nothing;
      • a stuck present or a stuck sleep gives up after the wait.

      Each of these fails against a mutant that drops the matching step. It also checks RX_VK_DLSSG in menu_init, the NGX folders, and the close and Restart path in host/display.c.

    • test_reflex_swapchain_lifetime, test_shader_stamp_cache, the framegen and upscale_independent fixtures, test_audio_output_menu and test_stick_dead_zone know vk_dlssg.cpp and the row.

Testing

  • First version: RTX 5080, 120 Hz G-SYNC, VSync from the NVIDIA app, DLSS Balanced, in a build of main (8c92fe3) with this series.
    • With frame generation: 116 fps shown, 58 of the game's (screenshot); without it, 96.
    • No tearing with the driver's VSync and G-SYNC (on the v2.0.13 build).
    • Streamline itself doesn't support VSync with FG on Vulkan, but the driver's VSync works. Frame Warp isn't available on Vulkan.
  • Thanks to Zarigis (RTX 4080 SUPER), pmwork-personal (RTX 5090) and Arkex-ai (RTX 5070 Ti), whose runs found the crash and the bugs above.
  • This round, on main d7a7306:
    • vk_dlssg.cpp compiles with and without Streamline, vk_dlss.cpp with and without NGX, and the other touched files compile.
    • tests/run_all.py dlssg sl_sdk make_game_sl make_game_ngx ngx_sdk upscale_independent framegen present_ reflex fps_rate menu stick_dead_zone on Windows: 32 passed, 3 failed.
      • test_present_aspect_vulkan fails the same way on untouched main.
      • test_reflex_swapchain_lifetime and test_present_semaphores default to clang++, which this PC lacks. Both pass with --cxx g++.
  • In game, this round (RTX 5080, a build of main d7a7306 with this PR): six sessions with DLSS Frame Gen on (2x and 3x), each closed through the window. Every one logged Streamline shut down for the exit (result 0) and exited, and Windows recorded no crash. A Restart from the menu relaunched fine from a session without Streamline; a Restart with Streamline running was not tried. DLSS/DLAA with nvngx_dlss.dll only in ngx\ still needs a PC without NVIDIA's own DLSS download: this one has the NVIDIA app's override, which is likely why DLSS worked here without the copy.

Not done / not tested

  • No Hookshot Builder tick yet: the command line only.
  • No rows past 4x, though Streamline offers up to 6x on RTX 50.
  • Not tried in the game: Restart with Streamline running, the 2 s give-up paths and a stuck device. vkDeviceWaitIdle has no timeout.
  • Not tried in the game: the NGX folder fix.
  • The download URL in sl_sdk.py was checked against a local copy of the zip, not fetched from CI.
  • Not tested: FSR 4 as the input, AMD Anti-Lag 2 alongside, Linux.
  • The menu screenshot below shows the row's help before this round's added sentence.

Screenshots

DLSS Frame Gen row and its help

DLSS Frame Gen 2x: 116 fps shown, 58 of the game's Off: 96 fps
DLSS Frame Generation on: 116 fps, game 58 DLSS Frame Generation off: 96 fps

HDR10 captures, tone-mapped to SDR for this page (paper white to white, highlights rolled off up to 800 nits).

🤖 Generated with Claude Code

@Zarigis

Zarigis commented Nov 10, 2026

Copy link
Copy Markdown

Tested on an RTX 4080 SUPER (so RTX 40 now has a data point). With DLSS Quality it works well: the frames double, all of them reach the screen, and pacing is the most even of the frame generation options we have. The cost is small: the game's own frame rate (not counting made frames) went from 53.4 to 51.2 fps. Two bugs got in the way first, both small:

  1. DLSS does not start once Streamline is loaded unless nvngx_dlss.dll is also beside hookshot.exe. With it only in ngx\ (where make_game.py puts it), DLSS/DLAA fail with 0xbad00004, so DLSS-G never gets inputs.
  2. RX_VK_DLSSG=1 alone starts Streamline but never turns DLSS-G on. It doesn't set opts.dlssg, which dlssg_inputs and dlssg_before_present check, and nothing is logged.
Measurements

Market gate, a scripted 20 s camera spin, 4K output (3840x2160), HDR10, windowed (1920x1080 window, "Composed: Flip"), 239 Hz display, MAILBOX. RTX 4080 SUPER, driver 617.42, Windows 11. A build of main (907ec4e) with this PR merged. PresentMon 2.6 captured display times for every present. One run each:

Setup Game fps Frames shown/s Display changes 2-3 refreshes apart Single-refresh changes 95th pct game frame
DLSS Quality, no frame generation 53.4 53 – 0% 24.4 ms
DLSS Quality + DLSS Frame Gen 2x 51.2 102.6 78% 18% 25.2 ms
DLAA + DLSS Frame Gen 2x 28.3 56 37% 8% 67.7 ms
The renderer's own 2x, Frame pacing on (100%, no upscaler) 41 ~80 37% 24% –

The log agrees with PresentMon: DLSS Frame Gen: 104.4 fps shown, 52.2 of the game's.

  • At 239 Hz, half of a ~51 fps frame is about 2.3 refreshes, so alternating 2 and 3 is the best possible. DLSS-G was close to that.
  • DLAA plus DLSS-G at 4K overloads this GPU: 28 game fps and frames up to 200 ms. A hint on the row about DLAA at high output resolutions might save people some confusion.
Bug 1: DLSS needs the runtime beside hookshot.exe while Streamline is loaded

Without Streamline, NGX loads nvngx_dlss.dll from the game's ngx\ folder as before. With Streamline loaded (the DLSS Frame Gen row on), the same build and folder give:

vk: NVIDIA DLSS: not available on this GPU (0xbad00004): DLSS needs an NVIDIA RTX GPU

NVSDK_NGX_VULKAN_Init succeeds, but SuperSampling_FeatureInitResult is 0xbad00004. With a copy of nvngx_dlss.dll next to hookshot.exe, DLSS starts, and NGX's own log (RX_VK_DLSS_LOG=1) shows where it took the runtime from:

ngx: [NGXInitLog:226] Loaded from path "...\nvngx_dlss.dll" by hookshot.exe   (the exe's folder, not ngx\)

I haven't confirmed why. My guess is that Streamline's own NGX start comes first, so the path list vk_dlss.cpp passes later isn't used. A simple fix is for make_game.py (and sl_sdk.py --game) to also put nvngx_dlss.dll beside hookshot.exe when HOOKSHOT_SL_SDK is set. The log message is misleading too: it blames the GPU.

Bug 2: RX_VK_DLSSG doesn't turn DLSS-G on

asked() in vk_dlssg.cpp reads RX_VK_DLSSG to decide whether to start Streamline, but opts.dlssg keeps the options file's value. dlssg_inputs returns early on opts.dlssg <= 0, and dlssg_before_present decides "the row is off" every frame. Since that never changes the mode, it returns before logging anything. The result is Streamline started, DLSS-G ready, and no frames made, with nothing in the log saying why.

Setting dlss_frame_gen=1 in hookshot_options.ini works. Applying RX_VK_DLSSG over opts.dlssg where the options are read (as RX_VK_FRAMEGEN_MULT does in vk_menu.cpp) would match the readme's "RX_VK_DLSSG=0..3 for one run".

Also worth knowing when testing
  • Streamline doesn't interpolate while the window isn't focused (sl.log: DLSS-G disabled: window not focused). A test launched in the background shows DLSS-G "on, 2x" with "fps shown" equal to the game's.
  • In that state it still costs a lot. With DLAA: 34 game fps against 43 with the row off and 46 without Streamline. Not a problem in normal play, but it can mislead an unattended benchmark.
  • sl.log also logs Failed to find global tag 'kBufferTypeDepth' / 'kBufferTypeMotionVectors' once, around an area load. Harmless as far as I can see.

@pmwork-personal

Copy link
Copy Markdown
Contributor

Tested this at 5120x2160 on an RTX 5090 (driver 621.14, Windows 11), DLAA, Streamline 2.14.16 matching the pinned hashes. Frame generation works: 2x showed ~91 fps from ~46 game fps, 3x 134–152 from 45–51, 4x 146–157 from 37–39. A few things from the run, in order of how much they hurt:

Every exit and every Restart crashed with DLSS Frame Gen on: an access violation inside NVIDIA's DLSS-G (read at 0x48). Nothing calls slShutdown, so the process exits with Streamline running, and the DLSS-G plugin tears its frame generation down in its DLL detach, after ExitProcess has already ended the threads it needs. What fixed it here: from mkwvk_close_asked (before the host's ExitProcess), take the present lock (with a ~2 s timeout so a stuck present can't hang the exit), wait for the device to go idle, set DLSS-G's mode to off, then slShutdown, and have every dlssg_* call do nothing after that. Clean exits and restarts afterwards, with "Streamline shut down for the exit (result 0)" in the log. Happy to share the diff.
Link's shield and hair stepped in made frames (no motion on those draws). #1831/#1834's actor-motion layouts (one bone, two bones, rigid on a joint) fixed both. His arms and feet still step a little when jumping, which I'm still looking into.
motionVectorsInvalidValue = 0.0f marks every pixel with exactly zero motion as invalid (anything still while the camera is still). Streamline's default is INVALID_FLOAT ("no invalid value"). Probably worth using that unless 0 was chosen on purpose.
On current main, tests/upscale_independent_test.cpp needs a dlssg_active() stub (framegen_on asks for it now).
Streamline reports up to 6x on RTX 50; the row stops at 4x. At 165 Hz Reflex holds shown frames to 157, so 4x caps the game at ~39 fps. The row's help could say that.
Also, with the NVIDIA app's DLSS override on, Streamline loads NVIDIA's OTA plugins from ProgramData instead of the ones next to the game. That's worth a log line, or a per-exe opt-out like other Streamline integrations use.

hckrman101 added a commit that referenced this pull request Nov 11, 2026
…ltrawide DLAA, RX 9070 XT) (#2075)

Adds the two lists sent to #2034 so far, each with
`nx2/pipeline_states.py add` (only the pipelines no other contributed
list holds; the note beside each records who, GPU, build, settings and
route).

| List | From | Given | New |
|---|---|---|---|
| `contrib/pmwork-personal-rtx-5090-dlaa-ultrawide` | @pmwork-personal:
RTX 5090, Windows 11, 5120x2160 DLAA, DLSS Frame Generation 3x; Kokiri
Forest and Castle Town | 1,390 | 506 |
| `contrib/yeetbot90-amd-rx-9070-xt` | @yeetbot90: RX 9070 XT, Windows
11, 1440p Lanczos (Auto), frame generation 3x; Kokiri Forest to Goron
City | 529 | 137 |

The shipped merge goes from 19,038 to **19,571** pipelines. Both lists
are version 1 and are converted to version 2 in the merge (none lost).

Notes:
- pmwork-personal's list was recorded on a fork of main (fc375e9 + #1944
and menu changes) across several sessions with different upscaler
settings; the records are the game's draw state, so that only changes
which pipelines were met.
- yeetbot90's HDR and mods were "unknown"; noted as such.

Checked: the zips held only `hookshot_pipeline_states.bin`;
`pipeline_states.py check` passes; `test_pipeline_states`,
`test_ship_state_list`, `test_pipeline_state_sources` pass.

Refs #2034

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

@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, and for the clear write-up. The Streamline side is put together carefully: manual hooking from load_global, the interposer's signature checked before it loads, Reflex/PCL markers through Streamline, and the renderer's own FG and Reflex standing aside. The two tester reports above (RTX 4080 SUPER, RTX 5090) show it working and pacing well. There are a few things to fix first, though, most of them already found by the testers:

  1. Every exit and Restart crashes with it on (@pmwork-personal's report). I checked: nothing calls slShutdown, so DLSS-G tears down in its DLL detach after ExitProcess. The fix they describe (from mkwvk_close_asked: take the present lock with a timeout, wait idle, DLSS-G mode off, slShutdown, then make every dlssg_* do nothing) looks right to me. A crash on every quit is the blocker here.
  2. RX_VK_DLSSG starts Streamline but never turns FG on (Zarigis' bug 2). Confirmed: saved_dlssg() reads it, but opts.dlssg keeps the ini's value, and dlssg_inputs / dlssg_before_present both check opts.dlssg. Apply it in menu_init next to RX_VK_FRAMEGEN_MULT (inline).
  3. DLSS/DLAA fail (0xbad00004) once Streamline is loaded unless nvngx_dlss.dll sits beside hookshot.exe (Zarigis' bug 1), so DLSS-G gets no inputs on a stock make_game.py folder. I'd rather find out why NGX ignores vk_dlss.cpp's path list (probably because Streamline's sl.common starts NGX first) than copy the DLSS runtime beside the exe, because main keeps it in ngx\ on purpose (place_ngx_runtime: "nothing beside the exe"). Either way the log line shouldn't blame the GPU.
  4. tests/test_upscale_independent.py breaks. It cuts framegen_on out of vk_framegen.cpp and compiles it against tests/upscale_independent_test.cpp, which has no dlssg_active(). That file needs a stub, the same as framegen_resources_test.cpp got. CI didn't run on this PR ("no checks reported"), so please run python -I -B tests/run_all.py locally.
  5. motionVectorsInvalidValue = 0.0f marks every pixel with exactly zero motion as invalid, which is anything still under a still camera. Streamline's default (INVALID_FLOAT) seems to be what's wanted (inline).

Overlap:

  • #1831 also adds gpu/vk/vk_dlssg.cpp / vk_dlssg.h (through NGX, with a different API), so whichever lands second has a hard conflict on the same file names. You already offered this as an alternative. Which route to take is @NotRexed's call, so I'm not approving either one over the other.
  • The branch is 63 commits behind main. Within this series, #1940/#1941/#1942 touch vk_temporal.cpp, vk_replay.cpp, vk_framegen.cpp and vk_menu.cpp near the same spots, and #1945/#1946 touch vk_menu.cpp/.h (Options). I expect textual conflicts only, but a rebase on main is due anyway.

Licensing / redistribution: this matches how main handles DLSS. scripts/sl_sdk.py mirrors ngx_sdk.py: a pinned release, SHA-256 on the zip and on every file taken out, NVIDIA's notice printed, --accept-nvidia-license or a typed yes required, the licenses saved beside the files, the zip deleted afterwards, sl-sdk/ and the zip gitignored. Nothing from NVIDIA is committed, linked (only wintrust/crypt32) or shipped; the release workflows don't set HOOKSHOT_SL_SDK, and the packaging only picks up the script. FSR 4's "bring your own DLLs" model is stricter still, and this is closer to the NGX one, which is fine. Two small points:

  • When the Builder gets a tick for this, it needs the same license text the DLSS tick has in MainWindow.xaml.cs (and the CONTRIBUTING note should say so, as the NGX section does).
  • --accept-nvidia-license in make_game.py now stands for both NVIDIA license sets. That's fine since each fetch has its own flag, but the help text could say which licenses it accepts for which fetch.

Nits:

  • saved_dlssg() hard-codes dlss_frame_gen=, which only matches because ini_key derives it from the row's name. Add an explicit ini_key entry for opts.dlssg so renaming the row can't silently break the pre-Vulkan read.
  • The comment at pathsToPlugins says no OTA plugins, but pmwork saw the NVIDIA app's DLSS override load OTA plugins from ProgramData anyway. A log line when that happens would help.
  • Row help: Streamline reports up to 6x on RTX 50 but the row stops at 4x. Also, as both testers saw, the Reflex cap divided by the multiplier caps the game's own rate (about 39 fps at 4x on 165 Hz), and DLAA at 4K can overload even a 4080. Both are worth a sentence in the help.

Comment thread gpu/vk/vk_dlssg.cpp
/// The row's value as saved, read before the menu reads its file (menu_init comes after the instance): 0 off, 1 to 3 (2x to
/// 4x). RX_VK_DLSSG=<0..3> over it
int saved_dlssg() {
if (const char* e = getenv("RX_VK_DLSSG")) return std::clamp(atoi(e), 0, 3);

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 makes Streamline start, but opts.dlssg still comes from the ini, so with dlss_frame_gen=0 (or no ini) and RX_VK_DLSSG=1, dlssg_inputs returns early and dlssg_before_present sees "the row is off" every frame, and nothing is logged (Zarigis' bug 2). Applying RX_VK_DLSSG over opts.dlssg in menu_init, where RX_VK_FRAMEGEN_MULT is applied, fixes it and matches the readme.

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: menu_init now applies RX_VK_DLSSG over opts.dlssg next to RX_VK_FRAMEGEN_MULT, before restart_init, so the env var both starts Streamline and turns frame generation on. test_dlssg.py checks both places.

Comment thread gpu/vk/vk_dlssg.cpp
logf("vk: Streamline did not start (result %d): DLSS Frame Gen off", (int)r);
return nullptr; // (the module stays loaded: Streamline may have hooked something already)
}
s.active = true, s.why = DLSSG_OK;

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.

Nothing ever undoes this: there is no slShutdown anywhere, so the DLSS-G plugin tears down in DLL detach after ExitProcess has ended its threads. That's the access violation on every exit/Restart that pmwork-personal reported. You need a shutdown from mkwvk_close_asked (present lock with a timeout, idle, DLSS-G off, slShutdown, then every dlssg_* is a no-op after 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.

Added dlssg_shutdown(), called at the end of mkwvk_close_asked. It stops new calls, drains an in-flight Reflex sleep without the present lock, takes the present and queue locks with a 2 s limit, waits for idle, turns DLSS-G off, then calls slShutdown, once. Every dlssg_* is a no-op afterwards. slShutdown is now one of the required exports. In the game (RTX 5080, 2x and 3x): six window closes, each result 0, no crash; a Restart with Streamline running is still to try.

Comment thread gpu/vk/vk_dlssg.cpp Outdated
c.cameraFar = f.far_z > c.cameraNear ? f.far_z : 100000.0f;
c.cameraFOV = f.fov_y > 0 ? f.fov_y : 1.0f;
c.cameraAspectRatio = p[0] != 0 ? std::fabs(p[5] / p[0]) : 16.0f / 9.0f;
c.motionVectorsInvalidValue = 0.0f;

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.

With 0 as the invalid value, every pixel with exactly zero motion counts as invalid, which is everything static while the camera is still. Unless that was on purpose, leave Streamline's default (INVALID_FLOAT, "no invalid value").

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.

Not on purpose. Removed, so Streamline's default (INVALID_FLOAT, no invalid value) applies, and the test keeps it unset.

Comment thread gpu/vk/vk_framegen.cpp
static const int allow = !getenv("RX_VK_FRAMEGEN") || atoi(getenv("RX_VK_FRAMEGEN")) != 0;
return framegen_wanted(opts.framegen, allow) && !dev.offscreen && !fg.failed;
// (hookshot: off while NVIDIA Streamline runs DLSS Frame Generation, vk_dlssg.cpp: it makes the frames then)
return framegen_wanted(opts.framegen, allow) && !dev.offscreen && !fg.failed && !dlssg_active();

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.

tests/test_upscale_independent.py cuts this function into tests/upscale_independent_test.cpp, which has no dlssg_active(), so that test stops compiling. It needs a bool dlssg_active() { return false; } stub, like the one framegen_resources_test.cpp got.

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.

Added bool dlssg_active() { return false; } to tests/upscale_independent_test.cpp; test_upscale_independent passes again.

@Arkex-ai

Arkex-ai commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Additional RTX 5070 Ti / Windows11 data point:3440x1440, DLSS Quality2294x960, Streamline2.14.16 pinned runtimes. I tested this PR merged locally with mainf8efe057/API14, not the PR head alone.

I reproduced the exit access violation twice (read at0x48 inside nvngx_dlssg.dll). Calling slShutdown from mkwvk_close_asked fixed normal exits locally, consistent with @pmwork-personal's report. One concurrency detail: dlssg_sleep drops the state mutex while executing SDK sleep. Shutdown must stop admitting calls and drain that in-flight sleep before slShutdown. Do not hold the present lock while waiting for that sleep to return. My local causal fixture blocks SDK sleep, starts shutdown and verifies that idle/shutdown are delayed until sleep returns; it also checks no trailing marker after shutdown begins and idempotent close. It fails against my first shutdown attempt and passes after this coordination. Use device_wait_idle_locked() so the recorder boundary is preserved. Active-FG normal exits returned0. A stuck-device timeout and Restart remain untested.

Matched local snapshot: about60native/120displayed fps before pause and after stabilization. First second after inventory return:59.6native/111.4displayed, displayed P95=12.55ms; inventory about60/60. PresentMon2.6, no simultaneous pixel capture, one run per mode. Presented HUD/inventory text remained legible in inspected frames; this does not identify an individual generated frame or cover the campaign. Off/integrated/Streamline controls used the same host/backend/checkpoint, without a claim of an overall winner or input latency.

The runtime-beside-exe, RX_VK_DLSSG option and foreground prerequisites reported above also reproduced. Sharing the shutdown findings here rather than opening an overlapping FG integration.

@j64esatxzdrhfg

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review. All five are addressed, rebased on main d7a7306, still one commit:

  1. Exit/Restart crash: dlssg_shutdown() now runs at the end of mkwvk_close_asked. Close, Quit and Restart all reach it through rx_close_asked before hos_window_closed_exit. The order is the one you and the testers described:

    • stop letting dlssg_* calls in;
    • wait for an in-flight dlssg_sleep with the present lock not held (Arkex-ai's point);
    • take the present and queue locks, 2 s at most each, else leave Streamline as it is;
    • device_wait_idle_locked(), DLSS-G off, slShutdown.

    It runs once, and both locks stay held until the process ends. test_dlssg.py runs the real functions against a stand-in SDK whose sleep blocks. A --prepare-shaders run no longer starts Streamline, since it never closes a window. In the game (RTX 5080, 2x and 3x): six sessions closed through the window, each logged Streamline shut down for the exit (result 0) and exited with no crash report. A Restart with Streamline running is still to be tried.

  2. RX_VK_DLSSG: applied over opts.dlssg in menu_init, next to RX_VK_FRAMEGEN_MULT.

  3. 0xbad00004: the root cause is that NGX keeps the folder list of its first start, and with Streamline that start is sl.common's. sl.common passes NGX the plugins' folder plus every Preferences::pathsToPlugins entry (commonEntry.cpp). The plugin manager takes plugins only from the first of those folders that has any. So pathsToPlugins now lists bin\ first, then vk_dlss.cpp's folders: RX_VK_DLSS_DIR, the game folder's ngx, %LOCALAPPDATA%\Hookshot\ngx. Nothing is copied beside the exe. With Streamline running, vk_dlss.cpp now logs 0xbad00004 as NGX not finding the runtime in its folders instead of blaming the GPU, and the menu shows "runtime not found" rather than "needs RTX". Not yet tried on a PC without NVIDIA's own DLSS download.

  4. upscale_independent_test.cpp: has the dlssg_active() stub. I ran tests/run_all.py filtered to the affected checks, not the whole suite. Of the 3 failures, test_present_aspect_vulkan fails the same way on untouched main. The other two default to clang++, which this PC lacks; both pass with g++.

  5. motionVectorsInvalidValue: removed; Streamline's INVALID_FLOAT applies.

Nits:

  • ini_key has an explicit dlss_frame_gen entry, and the test checks that saved_dlssg reads that key.
  • A log line appears when the NVIDIA app's DLSS override has Streamline use NVIDIA's plugins from ProgramData.
  • The row's help covers the shown-frame cap divided by the multiple (about 39 fps at 4x on 165 Hz) and DLAA at 4K.
  • --accept-nvidia-license's help names the licenses for each fetch.
  • CONTRIBUTING notes the license text a future Builder tick needs.
  • I left the row at 4x for now; 5x/6x for RTX 50 can follow separately.

On #1831: agreed, that's NotRexed's call.

@j64esatxzdrhfg

Copy link
Copy Markdown
Contributor Author

@Zarigis Thanks for the RTX 4080 SUPER numbers and the detailed write-ups. Both bugs are fixed in the update:

  1. NGX keeps the folders of its first start, which with Streamline is sl.common's, and sl.common passes NGX its pathsToPlugins. Those now include the game folder's ngx (plus RX_VK_DLSS_DIR and %LOCALAPPDATA%\Hookshot\ngx), so nothing needs to sit beside hookshot.exe. The log line no longer blames the GPU for this case. It likely worked here only because this PC has NGX's own DLSS download in ProgramData. Could you check it on yours, with nvngx_dlss.dll only in ngx\?
  2. RX_VK_DLSSG now sets opts.dlssg too.

The DLAA-at-4K hint is in the row's help now.

@j64esatxzdrhfg

Copy link
Copy Markdown
Contributor Author

@pmwork-personal Thanks for tracking the crash down and for the clear recipe; the update follows it:

  • the shutdown runs from mkwvk_close_asked: present lock with a 2 s limit, idle, DLSS-G off, slShutdown, then no-ops.
  • motionVectorsInvalidValue is back to Streamline's default.
  • The upscale_independent stub is added.
  • The row's help mentions the Reflex cap per multiple.
  • A log line says when the NVIDIA app's override loads NVIDIA's plugins from ProgramData.

5x/6x, a per-exe OTA opt-out, and the actor-motion layouts from #1831/#1834 are left for later. The exit fix held in six window closes here (2x and 3x, result 0, no crash). If you can run Exit and especially Restart with DLSS Frame Gen on again, that would confirm it on your setup.

@j64esatxzdrhfg

Copy link
Copy Markdown
Contributor Author

@Arkex-ai Thanks for the RTX 5070 Ti data and the concurrency note, which changed the design. dlssg_sleep now counts itself in under the state lock, and shutdown waits for that count to drop to zero (2 s at most) before it touches the present lock. It uses device_wait_idle_locked(). A test does what your fixture did: the SDK sleep blocks, the test checks that idle and slShutdown wait for it with the present lock free, that no marker gets in after shutdown begins, and that a second close is a no-op. It fails against mutants that skip the drain or hold the present lock. Restart goes through the same mkwvk_close_asked path. Like you, I haven't tried a stuck device or Restart in the game yet.

…manual hooking, Reflex)

DLSS Frame Generation gives RTX 40 and 50 cards smoother motion than the renderer's own interpolation, at less latency, with NVIDIA's network making the frames and Streamline's Reflex pacing them.

- gpu/vk/vk_dlssg.cpp/.h: NVIDIA Streamline 2.14 with manual hooking. With the new DLSS Frame Gen row set (read from hookshot_options.ini before Vulkan starts; RX_VK_DLSSG over it), sl.interposer.dll's vkGetInstanceProcAddr takes vulkan-1.dll's place in load_global, after NVIDIA's signature on it is checked. Each frame: the frame chain's depth and motion tagged, constants from the scene camera (no invalid motion value: Streamline's default), PCL markers and Reflex's sleep through Streamline, frame generation on for game frames (off with the Insert menu open, and after 60 frames without inputs: a frame now and then without them keeps it on), and a Reflex limit on the frames shown at refresh - refresh^2/3600 (116 at 120 Hz). The FPS counter counts the made frames and shows the game's own rate. Not started in a --prepare-shaders run (no window, no close to shut it down).
- The exit: Streamline is shut down from mkwvk_close_asked (the window's close, Quit and Restart, before the host's ExitProcess). Left to the DLL detach, the DLSS-G plugin tore its frame generation down after ExitProcess had ended its threads: an access violation (read at 0x48 in nvngx_dlssg.dll) at every exit and Restart. dlssg_shutdown lets no dlssg_* call in any more, waits for a Reflex sleep under way with the present lock free, takes the present lock and the queue's (2 s at most each: past that Streamline is left as it is and the exit goes on), waits for the device to idle, turns DLSS-G off and calls slShutdown, once; the locks stay held until the process ends.
- DLSS beside it: NGX starts once in a process, with the folders of its first start, and with Streamline that is sl.common's. So Streamline's pathsToPlugins name the folders vk_dlss.cpp gives NGX for DLSS's runtime (RX_VK_DLSS_DIR, the game folder's ngx, %LOCALAPPDATA%\Hookshot\ngx) after the plugins' own; before, DLSS and DLAA failed with 0xbad00004 unless nvngx_dlss.dll was also beside hookshot.exe or NGX had a DLSS of its own downloads. vk_dlss.cpp says that case as NGX's folders, not the GPU. The log says when the NVIDIA app's DLSS override has Streamline use NVIDIA's own plugins from ProgramData.
- The inputs come from the temporal upscaler's frame chain (vk_temporal.cpp: DLSS, DLAA, FSR 4), after its engine drew; the chain now keeps the scene camera's projection, and its depth and motion are a transfer's source while Streamline runs.
- vk_reflex.cpp, vk_framegen.cpp, vk_fsr3fi.cpp: the renderer's own Reflex and frame generation stand aside while it runs.
- Insert menu: the DLSS Frame Gen row (Off, 2x to 4x; restart; saved as dlss_frame_gen by ini_key, which vk_dlssg.cpp reads before Vulkan), shown in a build with Streamline on an NVIDIA GPU; RX_VK_DLSSG turns it on for the run too (menu_init). Its help says why it is not running when it is set, that the game's own rate is held to the shown frames' limit divided by the multiple, and that DLAA at a 4K output can be too much.
- nx2/build.py: HOOKSHOT_SL_SDK=<folder> builds vk_dlssg.cpp with Streamline's headers (Windows only) and links wintrust/crypt32 for the signature check. nx2/make_game.py copies the runtime (sl.interposer, sl.common, sl.dlss_g, sl.reflex, sl.pcl, nvngx_dlssg, NvLowLatencyVk) beside hookshot.exe, and --fetch-sl-sdk fetches the SDK first; --accept-nvidia-license's help names the licenses of each fetch.
- scripts/sl_sdk.py: Streamline's binaries are only in its release zip, so it downloads the pinned v2.14.16 zip after NVIDIA's licenses are accepted, checks it and every file it takes out against their SHA-256s, and deletes it. NVIDIA's files are never committed or shipped.
- Tests: tests/test_dlssg.py (the frame limit, the row and its key, RX_VK_DLSSG, the stubs, the hand-over points, NGX's folders, and the exit run with a stand-in SDK whose Reflex sleep blocks: the exit waits for it with the presents free, lets nothing in, then idle, DLSS-G off, slShutdown, once; a stuck present or sleep gives up after the wait), tests/test_sl_sdk.py, tests/test_make_game_sl.py; test_reflex_swapchain_lifetime, test_shader_stamp_cache, the framegen and upscale_independent fixtures, test_audio_output_menu and test_stick_dead_zone know vk_dlssg.cpp and the row.

Not done: a Hookshot Builder tick for it (the command line only); rows past 4x (Streamline offers up to 6x on RTX 50).

Tested: RTX 5080, driver with HAGS on, 120 Hz G-SYNC, NVIDIA app VSync on: 116 fps shown, 58 of the game's, no tearing, on v2.0.13 (where the inputs came from vk_dlss.cpp). Streamline does not support VSync with frame generation on Vulkan, but the driver's works; Frame Warp is not available on Vulkan. Testers' runs of the first version (RTX 4080 SUPER, RTX 5090, RTX 5070 Ti) found the exit crash, the 0xbad00004 and RX_VK_DLSSG. On main d7a7306: compile-checked with and without Streamline (and vk_dlss.cpp with and without NGX), and the game-free tests; the exit shutdown and the NGX folders are not yet tried in the game.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@j64esatxzdrhfg
j64esatxzdrhfg force-pushed the feat/dlss-frame-generation branch from c38b896 to e2710eb Compare November 11, 2026 15:07
@j64esatxzdrhfg

Copy link
Copy Markdown
Contributor Author

Rebased onto current main. One conflict: main now colours the FPS counter by the game's own rate against its cap when frame generation is on; that rule now covers DLSS Frame Generation too (made = opts.framegen >= 1 || (opts.dlssg >= 1 && dlssg_active())).

@hckrman101

Copy link
Copy Markdown
Collaborator

Thank you for your contribution, I will close this for now as we are going with NGX for framegen at the moment, since we already have NGX and it supports Linux. Please test those PRs instead!

@hckrman101 hckrman101 closed this Nov 11, 2026
hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
… up (#2118)

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.** #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:
- DLSS-G made every set in gameplay (600/600 per log window), ~135 fps
shown from ~45 game fps.
- Frames 7–9 ms apart on the display thread. The pacing felt as even as
#1944's Streamline path on the same machine.
  - Clean exits.
- `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**
- Only one GPU (RTX 5090), Windows only.
- Not checked on a fixed-refresh (non-VRR) display, where FIFO would
hold frames to the refresh.
- **Follow-up:** Reflex's "to the GPU's end" reads 243–442 ms under
DLSS-G (nullifyshot's measurement). Tagging GPU submits with
`VkLatencySubmissionPresentIdNV` should fix it; I'll do that as its own
PR.
- The HUD is drawn into the same picture (no HUD-less input), as with
FSR 3.1's interpolation. I didn't see HUD smear at 3x, but I didn't look
for it specifically.
- Link's limbs still step a little when jumping; that's the actor
motion, not frame generation. #1831/#1834's single/pair/rigid
actor-motion layouts fix his shield and hair here, so I'd suggest those
as their own PR. I'll follow up on the arms and feet.
- Relation to the open frame generation PRs:
- **#1944 (Streamline):** I tested it on the same machine, and its exit
crash and fix are in my comment there.
- **#1831 (NGX):** it was the starting point for this path; its
MvecScale 1 was right.
- **Overlap:** happy to fold this into either if the maintainers prefer.

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

---------

Co-authored-by: pmwork-personal <315945841+pmwork-personal@users.noreply.github.com>
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>
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.

5 participants