feat: AMD FSR 4 and FSR 3.1 as an Upscaler choice #1469

Closed
roblesleo wants to merge 1 commit from feat/amd-fsr into main
roblesleo commented 2026-11-10 04:04:25 +00:00 (Migrated from github.com)

The Upscaler row gains AMD FSR and AMD FSR Native AA (dlss=3 and 4) on Windows, on any GPU with Vulkan 1.3. They take the inputs vk_dlss.cpp already makes for DLSS (the scene's colour, depth, camera and actor motion, the camera's jitter), so FSR is a second backend at the same place: the input code is built when either upscaler is (HOOKSHOT_TEMPORAL), and vk_ffx.cpp runs AMD's signed FSR SDK 2.4 runtime for Vulkan (FSR 4.1.2, FSR 3.1.6) through its five C functions.

  • Two rows, shown while FSR draws: FSR version (Auto, FSR 4, FSR 3.1) and, with FSR 4, FSR 4 model (Auto, FP8, INT8, INT8 RDNA2, INT8 Lite). FSR 4 Auto takes AMD's model, and INT8 Lite where AMD offers no FSR 4 (NVIDIA, Intel). A chosen model runs whatever AMD's own check says: the model classes' IsSupported is pointed at ours (found by RTTI name in the loaded library; the file on disk is not changed). The DLSS model row now shows only with DLSS or DLAA.
  • AMD's runtime is not in the repository. scripts/ffx_sdk.py fetches it from AMD's FidelityFX SDK repository (tag v2.4.0) with pinned SHA-256s; make_game.py --fetch-ffx-sdk does so before a build, puts it in the game's ffx folder and turns the camera jitter on (OOT_DLSS_JITTER) as DLSS builds do. Only AMD's four MIT API headers are vendored (gpu/third_party/ffx).
  • RX_VK_DLSS=3/4, RX_VK_FSR=0, RX_VK_FSR_MODEL=<0..6>, RX_VK_FFX_DIR and RX_VK_FFX_LOG=1 for single runs; the README documents them.

Tested on an RTX 3060 Laptop (NVIDIA 617.14, Windows 11), Kokiri Forest: FSR 4.1.2 INT8 Lite 960x540 -> 1920x1080, ~140 MB of video memory (83 MB context, 58 MB inputs), over a thousand frames without a failure; FSR 3.1.6 also runs. Not yet tested on AMD hardware. tests/test_upscaler_rows and the new tests/test_ffx_sdk pass.

Assisted-by: Claude Opus 5.5

The Upscaler row gains AMD FSR and AMD FSR Native AA (dlss=3 and 4) on Windows, on any GPU with Vulkan 1.3. They take the inputs vk_dlss.cpp already makes for DLSS (the scene's colour, depth, camera and actor motion, the camera's jitter), so FSR is a second backend at the same place: the input code is built when either upscaler is (HOOKSHOT_TEMPORAL), and vk_ffx.cpp runs AMD's signed FSR SDK 2.4 runtime for Vulkan (FSR 4.1.2, FSR 3.1.6) through its five C functions. - Two rows, shown while FSR draws: FSR version (Auto, FSR 4, FSR 3.1) and, with FSR 4, FSR 4 model (Auto, FP8, INT8, INT8 RDNA2, INT8 Lite). FSR 4 Auto takes AMD's model, and INT8 Lite where AMD offers no FSR 4 (NVIDIA, Intel). A chosen model runs whatever AMD's own check says: the model classes' IsSupported is pointed at ours (found by RTTI name in the loaded library; the file on disk is not changed). The DLSS model row now shows only with DLSS or DLAA. - AMD's runtime is not in the repository. scripts/ffx_sdk.py fetches it from AMD's FidelityFX SDK repository (tag v2.4.0) with pinned SHA-256s; make_game.py --fetch-ffx-sdk does so before a build, puts it in the game's ffx folder and turns the camera jitter on (OOT_DLSS_JITTER) as DLSS builds do. Only AMD's four MIT API headers are vendored (gpu/third_party/ffx). - RX_VK_DLSS=3/4, RX_VK_FSR=0, RX_VK_FSR_MODEL=<0..6>, RX_VK_FFX_DIR and RX_VK_FFX_LOG=1 for single runs; the README documents them. Tested on an RTX 3060 Laptop (NVIDIA 617.14, Windows 11), Kokiri Forest: FSR 4.1.2 INT8 Lite 960x540 -> 1920x1080, ~140 MB of video memory (83 MB context, 58 MB inputs), over a thousand frames without a failure; FSR 3.1.6 also runs. Not yet tested on AMD hardware. tests/test_upscaler_rows and the new tests/test_ffx_sdk pass. Assisted-by: Claude Opus 5.5
hckrman101 commented 2026-11-10 04:09:44 +00:00 (Migrated from github.com)

We have 4 FSR PRs, #1008 is the main one that we are working on ATM

We have 4 FSR PRs, https://git.magicbean.dev/MagicBeanDev/Hookshot/pulls/1008 is the main one that we are working on ATM
hckrman101 (Migrated from github.com) reviewed 2026-11-10 04:16:56 +00:00
hckrman101 (Migrated from github.com) left a comment

Thanks, this is careful work and the fetch script with pinned SHA-256s is nice. The big issue is overlap: as mentioned above, #1008 is the FSR PR we're merging, and this one implements most of the same thing a second way.

Duplicated from #1008:

  • the Upscaler values dlss=3 (FSR) and 4 (FSR Native AA), with ur::native/temporal_scaled here vs full_scale/scaled there, and step_upscaler(v, dir, dlss_ok, fsr_ok) with the same signature
  • loading AMD's signed amd_fidelityfx_loader_vk.dll + amd_fidelityfx_upscaler_vk.dll, querying versions, ffxOverrideVersion, create/dispatch/destroy
  • vendoring the same four MIT FFX API headers (here under gpu/third_party/ffx, there under third_party/fidelityfx)
  • reusing DLSS's inputs (colour/depth/motion/jitter) and turning on OOT_DLSS_JITTER for FSR builds

#1008 does this behind a TemporalEngine interface in vk_temporal.cpp, so vk_dlss.cpp stays the DLSS engine. This PR puts #if HOOKSHOT_NGX / if (fsr) branches all through vk_dlss.cpp. Both rewrite the same parts of vk_dlss.cpp, upscaler_rows.h, vk_menu.cpp, make_game.py and the upscaler-row tests, so they can't both merge as they are. It's also 18 commits behind main.

New here, and worth keeping:

  • FSR 3.1 as a choice (the FSR version row). #1008 always takes the newest 4.x
  • scripts/ffx_sdk.py + make_game.py --fetch-ffx-sdk: AMD's runtime fetched at a pinned commit with SHA-256 checks, plus tests/test_ffx_sdk.py
  • near/far/FOV taken from the projection for cameraNear/cameraFar/cameraFovAngleVertical
  • the FSR 4 model row (forcing FP8/INT8/INT8 RDNA2/INT8 Lite by patching AMD's vtables). More on that inline

Suggestion: rebase onto #1008 once it lands, or onto its branch now, and turn this into a follow-up that adds (1) FSR 3.1 version selection inside vk_fsr4.cpp (or an FSR engine over the same interface), (2) the fetch script, and (3) the camera planes if #1008's engine doesn't already pass them. I'd split the model-forcing hook into its own PR. It's the riskiest part and needs a maintainer decision.

Other points:

  • vk_ffx.h defines HOOKSHOT_FFX on every _WIN32 build, so HOOKSHOT_TEMPORAL and the whole DLSS input path in vk_dlss.cpp now compile in every Windows build, NGX or not. No CI checks ran on this branch, so nothing shows that the Windows no-NGX path (all the new #ifdef HOOKSHOT_NGX splits) builds.
  • Env vars: here RX_VK_FSR=0 turns FSR off and RX_VK_DLSS=3/4 picks it. #1008 uses RX_VK_FSR=1/2 to pick it. Please follow #1008's meaning when rebasing.
  • Saved options: new ini keys fsr_version and fsr_4_model (named after the rows). The dlss values 3/4 mean the same as in #1008, so saved files would stay compatible. No numeric option-id collisions: nothing in host/ changes.
  • Only tested on NVIDIA. FSR 4's real targets (RDNA 3/4) haven't been tested, and that's where the model forcing matters most.

Not good to merge as is: it duplicates #1008, and CI didn't run.

Thanks, this is careful work and the fetch script with pinned SHA-256s is nice. The big issue is overlap: as mentioned above, #1008 is the FSR PR we're merging, and this one implements most of the same thing a second way. **Duplicated from #1008:** - the Upscaler values `dlss=3` (FSR) and `4` (FSR Native AA), with `ur::native`/`temporal_scaled` here vs `full_scale`/`scaled` there, and `step_upscaler(v, dir, dlss_ok, fsr_ok)` with the same signature - loading AMD's signed `amd_fidelityfx_loader_vk.dll` + `amd_fidelityfx_upscaler_vk.dll`, querying versions, `ffxOverrideVersion`, create/dispatch/destroy - vendoring the same four MIT FFX API headers (here under `gpu/third_party/ffx`, there under `third_party/fidelityfx`) - reusing DLSS's inputs (colour/depth/motion/jitter) and turning on `OOT_DLSS_JITTER` for FSR builds #1008 does this behind a `TemporalEngine` interface in `vk_temporal.cpp`, so `vk_dlss.cpp` stays the DLSS engine. This PR puts `#if HOOKSHOT_NGX` / `if (fsr)` branches all through `vk_dlss.cpp`. Both rewrite the same parts of `vk_dlss.cpp`, `upscaler_rows.h`, `vk_menu.cpp`, `make_game.py` and the upscaler-row tests, so they can't both merge as they are. It's also 18 commits behind main. **New here, and worth keeping:** - FSR 3.1 as a choice (the *FSR version* row). #1008 always takes the newest 4.x - `scripts/ffx_sdk.py` + `make_game.py --fetch-ffx-sdk`: AMD's runtime fetched at a pinned commit with SHA-256 checks, plus `tests/test_ffx_sdk.py` - near/far/FOV taken from the projection for `cameraNear`/`cameraFar`/`cameraFovAngleVertical` - the *FSR 4 model* row (forcing FP8/INT8/INT8 RDNA2/INT8 Lite by patching AMD's vtables). More on that inline **Suggestion:** rebase onto #1008 once it lands, or onto its branch now, and turn this into a follow-up that adds (1) FSR 3.1 version selection inside `vk_fsr4.cpp` (or an FSR engine over the same interface), (2) the fetch script, and (3) the camera planes if #1008's engine doesn't already pass them. I'd split the model-forcing hook into its own PR. It's the riskiest part and needs a maintainer decision. Other points: - `vk_ffx.h` defines `HOOKSHOT_FFX` on every `_WIN32` build, so `HOOKSHOT_TEMPORAL` and the whole DLSS input path in `vk_dlss.cpp` now compile in every Windows build, NGX or not. No CI checks ran on this branch, so nothing shows that the Windows no-NGX path (all the new `#ifdef HOOKSHOT_NGX` splits) builds. - Env vars: here `RX_VK_FSR=0` turns FSR off and `RX_VK_DLSS=3/4` picks it. #1008 uses `RX_VK_FSR=1/2` to pick it. Please follow #1008's meaning when rebasing. - Saved options: new ini keys `fsr_version` and `fsr_4_model` (named after the rows). The `dlss` values 3/4 mean the same as in #1008, so saved files would stay compatible. No numeric option-id collisions: nothing in `host/` changes. - Only tested on NVIDIA. FSR 4's real targets (RDNA 3/4) haven't been tested, and that's where the model forcing matters most. Not good to merge as is: it duplicates #1008, and CI didn't run.
hckrman101 (Migrated from github.com) commented 2026-11-10 04:16:56 +00:00

nit: s.asked is shared by both engines, so after switching FSR to DLSS (or back), the other engine's context/feature is only let go once neither has run for 120 frames. Both stay resident meanwhile (~83 MB FSR context plus the DLSS feature).

nit: `s.asked` is shared by both engines, so after switching FSR to DLSS (or back), the other engine's context/feature is only let go once *neither* has run for 120 frames. Both stay resident meanwhile (~83 MB FSR context plus the DLSS feature).
hckrman101 (Migrated from github.com) commented 2026-11-10 04:16:56 +00:00

nit: with FSR (not Native AA), colour, depth and motion are allocated at the output size even though the render size is 50 to 67% of it. That's about 2 to 4 times the input memory DLSS uses, done so dynamic resolution never re-makes them. That's a fair trade, but worth a line in the PR description next to the ~58 MB figure. #1008 handles this in the shared chain.

nit: with FSR (not Native AA), colour, depth and motion are allocated at the output size even though the render size is 50 to 67% of it. That's about 2 to 4 times the input memory DLSS uses, done so dynamic resolution never re-makes them. That's a fair trade, but worth a line in the PR description next to the ~58 MB figure. #1008 handles this in the shared chain.
@ -0,0 +92,4 @@
const bool amd = c.amd ? c.amd(self, device) : false;
const int want = g_model.load(std::memory_order_relaxed);
const bool forced = want >= MODEL_FP8 && want <= MODEL_LITE;
const bool yes = forced ? want - MODEL_FP8 == i : amd;
hckrman101 (Migrated from github.com) commented 2026-11-10 04:16:56 +00:00

With a model forced, this says yes where AMD's own check says no, for example FP8 on anything but RDNA 4. I'd expect that to fail at context creation in the better case and give VK_ERROR_DEVICE_LOST mid-game in the worse one, not to fall back to FSR 3.1. The menu offers it as a normal choice with only "Elsewhere it may not run" as a warning. Could the forced-but-unsupported case be gated behind an env var (a test knob), with the row offering only what AMD says yes to?

With a model forced, this says yes where AMD's own check says no, for example FP8 on anything but RDNA 4. I'd expect that to fail at context creation in the better case and give `VK_ERROR_DEVICE_LOST` mid-game in the worse one, not to fall back to FSR 3.1. The menu offers it as a normal choice with only "Elsewhere it may not run" as a warning. Could the forced-but-unsupported case be gated behind an env var (a test knob), with the row offering only what AMD says yes to?
@ -0,0 +257,4 @@
s.why = NOT_LOADED;
return false;
}
const std::string models = hook_models(s.upscaler);
hckrman101 (Migrated from github.com) commented 2026-11-10 04:16:56 +00:00

This overwrites slot 2 of four vtables in whatever amd_fidelityfx_upscaler_vk.dll load() finds, and it runs even with Auto (where the hook just passes AMD's answer through). The "slot 2 is IsSupported(self, device) returning bool" layout is only known for the pinned 4.1.2 DLL. load() also accepts an unverified DLL from RX_VK_FFX_DIR or %LOCALAPPDATA%\Hookshot\ffx, so a newer or older AMD build with a different layout would get some other virtual replaced by model_supported and crash inside AMD's code. At minimum, only hook when the loaded file's SHA-256 matches ffx_sdk.FILES, and skip the hook entirely for Auto and FSR 3.1. Separately, patching AMD's signed runtime in memory is something maintainers should sign off on (AMD's SDK license terms for the signed binaries). I'd move it to its own PR.

This overwrites slot 2 of four vtables in whatever `amd_fidelityfx_upscaler_vk.dll` `load()` finds, and it runs even with Auto (where the hook just passes AMD's answer through). The "slot 2 is `IsSupported(self, device)` returning bool" layout is only known for the pinned 4.1.2 DLL. `load()` also accepts an unverified DLL from `RX_VK_FFX_DIR` or `%LOCALAPPDATA%\Hookshot\ffx`, so a newer or older AMD build with a different layout would get some other virtual replaced by `model_supported` and crash inside AMD's code. At minimum, only hook when the loaded file's SHA-256 matches `ffx_sdk.FILES`, and skip the hook entirely for Auto and FSR 3.1. Separately, patching AMD's signed runtime in memory is something maintainers should sign off on (AMD's SDK license terms for the signed binaries). I'd move it to its own PR.
roblesleo commented 2026-11-10 13:37:52 +00:00 (Migrated from github.com)

Thanks! Closing this in favour of #1008, as suggested in #1458. Two parts of this PR aren't in #1008 yet, and I'll offer them as a PR on top of its branch: a choice of FSR 4 model (it lets FSR 4 run on NVIDIA and Intel GPUs, where AMD's own check offers none: tested with INT8 Lite on an RTX 3060 Laptop), and a script that fetches AMD's signed FSR SDK 2.4 Vulkan runtime with pinned SHA-256s, placing it in the game folder at build time.

Thanks! Closing this in favour of #1008, as suggested in #1458. Two parts of this PR aren't in #1008 yet, and I'll offer them as a PR on top of its branch: a choice of FSR 4 model (it lets FSR 4 run on NVIDIA and Intel GPUs, where AMD's own check offers none: tested with INT8 Lite on an RTX 3060 Laptop), and a script that fetches AMD's signed FSR SDK 2.4 Vulkan runtime with pinned SHA-256s, placing it in the game folder at build time.
Commenting is not possible because the repository is archived.
No description provided.