Skip to content

Choose FSR 4.1 or FSR 3.1 from one Insert menu row - #1097

Open
N3oTraX wants to merge 15 commits into
NotRexed:mainfrom
N3oTraX:fsr3/menu-row
Open

N3oTraX wants to merge 15 commits into
NotRexed:mainfrom
N3oTraX:fsr3/menu-row

Conversation

@N3oTraX

@N3oTraX N3oTraX commented Nov 9, 2026

Copy link
Copy Markdown

Part 3 of 3 of the split of #871, and the last of it.

Depends on #1096. Only the last commit is new here: 3 files, +58/-22. GitHub shows the rest until #1096 and its dependencies land.

What changed

  • The AMD FSR 4.1 row becomes FSR upscaler: Off, Auto, FSR 4.1, FSR 3.1, changed at once while playing. It shows the engine in use, and why a forced choice cannot run.
  • The readme and a short docs/upscaling.md describe the row and the two engines.

How it was tested

In game, on the #871 tree (6095c4d) RX 7900 XTX, RADV Mesa 26.2.6, Linux: the row switched between FSR 4.1.1 INT8 and FSR 3.1 live several times at 5120x2160, no crash or device lost in the log. The row's code is unchanged from that tree
On this branch hookshot_vk.so builds with 0 warnings. tests/run_all.py fsr upscale menu: 8 passed
Full suite 129 passed, 7 skipped, 1 failed: test_program_answer_domains, which fails the same way on main (8b2e1d3)

Not tested

🤖 Generated with Claude Code

BlueHeisenberg and others added 15 commits November 9, 2026 05:39
…A's DLLs not included)

- gpu/vk/vk_dlss.cpp, the DLSS part of vk_draw.cpp, vk/shaders/dlss_*.comp: the gameplay upscale network's place in the frame
  taken by DLSS, which draws the 3D at the DLSS mode's size and scales it to the output with the game's depth and its camera's
  motion. Ported from zortos293/Lichii (its Streamline loader, camera motion and their shaders; Lichii credits Duck912's
  Eglantine work).
- NVIDIA Streamline v2.14.16's headers in third_party/streamline (MIT, gpu/PROVENANCE.md). sl.interposer.dll, sl.common.dll,
  sl.dlss.dll and nvngx_dlss.dll are loaded from a streamline folder beside the game exe, or the NVIDIA app's DLSS; the
  interposer and the model must carry a valid NVIDIA Corporation signature (WinVerifyTrust). Windows only: elsewhere DLSS is
  unavailable.
- Menu rows DLSS (after a restart), DLSS mode (Ultra Performance .. DLAA, each with its size for the chosen output; it sets the
  3D's scale in place of Resolution) and DLSS preset (J..M, at once); RX_DLSS, RX_DLSS_PRESET, RX_DLSS_DIR over them,
  RX_DLSS_DEBUG for its inputs. The scale sent to the game is for the output it runs at (the window crop).
- readme: the rows, where NVIDIA's files come from; the Lichii credit.
…on top of NotRexed#850's DLSS frame code)

A second backend for NotRexed#850's replacement of OoT's gameplay upscale, ported
from the FSR 4.1 work on Lichii's OoT branch (madebyjordan/Lichii-Hyrule
oot-dev, 7f8ae5b337..c5f2fce17d). It takes the same colour, depth and
camera motion as DLSS; DLSS keeps priority when it is on.

- vk_fsr4.cpp: AMD's FSR API (FidelityFX SDK 2.4). Only the MIT headers
  are vendored (third_party/fidelityfx); AMD's signed loader and upscaler
  DLLs load at run time from RX_FSR4_DIR or beside hookshot.exe. The
  newest 4.x provider the loader lists is picked explicitly (RDNA 2 and 3
  run its INT8 model). LDR, non-linear colour, standard depth. One
  context per output size, render sizes up to it, so the Resolution row
  and the game's dynamic resolution need no new context.
- Jitter: the game draws without one, so the draws that test or write
  the scene's depth move by a Halton (2, 3) sub-pixel offset through the
  viewport, and FSR is told it. The offset is part of the state dyn_pack
  compares, or a frame whose draws match the last frame's would keep the
  old one. From Lichii: the phase moves on at the network's pass (where
  the game's frame ends), not at presents; it is on only while FSR drew
  the last network pass.
- AMD edge line (Lichii, RX 6700 XT): moved up or left, the AMD driver
  draws nothing into the scene's last column or row, a stale near-white
  value that became a flickering line in FSR's history. FSR's colour and
  the motion's depth repeat the column and row before them. Colour going
  into FSR is cleaned of NaN and Inf and clamped to [0, 1]; the output
  pass drops NaN and Inf for both upscalers.
- FSR history resets when FSR did not draw the last network pass or on
  a camera cut, not by present count.
- Menu: "AMD FSR 4.1" (Off/On, at once; ini key fsr4, RX_FSR4=0/1).
  Resolution sets what it upscales from (75% Quality, 50% Performance,
  100% anti-aliasing only). RX_FSR4_JITTER=0 turns the jitter off.
- What changes on native, compared with Lichii: no GPU-time divisor
  (Hookshot already scales the game's timer, so it keeps full quality),
  no hunt for the tonemap draw or depth reader (NotRexed#818 takes them where
  the draws are), no 4/3-only size check.
- tests/test_fsr_jitter.py: the jitter is compared and applied, moves on
  at the network's pass, and the edge repeat is in both passes.

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

- The FSR context is made with FFX_UPSCALE_ENABLE_DYNAMIC_RESOLUTION: the
  render size changes under it with the Resolution row and the game's
  dynamic resolution.
- vk_fsr4.cpp is Windows only (AMD's signed runtime is), with stubs
  elsewhere: the Linux build has no LoadLibraryExA.
- AMD's dispatches count as work (note_work, vk_work.h), so full_barrier
  does not leave out the barrier after them as a repeat.
- dlss_image gives a replaced image back 120 presents later, as
  typed_upscale does with its histories, instead of keeping it: DLSS
  fixes the render size, but FSR can run with Resolution Auto.
- The dlss_*.comp headers regenerated at SPIR-V 1.6, as NotRexed#850 makes them.
- readme: FSR 4 claims limited to what was tested (RX 6700 XT, INT8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There is no AMD loader on Linux, so the upscaler library itself
(amd_fidelityfx_upscaler_vk.so) is loaded with dlopen from RX_FSR4_DIR
or beside the game. A Vulkan FSR 4 build reads the SDK's older backend
description and records with descriptor buffers: the description is
chained next to the current one, and VK_EXT_descriptor_buffer is
enabled only when the library is there. Windows is unchanged.

BITMAPFILEHEADER joins the POSIX windows.h shim, for the menu dump.

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

A Vulkan FSR 4 build lists its Auto provider before INT8 and FP8, so
taking the last of equal versions chose FP8 on hardware without it. It
also rejects a dispatch without an exposure: the game's colour is
already tonemapped, so it gets a 1x1 R32F of 1.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The file was Windows-only, which hid the Linux loading. Remove the
guard and its stubs, and say in the readme what Linux needs.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first half of an FSR backend: its shaders. Adds
AMD's FidelityFX SDK v1.1.4 GPU sources under third_party/fidelityfx-sdk,
unmodified and MIT, with a provenance note, and scripts/fsr3_shaders.py to
compile the eleven variants into gpu/vk/shaders/fsr3_*_comp_spv.h.

Eleven, not eight: the eight passes, plus wave-free variants of both pyramids
for devices without the subgroup operations the single-pass downsampler
prefers, and an accumulate compiled with sharpening on. The option set lives in
one place in the script because each option is a promise about the inputs --
HDR colour, low-resolution motion that is not jittered, reversed depth -- and
the host code has to hand the passes what they were compiled for.

This is the SDK-source route rather than AMD's FidelityFX API, so no runtime
library is needed, none is included, and one code path serves Windows and
Linux. NotRexed#577 vendors AMD's API headers for the other route under
third_party/fidelityfx; if both land, one should go.

The headers are checked in, like every other shader in gpu/vk/shaders, so a
clone builds with no glslang. tests/test_fsr3_shaders.py recompiles all eleven
and compares, and validates each module with spirv-val, so the headers cannot
drift from their sources and an invalid module is caught here rather than on a
device. It is wired into run_all's tool table, and glslang-tools is added to
the Linux CI step that had spirv-tools only.

That costs about 1.3 MB of generated hex. Generating at build time would remove
it at the price of a glslang dependency for every build.

No renderer change: nothing includes these headers yet. The host side that
records the passes is the next commit.

Checked here: all eleven compile and pass spirv-val, the check passes, and it
fails as it should on a changed word in a header, a changed compile option and
a missing header. python -I -B tests/run_all.py: 90 passed, with the same 8
failures and 12 skips as upstream/main (a missing clang++ or SPIR-V headers).

Assisted-by: Claude Opus 5
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The renderer hands FSR tonemapped, square-root encoded colour (LDR), a
standard D32 depth and render-pixel motion, so build the passes that way
(HDR input off, inverted depth off) instead of converting the inputs.
Regenerate the eleven headers, make --check ignore glslang's version
comment (same SPIR-V) and pin the options in tests/test_fsr3_shaders.py.

The note in third_party/fidelityfx-sdk no longer points at
docs/upscaling.md, which this series does not bring in.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backend runs on the frame chain NotRexed#1008 brings (dlss_dispatch,
fsr4_evaluate, FsrJitter, opts.fsr4), so it needs that pull request until
it lands. Merged rather than rebased so its commits keep their hashes and
drop out of this diff once it is merged.

The conflict in gpu/vk/vk_menu.cpp keeps both sides: help_of from NotRexed#1008
and kMenuWidth = menu_hit::kWidth from NotRexed#946.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first half of the backend: what an evaluation needs before one can be
recorded. gpu/vk/vk_fsr3.cpp builds the eleven pass pipelines from the SPIR-V
the previous commit checked in, the two samplers every pass declares at
bindings 1000 and 1001, and the SDK's eighteen internal images.

The resource table and the per-pass binding lists are written out in the SDK's
order rather than derived, because they are the SDK's and not ours:
ffx_fsr3upscaler.cpp fixes the formats and sizes, and each pass file numbers
its own bindings. One wrong entry is a validation error at best and a wrong
picture at worst. The one place that knowingly departs from the SDK is the luma
history, allocated R8G8B8A8_UNORM because that is what the pass files declare
as its storage format, where the SDK allocates RGBA16F -- a storage format
qualifier has to match.

supported() asks once whether the device has push descriptors and every
storage format the images need, so a device that cannot run the passes says so
before an allocation fails mid-frame rather than after. prepare() re-makes the
images when the render or upscale size changes and tears everything down rather
than leaving a half-built set behind.

Nothing calls this yet, so there is no behaviour change.

Builds clean with RX_VK_ONLY=1, 0 warnings. Linux, RTX 5060 Ti, driver
615.71.09.

Assisted-by: Claude Opus 5
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The other half: the constants, the clears, the barriers and the nine
dispatches, following the SDK's ffx_fsr3upscaler.cpp rather than inventing an
order. cbFSR3Upscaler is laid out field for field with a static_assert on its
148 bytes, because every pass reads it by offset and a field out of place is a
wrong picture rather than a compile error.

What is faithful to the SDK and worth checking in review: the jitter phase
count moves one step a frame toward 8*(ratio^2) instead of jumping; the SPD
counter, its mips and the previous-nearest-depth scratch are cleared every
frame while the accumulation and frame info are cleared only on a reset; the
Lanczos weights are built once and copied in; the two ping-pong sets swap on
parity at the end; and RCAS only runs when sharpening is asked for, otherwise
the sharpening accumulate variant writes the output directly.

Three things specific to this renderer rather than the reference. Barriers are
synchronization2, which is what this tree loads (vkCmdPipelineBarrier is not in
vk_loader.h). The constant blocks live in one host-visible mapped buffer with a
set of slots per frame in flight, so writing one frame cannot disturb a frame
still being read. And descriptors are pushed per pass, with the two samplers at
the bindings the SDK's GLSL fixes them to.

upscale() refuses rather than guesses. No colour, depth or motion is a refusal,
and so is a missing near plane or field of view: device_to_view_depth and
tan_half_fov are built from them, and a guessed camera makes disocclusion wrong
in a way that reads as a renderer bug. Nothing calls this yet, so there is no
behaviour change.

Untested beyond compiling: builds clean with RX_VK_ONLY=1, 0 warnings, Linux,
RTX 5060 Ti, driver 615.71.09. No pass has executed, so nothing here is claimed
to produce a correct image yet.

Assisted-by: Claude Opus 5
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reference implementation documents that device_to_view_depth only scales
FSR's disocclusion thresholds and uses a typical near plane and field of view,
so refusing without the real camera was stricter than the evidence warrants.
Inputs defaults both instead.

Assisted-by: Claude Opus 5
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read the camera for finite standard depth, bind the SDK's default exposure
(an image of zero reads as 1.0), and use the no-wave pyramids where the
device lacks the subgroup operations. The images of an old size were
destroyed while in flight and the new ones never left UNDEFINED: retire
them and restart the history instead. Add available() and reason() so the
menu can show why it cannot run.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FSR 3.1 now runs on the chain FSR 4.1 uses (dlss_dispatch), from the same
colour, depth, motion and jitter, with near, far and field of view read
from the camera matrix; the output goes through dlss_output.comp as
before, and the history restarts whenever the engine changes.

The choice is Off, Auto, FSR 4.1 or FSR 3.1. Auto takes FSR 4.1 when AMD's
library is present with a model for the card, else the built-in FSR 3.1,
else the game's upscale; FSR 4.1 failing in play falls back to FSR 3.1.
gpu/vk/upscale_engine.h holds that choice and the camera read, plain
data so tests/upscale_engine_test.cpp checks them without a game or a
GPU.

RX_UPSCALER and upscaler= are the new keys; RX_FSR4=0/1 and an old ini's
fsr4= still work (Off and Auto). The menu row is still the AMD FSR 4.1
row, whose On is now Auto: the next commit makes it the four-value row.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the AMD FSR 4.1 row with an FSR upscaler row: Off, Auto, FSR 4.1
and FSR 3.1, changed at once. The row shows the engine in use and why a
forced choice cannot run. Document the row and the two engines in the
readme and in docs/upscaling.md.

Assisted-by: Claude Opus 5.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hckrman101

Copy link
Copy Markdown
Collaborator

Hi! This PR has merge conflicts with main now (a lot has landed today). Could you merge main in or rebase and resolve them? Happy to give it a proper review once it's up to date.

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