Skip to content

AMD FSR 4.1 as a second upscaler engine, on #841's frame chain - #1008

Open
madebyjordan wants to merge 8 commits into
NotRexed:mainfrom
madebyjordan:amd/fsr4
Open

madebyjordan wants to merge 8 commits into
NotRexed:mainfrom
madebyjordan:amd/fsr4

Conversation

@madebyjordan

@madebyjordan madebyjordan commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

AMD FSR 4.1 as a second upscaler engine, on the frame chain #841 merged for DLSS. Rebuilt on current main: it no longer carries #850's DLSS.

What changed

  • Shared frame chain. NVIDIA DLSS, DLAA and Reflex (NGX + Vulkan, Linux and Windows, opt-in) #841's inputs (scene camera and depth, colour, camera + actor motion, in-game camera jitter) aren't DLSS-specific, so they move out of vk_dlss.cpp into vk_temporal.cpp behind a small engine interface. vk_dlss.cpp keeps NGX, the feature and its evaluation, and does the same thing as before.
  • FSR engine. vk_fsr4.cpp runs FSR 4.x through AMD's FSR API on those same inputs, taking the newest FSR 4 AMD's library offers (never an older FSR in its place). Only AMD's MIT headers are vendored (third_party/fidelityfx); AMD's signed DLLs are the player's.
  • Menu. Upscaler row: Game's own, NVIDIA DLSS, NVIDIA DLAA, AMD FSR 4, AMD FSR 4 Native AA. FSR uses DLSS's Render resolution steps (same ratios). The saved key stays dlss.
  • Opt-in build, like DLSS. make_game.py --fsr / HOOKSHOT_FSR=1 / a Builder tick. It also turns on the camera jitter in the game. Default builds are unchanged.
  • Fix for GPUs without cooperative matrices. The 67/58/50/33% Render resolution networks don't translate there (RDNA 2), so their upscale fell back to a linear stretch that no upscaler ever saw. That fallback now offers the frame to the temporal upscaler first.
  • CI. The DLSS link-check job also builds FSR and runs when the shared chain changes. The Linux AppImage job builds the back end once more with FSR, so its Linux side is compiled too.

Tested

  • RX 6700 XT, Windows, FSR 4.1.2 (INT8): it runs at 75%, 67%, 58%, 50% and 33%, and at Native AA; 600 of 600 frames in each status line, the jitter moving, no history resets in play. No blurring or ghosting seen in play.
  • Cost on RDNA 2: about 5–7 ms of GPU time per frame (13.7 → 18–21 ms), the same at every Render resolution, because FSR works at the output size. Native AA (no upscale) costs about the same.
  • tests/run_all.py: 177 pass; the 5 failures also fail on plain main here (Windows symlinks, local clang warnings and headers).
  • Not tested here: the DLSS half in play (no RTX card; CI's DLSS job compiles it, and @hckrman101 checked the move against NVIDIA DLSS, DLAA and Reflex (NGX + Vulkan, Linux and Windows, opt-in) #841), and Linux in play (CI compiles it).

Related

🤖 Generated with Claude Code

@madebyjordan madebyjordan changed the title AMD FSR 4.1 (collaboration branch) AMD FSR 4.1 as a second upscaler engine, on #841's frame chain Nov 9, 2026
@madebyjordan
madebyjordan force-pushed the amd/fsr4 branch 2 times, most recently from bbd11e9 to 050fc55 Compare November 9, 2026 22:50

@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.

Read the rebuilt version (050fc55) against current main. Pulling the frame chain out into vk_temporal.cpp behind a small engine interface is a good shape, and the move looks faithful: I diffed temporal_upscale against main's dlss_upscale and prepare against make_feature. The only changes are the engine indirection, s.drawing's handover and the near/far/fov capture, so the DLSS half should behave as it did. It merges cleanly with main, CI is green (including the DLSS+FSR link check), there are no new HOS_OPT ids (the new values 3/4 go under the existing dlss key), and nothing new needs adding to tests/run_all.py. Provenance is documented: the headers carry AMD's MIT notice, the README pins SDK v2.4.0, and the signed DLLs aren't vendored.

What I'd still look at, inline:

  • ~Context calls ffxDestroyContext from a function-static at process exit (likely a hang or crash on close).
  • The Linux-only code isn't compiled anywhere in CI.
  • Nit: FSR's inputs are made at the output size.

On the other FSR PRs: #1008 isn't superseded. #1095 and #1313 only vendor FSR 3.1 shader sources, with no file overlap. #1096/#1097 were stacked on the old #1008 (dlss_dispatch, the #850 base) and touch the same files (vk_fsr4., vk_dlss., vk_menu.cpp, vk_draw.cpp). They're dirty now and would need rebasing onto the TemporalEngine interface here, where FSR 3.1 would fit as a third engine. #1335 is a separate FSR 3.1 + FSR 4 + DLSS NR stack (its own vk_ffx.cpp/vk_fsr3.cpp/vk_ngx.cpp) that edits upscaler_rows.h, vk_dlss.cpp, vk_menu.cpp, vk_draw.cpp, vk_main.cpp, build.py and make_game.py too. The two will conflict whichever lands first, so it's worth deciding which FSR 4 path main takes before both go further.

Comment thread gpu/vk/vk_fsr4.cpp
uint32_t width = 0, height = 0; // (its output, which is also the most the render size can be)
uint64_t memory = 0; // (what AMD's library says it holds)
~Context() {
if (context) lib().destroy(&context, nullptr);

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 destructor also runs at process exit: st()'s function-static State (and any temporal_later lambdas still holding a shared_ptr<Context> in vk_temporal's dead) get destroyed then. Hookshot leaves through ExitProcess (hos_main.c, hos_relaunch.c) and never destroys the device. So on Windows this calls into AMD's DLL after it has already had its DLL_PROCESS_DETACH (it was loaded after hookshot_vk.dll, so it detaches first), while other threads have been killed, possibly inside the driver holding its locks. On Linux, exit() runs it while the render thread may still be recording with the same context. Either way it's a likely crash or hang on close. DLSS on main avoids this because its handles are raw pointers with no destructor. The simplest fix is to leak the statics, e.g. static State& s = *new State; here (and the same for Library), so nothing runs at exit.

Comment thread gpu/vk/vk_fsr4.cpp Outdated
made = 2;
}
// (the inputs at the context's largest render size: the render size moves under it without new images)
*in_w = ow, *in_h = oh;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit: this makes the chain's colour/depth/motion images output-sized even at 33%. At 4K that's about 130 MB (RGBA16F + R32F + RG16F) instead of about 15 MB. It's fine with maxRenderSize = output for Auto, but the fixed modes could ask for the mode's size and only grow when the render size does. Not blocking.

env:
RX_VK_ONLY: '1'
RX_RELEASE: '1'
HOOKSHOT_FSR: '1'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This only compiles the Windows side. All the #ifndef _WIN32 code in vk_fsr4.cpp (dlopen, the _DEPRECATED backend chain, unit_exposure) and the descriptor-buffer feature in vk_main.cpp never gets compiled in CI, and the PR says Linux wasn't tested. Because it's header-only, a cheap step in an existing Linux job (for example HOOKSHOT_FSR=1 on the AppImage/source-checks vk build, or a single -fsyntax-only of vk_fsr4.cpp with -DHOOKSHOT_FSR -Ihost/posix) would at least keep it building.

@madebyjordan

Copy link
Copy Markdown
Contributor Author

Thanks for the careful read, and for checking the move against your originals. All three are addressed in the new commit:

  • Exit: lib() and st() in vk_fsr4.cpp, and the chain's st() in vk_temporal.cpp (its deferred frees can hold an FSR context), are now leaked statics, so nothing of FSR's is destroyed at exit. DLSS's state is unchanged.
  • Linux in CI: the Linux AppImage job builds hookshot_vk.so once more with HOOKSHOT_FSR=1, in a copy of its work folder, so the #ifndef _WIN32 parts of vk_fsr4.cpp and the descriptor-buffer feature in vk_main.cpp are compiled. The AppImage itself is unchanged.
  • Input size: FSR's inputs now grow to the largest render size its context has had, capped at the output, so a fixed 33% keeps 33%-sized images.

A second pass turned up a few small things, fixed in the same commit:

  • RX_VK_DLSS=3 and up mean DLAA again, as main's clamp had it. The split had made them DLSS.
  • When AMD's loader is present but fails to load, the log now gives Windows' error instead of saying the file is missing.
  • make_game.py --fsr with the AppImage's prebuilt back end now says that back end has no FSR.

Also rebased on current main, which had started conflicting. That includes your #1364: the scene-size rule (scene_size_fits, the output noted from the upscale) moved into vk_temporal.cpp as temporal_scene_size, so FSR gets it too, and the Linux NGX path handling and the DLSS_RUNTIME_NOT_ASCII reason stay in vk_dlss.cpp as you wrote them. test_dlss_checks.py now reads the chain for the size rule. On the other FSR PRs: agreed. FSR 3.1 would fit as a third engine on the TemporalEngine interface, and which FSR 4 path to take is NotRexed's call.

madebyjordan and others added 7 commits November 9, 2026 23:42
The inputs NotRexed#841 builds for DLSS (scene camera and depth, the colour as the
network samples it, camera and actor motion, the host camera jitter) are not
DLSS's own, so they move out of vk_dlss.cpp into vk_temporal.cpp, a frame
chain with engines behind a small interface (TemporalEngine: start, prepare,
evaluate, release, memory). vk_dlss.cpp keeps NGX, the feature and its
evaluation, unchanged in what they do. vk_fsr4.cpp is the second engine,
through AMD's FSR API (MIT headers in third_party/fidelityfx; AMD's signed
DLLs are the player's, loaded at run time).

- Upscaler row: Game's own, NVIDIA DLSS, NVIDIA DLAA, AMD FSR 4, AMD FSR 4
  Native AA. FSR uses DLSS's Render resolution steps (same ratios). Saved key
  stays dlss; opts.dlss is now opts.upscaler.
- Build switch HOOKSHOT_FSR=1 (make_game.py --fsr, a Builder tick) compiles
  vk_fsr4.cpp with AMD's headers and puts the camera jitter into the game, as
  an NGX build does. Default builds are unchanged.
- RX_VK_FSR=0/1/2, RX_VK_FSR_DIR for AMD's library.
- The DLSS CI job also builds FSR, and runs when the chain changes.

Co-authored-by: NoBoDyZ <neotrax70@gmail.com>
Co-authored-by: hckrman101 <239247847+hckrman101@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…caler

On a GPU without cooperative matrices (AMD RDNA 2) the fixed shares' own
upscale networks (67% 6676619641320ba7, 58%, 50%, 33%) are left out and
upscale_stub stretches the scene linearly, so DLSS or FSR only ever saw the
game's 3/4. The stub now offers the frame to the temporal upscaler first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ler's input

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n upscale_stub

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…upscaler

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ender size, its Linux side built in CI

- vk_fsr4.cpp lib() and st(), vk_temporal.cpp st(): leaked statics. Hookshot exits
  through ExitProcess / exit() with the device alive; a destructor then called
  ffxDestroyContext after AMD's DLL detached (hckrman101's review).
- FSR's inputs are as large as the largest render size its context has had,
  not the output's (about 15 MB instead of 130 MB at 4K and 33%).
- linux-appimage.yml builds hookshot_vk.so once more with HOOKSHOT_FSR=1, so the
  #ifndef _WIN32 parts of vk_fsr4.cpp and vk_main.cpp are compiled in CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Renaming it to opts.upscaler broke every test that builds its own Options
stand-in (test_audio_output_menu, test_stick_dead_zone). The saved key was
dlss all along; the field keeps that name, documented as the Upscaler row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… PIPESTATUS)

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.

2 participants