feat(vk): DLSS frame generation (NGX), more actor motion for DLSS, FidelityFX FSR 4 fallback (builds with #1833, #1834) #1831

Closed
FIocker wants to merge 1 commit from super-build/upscaling-framegen into main
FIocker commented 2026-11-10 17:46:30 +00:00 (Migrated from github.com)

This adds NVIDIA DLSS frame generation through NGX and gives more of OoT's moving models their own motion, so DLSS ghosts less behind them. It also offers AMD's FidelityFX FSR 4 where main's FSR 4 engine isn't built.

#1833, #1834 and this PR build only together, and should be reviewed and merged as one unit. The menu rows, the vk_main and vk_replay hooks and the build lists are in #1834; the neural rendering this chain feeds is in #1833. FIocker:super-build/all is the three together, rebuilt on main at d7a73062, and it's what was built and tested. #1830 is separate and builds alone.

Part Branch Builds
Shader fixes super-build/shaders (#1830) on its own
Upscaling and frame generation (this PR) super-build/upscaling-framegen (#1831) with #1833 and #1834
DLSS 5 neural rendering super-build/neural (#1833) with this PR and #1834
Menu, renderer and build plumbing super-build/core (#1834) with this PR and #1833

What it does

Temporal upscaler chain (vk_temporal.cpp, on main's DLSS/FSR 4 chain):

  • Actor motion covers more skinned models: single-influence sets (kSingle), two-influence sets (kPair), and rigid models bound to one bone without weights (kNoWeight), such as Navi's body, props and held items. dlss_actor.vert and dlss_actor.h read the new formats.
  • The scene camera's view and projection are kept alongside the view-projection.
  • temporal_upscale_recent() says whether a temporal upscaler drew in the last frames.
  • Neural rendering from the chain:
    • Before the HUD (default): on the engine's picture, so the model never sees the HUD.
    • Before the upscaler (#1833's Stage row): on the render-size scene. Here the model now gets the chain's own depth, motion, jitter and resets (NrFrame) instead of a flat depth and no motion. A temporal model then keeps the jitter the engine needs and doesn't ghost on camera motion.

Frame generation:

  • NVIDIA DLSS frame generation through NGX directly (vk_dlssg.cpp, vk_ngx.cpp), sharing DLSS's one NGX session (dlss_ngx_session).
  • Methods (framegen_methods.h, with tests): Auto, NVIDIA DLSS and Built-in, of what runs here.
  • AMD's FSR frame generation is not offered. Main made FSR 3.1's interpolation opt-in on 2026-11-10 (RX_VK_FSR_FG=1, after #1635 and its duplicates: the HUD moved with the scene). This PR keeps that, and doesn't offer FidelityFX FSR 4's frame generation either, since it misbehaved the same way in play. The method enum keeps both values, so a saved choice reads as Auto and a future fix can bring them back.
  • Streamline frame generation is removed (it was vk_slfg.cpp, env-only). It misbehaved in play, and its hooks in Reflex and the present were what main's tests tripped on.

FSR 4 through AMD's FidelityFX runtime (vk_ffx.cpp, MIT headers in third_party/ffx-api): this is the Upscaler row's FSR 4 only in builds without main's FSR 4 engine (no HOOKSHOT_FSR). Where vk_fsr4.cpp is built and available, that engine takes the row.

Window sharpening over a temporal upscaler is a fifth of the Sharpness row (RX_VK_TEMPORAL_SHARPEN). At full strength over DLSS's motion-time softness, dense grass turned into an oily smear whenever the camera moved. Main has since fixed the black HDR picture under the scaling pass the same way this PR did (the SAMPLED usage), so that part is gone from here.

Camera motion pass (vk_camera_motion.cpp) for the open NR backend, and docs/upscaling.md.

Main's recorder (#1473): vk_camera_motion, vk_dlssg and vk_ffx include vk_work.h last, so their vkCmd* go through it. NGX's and FidelityFX's own recording gets a B_DIRECT boundary first, as vk_dlss.cpp has. test_recorder_sync.py passes.

Testing

  • Tests: tests/run_all.py on the three together, locally (clang targeting MSVC, the CPM Vulkan headers, glslang 11.12): 328 passed. The only failures are the same 18 that main at d7a73062 has on this machine, all down to this PC's toolchain.
  • In game, before the rebase: Windows 11, NVIDIA RTX, OoT v131072, 3840x1820, DLSS Quality (2779x1318 → 4168x1976), PresentMon over 20 s in Kokiri Forest.
    • DLSS with DLSS frame generation 2x: 99.4 fps, p99 15.0 ms, max 40.8 ms. The log says frame generation method: NVIDIA DLSS.
    • Actor motion: the motion debug view shows Link's cap and Navi carrying their own motion.
  • Since the rebase: built with MSVC and the NGX SDK; in-game results for this build will follow in a comment.
  • Not tested: main's FSR 4 engine (no HOOKSHOT_FSR here) and AMD GPUs.

🤖 Generated with Claude Code

This adds NVIDIA DLSS frame generation through NGX and gives more of OoT's moving models their own motion, so DLSS ghosts less behind them. It also offers AMD's FidelityFX FSR 4 where main's FSR 4 engine isn't built. **#1833, #1834 and this PR build only together**, and should be reviewed and merged as one unit. The menu rows, the `vk_main` and `vk_replay` hooks and the build lists are in #1834; the neural rendering this chain feeds is in #1833. [`FIocker:super-build/all`](https://github.com/FIocker/Hookshot/tree/super-build/all) is the three together, rebuilt on main at d7a73062, and it's what was built and tested. #1830 is separate and builds alone. | Part | Branch | Builds | |---|---|---| | Shader fixes | `super-build/shaders` (#1830) | on its own | | Upscaling and frame generation (this PR) | `super-build/upscaling-framegen` (#1831) | with #1833 and #1834 | | DLSS 5 neural rendering | `super-build/neural` (#1833) | with this PR and #1834 | | Menu, renderer and build plumbing | `super-build/core` (#1834) | with this PR and #1833 | ## What it does **Temporal upscaler chain** (`vk_temporal.cpp`, on main's DLSS/FSR 4 chain): - **Actor motion covers more skinned models**: single-influence sets (`kSingle`), two-influence sets (`kPair`), and rigid models bound to one bone without weights (`kNoWeight`), such as Navi's body, props and held items. `dlss_actor.vert` and `dlss_actor.h` read the new formats. - The scene camera's view and projection are kept alongside the view-projection. - `temporal_upscale_recent()` says whether a temporal upscaler drew in the last frames. - **Neural rendering from the chain**: - **Before the HUD (default)**: on the engine's picture, so the model never sees the HUD. - **Before the upscaler** (#1833's Stage row): on the render-size scene. Here the model now gets the chain's own depth, motion, jitter and resets (`NrFrame`) instead of a flat depth and no motion. A temporal model then keeps the jitter the engine needs and doesn't ghost on camera motion. **Frame generation:** - **NVIDIA DLSS frame generation through NGX directly** (`vk_dlssg.cpp`, `vk_ngx.cpp`), sharing DLSS's one NGX session (`dlss_ngx_session`). - **Methods** (`framegen_methods.h`, with tests): Auto, NVIDIA DLSS and Built-in, of what runs here. - **AMD's FSR frame generation is not offered.** Main made FSR 3.1's interpolation opt-in on 2026-11-10 (`RX_VK_FSR_FG=1`, after #1635 and its duplicates: the HUD moved with the scene). This PR keeps that, and doesn't offer FidelityFX FSR 4's frame generation either, since it misbehaved the same way in play. The method enum keeps both values, so a saved choice reads as Auto and a future fix can bring them back. - **Streamline frame generation is removed** (it was `vk_slfg.cpp`, env-only). It misbehaved in play, and its hooks in Reflex and the present were what main's tests tripped on. **FSR 4 through AMD's FidelityFX runtime** (`vk_ffx.cpp`, MIT headers in `third_party/ffx-api`): this is the Upscaler row's FSR 4 only in builds without main's FSR 4 engine (no `HOOKSHOT_FSR`). Where `vk_fsr4.cpp` is built and available, that engine takes the row. **Window sharpening over a temporal upscaler** is a fifth of the Sharpness row (`RX_VK_TEMPORAL_SHARPEN`). At full strength over DLSS's motion-time softness, dense grass turned into an oily smear whenever the camera moved. Main has since fixed the black HDR picture under the scaling pass the same way this PR did (the `SAMPLED` usage), so that part is gone from here. **Camera motion pass** (`vk_camera_motion.cpp`) for the open NR backend, and `docs/upscaling.md`. **Main's recorder** (#1473): `vk_camera_motion`, `vk_dlssg` and `vk_ffx` include `vk_work.h` last, so their `vkCmd*` go through it. NGX's and FidelityFX's own recording gets a `B_DIRECT` boundary first, as `vk_dlss.cpp` has. `test_recorder_sync.py` passes. ## Testing - **Tests**: `tests/run_all.py` on the three together, locally (clang targeting MSVC, the CPM Vulkan headers, glslang 11.12): 328 passed. The only failures are the same 18 that main at d7a73062 has on this machine, all down to this PC's toolchain. - **In game, before the rebase**: Windows 11, NVIDIA RTX, OoT v131072, 3840x1820, DLSS Quality (2779x1318 → 4168x1976), PresentMon over 20 s in Kokiri Forest. - **DLSS with DLSS frame generation 2x**: 99.4 fps, p99 15.0 ms, max 40.8 ms. The log says `frame generation method: NVIDIA DLSS`. - **Actor motion**: the motion debug view shows Link's cap and Navi carrying their own motion. - **Since the rebase**: built with MSVC and the NGX SDK; in-game results for this build will follow in a comment. - **Not tested**: main's FSR 4 engine (no `HOOKSHOT_FSR` here) and AMD GPUs. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
hckrman101 commented 2026-11-10 17:51:05 +00:00 (Migrated from github.com)

Heads-up: this conflicts with main now (a lot landed today). Could you merge main into the branch (or rebase) and resolve the conflicts? Thanks!

Heads-up: this conflicts with main now (a lot landed today). Could you merge main into the branch (or rebase) and resolve the conflicts? Thanks!
FIocker commented 2026-11-11 10:45:33 +00:00 (Migrated from github.com)

Rebased: the series is rebuilt on main at d7a73062, the conflicts are resolved, and the descriptions are rewritten. The full replies to your reviews are on #1830, #1833 and #1834. For this PR:

  • Streamline frame generation is removed.
  • FSR frame generation: main made FSR 3.1's interpolation opt-in itself (#1635), so the series follows main there, and the description says why FidelityFX FSR 4's isn't offered either.
  • HDR SAMPLED fix: main fixed the black HDR picture under the scaling pass the same way, so ours is gone.
  • Neural rendering before the upscaler now gets the chain's own depth, motion, jitter and resets.
  • Recorder: the new files go through main's recorder (#1473), and test_recorder_sync.py passes.

#1831, #1833 and #1834 build only together, as the descriptions now say.

Rebased: the series is rebuilt on main at d7a73062, the conflicts are resolved, and the descriptions are rewritten. The full replies to your reviews are on #1830, #1833 and #1834. For this PR: - **Streamline frame generation is removed.** - **FSR frame generation**: main made FSR 3.1's interpolation opt-in itself (#1635), so the series follows main there, and the description says why FidelityFX FSR 4's isn't offered either. - **HDR `SAMPLED` fix**: main fixed the black HDR picture under the scaling pass the same way, so ours is gone. - **Neural rendering before the upscaler** now gets the chain's own depth, motion, jitter and resets. - **Recorder**: the new files go through main's recorder (#1473), and `test_recorder_sync.py` passes. #1831, #1833 and #1834 build only together, as the descriptions now say.
hckrman101 (Migrated from github.com) reviewed 2026-11-11 12:06:11 +00:00
hckrman101 (Migrated from github.com) left a comment

Thanks for the rebase and the rewritten description.

Overlap: #2118 adds the same DLSS Frame Generation through NGX, also on DLSS's session. Its description says it started from this PR's path, including its MvecScale of 1. Only one of the two should land, so they need to converge.

Scope. This PR bundles DLSS-G with FidelityFX FSR 4 through AMD's runtime (vk_ffx.cpp plus vendored ffx-api headers), more actor-motion layouts, neural-rendering hooks, a camera-motion pass and an 848-line doc. It also only builds together with #1833 and #1834, and #1834 currently conflicts with main. That makes it much harder to land than a focused PR.

  • On this head only the commit-title check ran.
  • On its own the branch can't compile: opts.framegen_method is only in #1834's vk_menu.h.
  • test_framegen_methods.py looks for a menu row that is also only in #1834.

I'd suggest splitting it: DLSS-G on its own, and the single/pair/no-weight actor-motion layouts on their own. #2118's author reports those layouts fix Link's shield and hair and suggests the same split. FSR 4 through FidelityFX would then be a third PR.

What #2118 does better on frame generation, and could be adopted here:

  • Actor motion. DLSS-G gets the temporal chain's motion, actor motion included. Here it gets FSR 3.1's assembled inputs, which have camera motion only (vk_dlssg.h says so).
  • Pacing at 3x/4x. #2118 extends present-ahead to 3x/4x with a spare swapchain image per frame, and keeps Reflex on under frame generation, marking only the game frames. Here main's present-ahead still runs for 2x only, and Reflex stays off with frame generation.
  • Multiplier limits. It reads MultiFrameCountMax and leaves sets it can't make to the built-in method (see the inline note at 5x+).
  • Feature lifetime. It waits for the queue to go idle before releasing the feature and defers freeing its images. Here both are freed at once (inline).
  • Fewer copies. It writes the made frames straight into the set's images, with no extra full-size copy per frame.
  • Builds alone, with green CI and in-game runs from three testers on RTX 50 cards, at 3x and 4x as well.

What this one does better:

  • DLSS-G isn't tied to a temporal Upscaler being on.
  • The method choice lives in a small tested header.
  • Frame generation's device extensions are only requested when it's wanted.

Smaller points:

  • Turning frame generation off keeps the feature and the made images until shutdown(), which isn't called in this PR.
  • The description says Streamline frame generation "is removed", but main has no vk_slfg.cpp. That refers to the fork's earlier version, so it might be worth rewording.
  • DLSS-G was measured at 2x before the rebase. The results for this build are still to come.

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

Thanks for the rebase and the rewritten description. **Overlap:** [#2118](https://github.com/NotRexed/Hookshot/pull/2118) adds the same DLSS Frame Generation through NGX, also on DLSS's session. Its description says it started from this PR's path, including its MvecScale of 1. Only one of the two should land, so they need to converge. **Scope.** This PR bundles DLSS-G with FidelityFX FSR 4 through AMD's runtime (`vk_ffx.cpp` plus vendored ffx-api headers), more actor-motion layouts, neural-rendering hooks, a camera-motion pass and an 848-line doc. It also only builds together with #1833 and #1834, and #1834 currently conflicts with main. That makes it much harder to land than a focused PR. - On this head only the commit-title check ran. - On its own the branch can't compile: `opts.framegen_method` is only in #1834's `vk_menu.h`. - `test_framegen_methods.py` looks for a menu row that is also only in #1834. I'd suggest splitting it: DLSS-G on its own, and the single/pair/no-weight actor-motion layouts on their own. #2118's author reports those layouts fix Link's shield and hair and suggests the same split. FSR 4 through FidelityFX would then be a third PR. What #2118 does better on frame generation, and could be adopted here: - **Actor motion.** DLSS-G gets the temporal chain's motion, actor motion included. Here it gets FSR 3.1's assembled inputs, which have camera motion only (`vk_dlssg.h` says so). - **Pacing at 3x/4x.** #2118 extends present-ahead to 3x/4x with a spare swapchain image per frame, and keeps Reflex on under frame generation, marking only the game frames. Here main's present-ahead still runs for 2x only, and Reflex stays off with frame generation. - **Multiplier limits.** It reads `MultiFrameCountMax` and leaves sets it can't make to the built-in method (see the inline note at 5x+). - **Feature lifetime.** It waits for the queue to go idle before releasing the feature and defers freeing its images. Here both are freed at once (inline). - **Fewer copies.** It writes the made frames straight into the set's images, with no extra full-size copy per frame. - **Builds alone,** with green CI and in-game runs from three testers on RTX 50 cards, at 3x and 4x as well. What this one does better: - DLSS-G isn't tied to a temporal Upscaler being on. - The method choice lives in a small tested header. - Frame generation's device extensions are only requested when it's wanted. Smaller points: - Turning frame generation off keeps the feature and the made images until `shutdown()`, which isn't called in this PR. - The description says Streamline frame generation "is removed", but main has no `vk_slfg.cpp`. That refers to the fork's earlier version, so it might be worth rewording. - DLSS-G was measured at 2x before the rebase. The results for this build are still to come. The decision on which of the two lands stays with @NotRexed.
@ -0,0 +168,4 @@
const NVSDK_NGX_Result res =
ngx::create_feature(cmd, NVSDK_NGX_Feature_FrameGeneration, params, &feature);
if (NVSDK_NGX_FAILED(res) || !feature) {
logf("vk: dlssg: the feature was not created for %ux%u (0x%08x)", g.width, g.height,
hckrman101 (Migrated from github.com) commented 2026-11-11 12:06:11 +00:00

When creation fails (for example with the device made without frame generation's extensions, which asked() notes can happen when the row is turned on mid-game), ngx_feature retries and logs this line on every present with no cap. Consider giving up after a few failures until the extents change.

When creation fails (for example with the device made without frame generation's extensions, which `asked()` notes can happen when the row is turned on mid-game), `ngx_feature` retries and logs this line on every present with no cap. Consider giving up after a few failures until the extents change.
@ -0,0 +209,4 @@
// uses; the camera-motion pass uses the same pair and its picture agreed with the game's, which
// is the only check available without NVIDIA's own tooling.
if (in.have_matrices) {
ngx::set(params, NVSDK_NGX_DLSSG_Parameter_ClipToPrevClip, (void*)in.clip_to_prev_clip);
hckrman101 (Migrated from github.com) commented 2026-11-11 12:06:11 +00:00

vk_dlssg.h says the matrices are row major and "transposed on the way in", but here they are handed to NGX as they are. #2118 transposes the same dlss_math::reprojection output (its fg_put, "NGX's matrices are rows of a row-vector convention"). One of the two is wrong. Could you check which convention NGX expects and make the code and the header comment agree?

`vk_dlssg.h` says the matrices are row major and "transposed on the way in", but here they are handed to NGX as they are. #2118 transposes the same `dlss_math::reprojection` output (its `fg_put`, "NGX's matrices are rows of a row-vector convention"). One of the two is wrong. Could you check which convention NGX expects and make the code and the header comment agree?
@ -0,0 +331,4 @@
g.render_height == render_height && g.present_format == present_format)
return true;
// Extents or format changed: the feature was made for the old ones.
ngx_drop_feature();
hckrman101 (Migrated from github.com) commented 2026-11-11 12:06:11 +00:00

On a resize or multiplier change, this releases the NGX feature and then drop_images the made frames immediately. The previous present's command buffer (its DLSS-G evaluation and the copy out of g.made[]) can still be in flight, so both the feature and the images can be freed while the GPU uses them. They need a queue idle first (as main's vk_dlss.cpp release and #2118's fg_release do), or temporal_later-style deferral for the images.

On a resize or multiplier change, this releases the NGX feature and then `drop_image`s the made frames immediately. The previous present's command buffer (its DLSS-G evaluation and the copy out of `g.made[]`) can still be in flight, so both the feature and the images can be freed while the GPU uses them. They need a queue idle first (as main's `vk_dlss.cpp` release and #2118's `fg_release` do), or `temporal_later`-style deferral for the images.
hckrman101 (Migrated from github.com) commented 2026-11-11 12:06:11 +00:00

At 5x to 8x, want is capped at 3 (kMaxMade). dlssg_frames then returns 3, set->made becomes 3, and the set is shown as 4 frames while the row says 5x+, so the built-in method never fills the rest. It also doesn't read NGX's MultiFrameCountMax (1 on RTX 40), so 3x/4x there depend on NGX refusing each set. Leaving the set to the next method when m - 1 is more than DLSS-G can make would avoid both.

At 5x to 8x, `want` is capped at 3 (`kMaxMade`). `dlssg_frames` then returns 3, `set->made` becomes 3, and the set is shown as 4 frames while the row says 5x+, so the built-in method never fills the rest. It also doesn't read NGX's `MultiFrameCountMax` (1 on RTX 40), so 3x/4x there depend on NGX refusing each set. Leaving the set to the next method when `m - 1` is more than DLSS-G can make would avoid both.
@ -0,0 +41,4 @@
static wchar_t wide[MAX_PATH + 1];
static const bool once = []() {
char path[MAX_PATH + 1]{};
const DWORD n = GetModuleFileNameA(nullptr, path, MAX_PATH);
hckrman101 (Migrated from github.com) commented 2026-11-11 12:06:11 +00:00

GetModuleFileNameA plus widening byte by byte breaks for a game folder with non-ASCII characters (ANSI code page, against the UTF-8 path rule in AGENTS.md / test_utf8_paths.py). It also differs from the data path ngx_init gives NGX (game_dir() / "ngx"). Reusing game_dir() and the same data folder would fix both.

`GetModuleFileNameA` plus widening byte by byte breaks for a game folder with non-ASCII characters (ANSI code page, against the UTF-8 path rule in AGENTS.md / `test_utf8_paths.py`). It also differs from the data path `ngx_init` gives NGX (`game_dir() / "ngx"`). Reusing `game_dir()` and the same data folder would fix both.
hckrman101 commented 2026-11-11 16:24:28 +00:00 (Migrated from github.com)

Thanks for all the work on this, @FIocker. We're going with #2118 for DLSS frame generation for now, so I'm closing this one.

#2118 builds on its own, its CI is green and testers have run it on RTX 50 cards. It also already has the 3x+ pacing and Reflex. This branch needs #1834 to build, and it bundles several separate changes. The parts this PR did better are listed on #2118 as suggestions, credited to you, so the ideas aren't lost.

The other pieces here (more actor motion for DLSS, the FidelityFX FSR 4 fallback) would be very welcome as small, separate PRs on top of main.

Thanks for all the work on this, @FIocker. We're going with [#2118](https://git.magicbean.dev/MagicBeanDev/Hookshot/pulls/2118) for DLSS frame generation for now, so I'm closing this one. #2118 builds on its own, its CI is green and testers have run it on RTX 50 cards. It also already has the 3x+ pacing and Reflex. This branch needs #1834 to build, and it bundles several separate changes. The parts this PR did better are listed on #2118 as suggestions, credited to you, so the ideas aren't lost. The other pieces here (more actor motion for DLSS, the FidelityFX FSR 4 fallback) would be very welcome as small, separate PRs on top of main.
Owner

@FIocker commented on GitHub · 2026-11-12 13:09 UTC · original

Thanks, @hckrman101, and for crediting the ideas on #2118. As suggested, the single, pair and no-weight actor-motion layouts are now their own PR on main: #2787. The neural rendering in #1833 is rebuilt on #2118's NGX session.

> [@FIocker](https://github.com/FIocker) commented on GitHub · 2026-11-12 13:09 UTC · [original](https://github.com/NotRexed/Hookshot/pull/1831#issuecomment-6824503791) Thanks, @hckrman101, and for crediting the ideas on #2118. As suggested, the single, pair and no-weight actor-motion layouts are now their own PR on main: #2787. The neural rendering in #1833 is rebuilt on #2118's NGX session. <!-- github-sync:c:6824503791 -->
Commenting is not possible because the repository is archived.
No description provided.