AMD FSR 3.1 upscaling and frame generation (split from #1335) #1386
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!1386
Loading…
Reference in a new issue
No description provided.
Delete branch "pr/fsr3-upscale-framegen"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
AMD FSR 3.1 upscaling and FSR 3.1 frame generation, split out of #1335 as asked. Nothing else from #1335 is in it.
dlss=3inhookshot_options.ini; 1 and 2 keep meaning DLSS and DLAA). It takes the gameplay upscale network's dispatch: the same dispatch DLSS takes, found the same way.third_party/fidelityfx-sdkand compiled intogpu/vk/shaders/fsr3_*_spv.h. Every pass is a dispatch of ours. FSR 3.1 needs no AMD runtime and no DLL: nothing of AMD's is linked or loaded at run time, so it is in every build on every platform. AMD's runtime DLLs are only for FSR 4. FSR 4 is not in this PR (FSR 4.1 is #1008's).vk_dlss.cppis indlss_mode(). It used to clamp the row's value into 0..2, so 3 would have turned into DLAA. Now any value other than 1 or 2 means "not DLSS".glslang-toolstosource-regressions.yml's apt line, becausetests/test_fsr3_shaders.pycompiles every vendored pass again and checks the checked-in headers.Supersedes #1313: its commit is this PR's first commit, unchanged apart from one context line in
tests/run_all.pyafter the rebase.Commits:
build(vk): the vendored passes,scripts/fsr3_shaders.py, the 28 headers and their test (#1313)feat(vk): FSR 3.1 upscaling (vk_fsr3.*,vk_fsr3_inputs.*,fsr3_math.h,shaders/fsr3_prep.comp), the jitter in every liftfeat(vk): FSR 3.1 frame interpolation (vk_fsr3fi.*, the chain invk_framegen.cpp)feat(menu): the Upscaler row's FSR 3.1 value, the Frame generation hint, andreadme.mdWhy
Players have asked for FSR 3.1 frame generation, and #1335 was too big to review and test in one piece.
Inputs: what comes from #841 and what does not yet
All of FSR's inputs are made in one file,
gpu/vk/vk_fsr3_inputs.cpp, behind one function,prepare(), which returns oneFramestruct. Moving FSR 3.1 onto #1008's shared temporal inputs means changing that file alone.nx2/oot_jitter.pywithhos_dlss_jitter_clip. The fork's own jitter (a viewport offset invk_draw.cpp) is not here.nx2/lift.pynow installs the jitter in every standalone lift, not only in builds with NVIDIA's SDK.make_game.pyno longer setsOOT_DLSS_JITTER, andoot_jitter.wanted()is gone. The lift key coversnx2/, so the first build after this PR lifts the game again.tests/test_make_game_ngx.pyno longer checks the old switch.rx_option(HOS_OPT_DLSS_JITTER_W/_H/_PHASES)returnsdlss_jitter_size(), or FSR'sfsr3_inputs::jitter_size()(the scene's size while FSR has drawn in the last 30 frames). The phase count isdlss_jitter_phases(), the host's own sequence length, and FSR is given the same count.RX_VK_FSR_JITTER=0turns it off for a run.vk_dlss.cppdoes. It usesdlss_math.h'sjitter_to_pixelsandunjitterto convert the offset and take it off the motion.HOS_OPT_DLSS_JITTER_*,hos_dlss_jitter_clip) are kept, so its interfaces do not churn under #1008.nx2/oot_camera.pyandhost/hos_oot_camera.care not here.dlss_note_scene_draw.fsr3_math.hderives FSR'sdevice_to_view_depthfactors andtan(fov/2)from the projection at +0x70. This was checked in a run:p22 -1.0002, p23 -0.20002gives near 0.1, far 1000.5 and a 45.00 degree field of view.tests/test_fsr3_math.cppchecks the factors against FSR's own view-distance formula, for both depth directions and for finite and infinite far planes.shaders/fsr3_prep.compis the fork's camera-motion pass, reworked todlss_prep.comp's math: reproject the depth from this frame's jittered clip space to the previous frame's unjittered one, and take this frame's jitter off. The same pass also writes FSR's colour and depth copies.vk_dlss.cppbehindHOOKSHOT_NGX, so a build without NVIDIA's SDK has none of it. Moving it out from behind the gate is #1008's refactor (vk_temporal.cpp), and this PR does not duplicate it.vk_fsr3_inputs.cpp'sprepare()takes #1008's inputs and FSR gets the actor motion.dlss_out.comp, the same pass DLSS's picture goes through.Left out of the fork's version:
upscale_seam.h,upscale_contract.h,upscale_format.hand their tests (the stand-in takes the dispatch the waydlss_stand_indoes)RX_VK_FSR_STAGE, the VP hunt, the MRT census)Fixed in the port:
vk_work.h, sofull_barriercould leave out a barrier after their work.Frame generation
The chain in
vk_framegen.cppis: FSR 3.1, then the renderer's own blend for any present FSR does not take (no inputs for that present, a reset, a GPU that cannot run the passes).RX_VK_FSR_FG=0turns FSR's interpolation off for a run (=1forces it on over the row).GPUs and drivers
FSR 3.1 is offered where the device has what the passes need. The Upscaler row is shown on such a GPU even in a build without NVIDIA's SDK. These are on top of the renderer's own floor (Vulkan 1.3: dynamic rendering, synchronization2, timeline semaphores).
fsr3::supported()):VK_KHR_push_descriptorwithmaxPushDescriptorsof at least 15shaderStorageImageExtendedFormatsandshaderStorageImageWriteWithoutFormatfsr3fi::capable()):shaderStorageImageReadWithoutFormat,shaderStorageImageWriteWithoutFormatandshaderStorageImageExtendedFormatsThe log says which way it went:
vk: fsr3: AMD FSR 3.1 can run on this GPU, orvk: fsr3: not on this GPU: <why>, and the same forfsr3fi. Tested only on an NVIDIA RTX 5060 Ti with driver 615.71.09 on Linux.Testing
Linux, RTX 5060 Ti, driver 615.71.09.
D=<deps>isbuild-linux/deps.Builds (both link):
EMSDK=$D/emsdk RX_CPM=$D/cpm OOT_WORK=... RX_MARCH=x86-64-v3 RX_VK_ONLY=1 python -I nx2/build.py --standalone --jobs 8: compiled, 0 failed, linkedhookshot_vk.so(no NVIDIA SDK)HOOKSHOT_NGX_SDK=<ngx-sdk>: compiled, 0 failed, linked ("with NVIDIA DLSS")Tests:
0828863): the renderer builds and links without NVIDIA's SDK, andtests/run_all.py(with the CPM headers found, so the SPIR-V and Vulkan header tests run too) gives 202 passed, 0 failed, 5 skipped.
tests/run_all.pyon main (a116390): 180 passed, 0 failed, 19 skippedtests/run_all.pyon this branch: 182 passed, 0 failed, the same 19 skipped. The two new ones aretest_fsr3_shaders.pyandtest_fsr3_math.py.75a676e: 166 / 168 passed, 0 failed, 17 skipped.Whole game:
make_game.py --update <a copy of the game folder> --jobs 10built the whole game from this branch in 10 minutes. The lift loggeddlss jitter: 3 of 3 sitesin a build without NVIDIA's SDK. A run of that build (FSR 3.1 + 2x, pacing on) gave a game fps of 58.4 and 117.2 frames shown a second, with FSR 3.1 making 600 of 600 frames and no refusal lines.In game:
HOS_INPUT_FRAME_CLOCK=1 HOS_REALTIME_STEP=0 RX_VK_PRECOMPILE=0, the sameHOS_INPUT_SCRIPTin every arm, and a stop at 6000 presents. The game's fps is the mean of thevk: N presents, X fpslines between presents 4800 and 6000.HOS_HEADLESS: frame generation is off without a window (framegen_on()checks!dev.offscreen).75a676eontoa116390. The FSR code is the same; the rebase only resolved conflicts with frame generation Auto, the menu andrun_all.py. The whole-game run above is on the rebased branch.RX_VK_PASS_TIMING=1: FSR 3.1's seven passes take 0.60 ms a frame, against the game's network's 0.078 ms. Frame interpolation's 17 passes take 1.17 ms a frame (0.61 ms of that is the optical-flow search).vk: fsr3 1536x864 -> 2048x1152 with assembled depth and camera motion (network fd029a317a9a4b94 ...)vk: fsr3 inputs: ... near 0.1000 far 1000.53 fov 45.00 deg, depth 0 at the near plane: reversed for FSRvk: fsr3: N frames drawn ... this one's jitter +0.312 -0.352 px of 14 phases(the jitter changes every frame)vk: fsr3fi: 600 of the last 600 made frames by FSR 3.1Visual check:
RX_VK_SHOTS+RX_VK_SHOT_MADE=1(a shot every 150 presents, 6000 presents): FSR's frames and FSR's made frames look right, with correct colour and no garbage, and the renderer's own flash check wrote nowhite_*orflash_*file.RX_VK_DUMP=5400: the HUD pass after the upscale (step 266) draws over FSR's picture at 2048x1152 and looks right.Not covered
🤖 Generated with Claude Code
This landed in the v2.0.12 branch (merged there for testing; it reaches
mainwith that release), so closing. Thanks!Reopened: closing this was a mistake on my side. It was tested in the v2.0.12 branch, and it will be merged into main here, so it shows as merged. Sorry for the noise.
Thanks for splitting this out, it's much easier to follow than #1335. The FSR side reads well: everything comes from AMD's vendored GLSL with no runtime, the binding tables and constant blocks have size asserts, old images are freed a few frames later instead of right away, the passes are wrapped in
full_barrier/descriptors_disturbed, andfsr3_math.hchecks out. I worked through the near/far/fov derivation for the p22/p23 in the description and got 0.1 / 1000.5 / 45 deg, same as you. I didn't find a sync or lifetime bug invk_fsr3.cpp,vk_fsr3_inputs.cpporvk_fsr3fi.cpp. What's left is about landing it:CI. The only failure is
Python source checks (ubuntu-24.04)->tests/test_dlss_actor.py:shaders/dlss_actor_frag_spv.h is not made from dlss_actor.frag as it is now. Nothing in the PR touches that shader. The newglslang-toolsapt line is what triggers it: that test compares the DLSS headers against a fresh compile only whenglslangValidatoris on PATH. The runner now gets Ubuntu's glslang 15.1.0, but main's headers were built with 16.6.0 (// 1116.6.0), and 15.1 produces different words fordlss_actor.frag. A local glslang 16.6.0 rebuilds main's header byte for byte, so main is fine. Your owntest_fsr3_shaders.pypasses on 15.1. So the failure comes from this PR's workflow change, not from its code. Possible fixes: pin a glslang that matches the checked-in headers (a 16.x release tarball, say), maketest_dlss_actor.pyskip or ignore the generator on a version mismatch the way yourstrip_generatordoes (that helps only if the words match), or run the FSR header check in its own step with its own glslang. The other checks were still pending when I looked; the DLSS Windows link check and the AppImage passed.Conflict with main. One hunk, in
vk_replay.cpp'srx_optionchain: main addedHOS_OPT_GYRO_DEADZONEon the same line. Keep main's... id == HOS_OPT_GYRO_DEADZONE ? opts.gyro_deadzone :and then yourjitter_option(id)line. Nothing else conflicts. The PR adds no new option ids; FSR 3.1 is value 3 under the existingdlsskey, so the id list (0-9, 16-22, 24-26, 32-35, 4096+, and 10/11/23/27/28 in other open PRs) isn't affected.How this fits with #1008. These two can't merge as they stand. They're close in design, though, and that's mostly to your credit: you kept FSR's inputs behind
fsr3_inputs::prepare()so they could move over later.dlss=3for FSR (4.1) anddlss=4for FSR native AA (#1469 uses the same 3/4). This PR usesdlss=3for FSR 3.1. Whichever lands second has to renumber, or a saveddlss=3silently changes meaning. If #1008 goes first (it's approved), FSR 3.1 would be value 5.RX_VK_FSRis FSR 4 (=0off,=1/=2over the row). HereRX_VK_FSR=1forces FSR 3.1. One of them needs another name, e.g.RX_VK_FSR3.TemporalFrameis almost exactly yourfsr3_inputs::Frame: colour as the network samples it in RGBA16F, R32F depth not reversed, RG16F motion in render pixels with the jitter removed, the jitter in render pixels, reset, and near/far/fov. On top of #1008, FSR 3.1 becomes a thirdTemporalEngine.evaluateisfsr3::upscaleplus your 1 - d depth flip (a tiny pass, or a flag infsr3_prep.comp).startandpreparewrapsupported()and the image setup, andreleasedrops the images.vk_fsr3_inputs.cppandfsr3_stand_inthen go away, and FSR 3.1 picks up #1008's actor motion for free. Thedlss_mode()change andjitter_optionalso go away, because #1008 hastemporal_mode()andtemporal_jitter_size().fsr3_inputs::take(). That would be a smalltemporal_last_frame()hook invk_temporal.cpp, and it would also have to run when the Upscaler is the game's own, since FI-only mode needs the inputs then too.oot_jitter.wanted()and setsOOT_DLSS_JITTERfor NGX or--fsrbuilds. This PR installs the jitter in every lift. That's fine and probably what #1008 wants once an SDK-free engine exists, but it's a conflict to resolve inlift.py,make_game.pyandoot_jitter.py.So the cheapest order is probably #1008 first, then this rebased as an engine on it. That also settles #1469, which adds FSR 3.1 a second way: one of the two should go. I'd ask the maintainer which order they want before you do the rebase.
One behaviour point (inline): Frame generation at 2x switches every capable GPU over to FSR's interpolation with no menu option to turn it off.
Nits:
jitter_optioninvk_replay.cppis a new non-static function at namespace scope, so make itstatic.make_image(g.spd_mips, ..., std::max(spd_levels, kSpdStoredMips))would ask for more mips than the image allows if half the render size were under 32 px. That can't happen behindnote_scene_draw's 640x360 floor, butstd::minwith the real chain length (asvk_fsr3fi.cppdoes) is safer.@ -58,3 +58,3 @@if: runner.os == 'Linux'run: sudo apt-get update && sudo apt-get install -y g++ clang spirv-toolsrun: sudo apt-get update && sudo apt-get install -y g++ clang spirv-tools glslang-toolsThis is what fails CI. With glslang on PATH, main's
tests/test_dlss_actor.pyalso compiles the DLSS shaders and compares the words. Ubuntu 24.04'sglslang-toolsis 15.1.0, but main's headers come from 16.6.0, anddlss_actor.fragcompiles differently between them (16.6.0 reproduces main's header exactly). Either pin a glslang that matches the checked-in headers, or keep this check from turning on a version-sensitive one.@ -9,0 +10,4 @@enum { GAME = 0, DLSS = 1, DLAA = 2, FSR3 = 3, kUpscalers = 4 };/// One bit per upscaler that can run here (1 << GAME always): what the row steps throughinline unsigned bit(int upscaler) { return 1u << upscaler; }/// The DLSS and DLAA bits, for a caller that knows only whether DLSS runs#1008 (approved) and #1469 both already give
dlss=3to FSR 4 anddlss=4to its native AA. Whichever lands second has to renumber, or a saveddlss=3changes meaning between builds. If #1008 goes first, this would beFSR3 = 5.This takes over 2x for everyone on a capable GPU, including players who never picked FSR as the Upscaler. Your own numbers show it costs about 1.2 ms of GPU time plus the prep pass, and 1-3 fps with pacing off. The only way out is
RX_VK_FSR_FG=0. I'd make it opt-in: either a value on the Frame generation row, or only when the Upscaler is FSR 3.1. Then a player who's happy with the current 2x doesn't get a different interpolator after an update.@ -0,0 +8,4 @@// Those inputs are made in vk_fsr3_inputs.cpp; fsr3_upscale below is the dispatch's side: the network's input in, FSR's// picture squared back into the network's output, as vk_dlss.cpp's dlss_out.comp does for DLSS.//// RX_VK_FSR=0: never (the row stays), =1: FSR 3.1 over the Insert menu's Upscaler row.RX_VK_FSRis already FSR 4's in #1008 (=0off,=1/=2over the row). Could this one beRX_VK_FSR3so the two don't fight once both are in?