Repository navigation
feat(vk): FSR 3.1 as an engine of the temporal chain, with the characters' motion - #2305
Conversation
…ters' motion FSR 3.1 made its own inputs (vk_fsr3_inputs.cpp) with the camera's motion alone, so everything that moves with the camera, Link above all, got the motion of what is behind it and smeared while he walked. DLSS and FSR 4 already draw the skinned draws' own motion on vk_temporal.cpp's chain. FSR 3.1 is now a third engine of that chain (slot 2, kChainEngine in vk_fsr3.cpp): the chain's colour, depth (reversed for FSR's passes where the projection puts the near plane at 0: dlss_prep.comp flag bit 3), motion with the actor pass drawn over it, its jitter and phase count, and the unjittered projection for FSR's depth constants. The passes' picture is copied into the chain's output and squared back by dlss_out.comp as before. - A frame whose evaluation fails is left to the network for that frame only (TemporalEngine::retry), not for good: FSR 3.1's constants come from the frame's stream. - Retired images are freed before each prepare on the chain too, and the engine's release lets FSR 3.1's images go when the chain stops drawing with it (before, they stayed after switching to another upscaler). - RX_VK_FSR_JITTER=0 still turns its jitter off on the chain. - A NaN in the scene colour is written as black (fsr3_prep.comp's rule). - RX_VK_FSR3_CHAIN=0: its own inputs, as before. Tests: temporal_jitter_test covers FSR 3.1's slot, its failure gate and RX_VK_FSR_JITTER=0 on the chain (a new run, "fsr-jitter-off"); test_upscaler_rows checks that the row's FSR 3.1 runs on the chain before the fallback to the game's own and that fsr3_wanted is off while it does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With FSR 3.1 on the chain, the prep writes 1 - d into the chain's depth (bit 3, for FSR's passes). Anything else that reads that frame's depth, such as frame generation or other passes, cannot tell. TemporalFrame::depth_reversed now says so, set from the same fsr_flip as the prep's bit, so readers can pass the right depth convention on instead of assuming plain depth. test_upscaler_rows checks that temporal_upscale sets it from fsr_flip wherever it reverses the depth. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/fsr3-temporal-chain # Conflicts: # docs/tested-configurations.md
…/fsr3-temporal-chain
…/fsr3-temporal-chain # Conflicts: # gpu/vk/vk_temporal.cpp
NotRexed#2118's DLSS Frame Generation (NGX) copies the chain's depth each frame and told NGX it was not reversed. With FSR 3.1 on the chain the depth is 1 - d (dlss_prep.comp bit 3), so generated frames under FSR 3.1 would have had their occlusion backwards. The frame's depth_reversed now goes with the copied inputs (Fg::depth_reversed) and into NVSDK_NGX_DLSSG_Opt_Eval_Params::depthInverted. DLSS and FSR 4 are unchanged (their depth is not reversed). test_upscaler_rows checks both lines; it fails with the hard-coded false. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/fsr3-temporal-chain
…/fsr3-temporal-chain # Conflicts: # docs/development.md # docs/tested-configurations.md # gpu/vk/shaders/dlss_prep.comp # gpu/vk/shaders/dlss_prep_comp_spv.h # gpu/vk/vk_temporal.cpp # gpu/vk/vk_temporal.h
hckrman101
left a comment
There was a problem hiding this comment.
Thanks, this is a clear improvement: FSR 3.1 finally gets the skinned draws' motion, and the motion-vector screenshot makes the case well. I went through the inputs and they line up with what FSR 3.1 got on its own path:
- Motion: the chain's motion is in render pixels toward the frame before with the jitter taken off, the same convention as
vk_fsr3_inputs.cpp, andchain_evaluateleavesmotion_scaleat 1, somotion_vector_scaleis 1/render size as before. Low-res motion, no display-res mixup (FSR 3.1 interpolation's #1943 question doesn't apply here, since FG keeps its own inputs). - Jitter: same
jitter_to_pixels, and the phase count comes from the samedlss_jitter_phasesthe chain publishes.RX_VK_FSR_JITTER=0still works, the jitter gate has its own slot, andvk_fsr3_inputs' own jitter stays off while FSR 3.1 is on the chain (it is gated onfsr3_wanted), so there's no double jitter. - Depth: the flip uses
camera_from_projectionon the unjittered projection, the same test the own path makes. Motion is still computed from the unflippedd,actor_motionreads the game's depth view rather thans.depth, anddepth_reversedreaches NGX frame generation. - History: cuts and gaps reset through the chain's existing reprojection/gap logic. A change of engine releases FSR's images, and
prepare_sizesthen returns 2, so the next frame resets. Theretrypath clearslast_frame, so the frame after a failed evaluation starts a new history. - #2166 still works on the chain: in
temporal_rejected_network,fsr3_upscalenow returns false andtemporal_wanted()is true for FSR3, so left-out networks go throughtemporal_upscale. (Nit: that path's log line will then say "the temporal upscaler" rather than FSR 3.1.) - #1654's LOD bias:
upscale_lod_biasfalls through totemporal_mode(), which now returns FSR3, so the bias is unchanged. Same for the menu'supscaler(). - DLSS and FSR 4: unchanged apart from the NaN scrub in
dlss_prep.comp(harmless, arguably a fix) and bit 3, which is never set for them. - FSR 3.1 interpolation (
RX_VK_FSR_FG=1): it keepsvk_fsr3_inputsas it already does while DLSS upscales, so this is the same pairing as DLSS + FSR FG. Fine as untested, but a quick run would be nice.
What blocks merging right now:
- It conflicts with main. GitHub reports the PR as not mergeable. #2166 is already in your base, so that part is fine, but #2342 (FSR 4 native HDR, merged since) touches the same lines:
dlss_prep.comp's colour store (max/kSquare/kColorMax) and its SPIR-V header, thepc.flagsline and the prep/out pipeline selection intemporal_upscale,TemporalEngine(hdraftermemory),TemporalFrame's comment, and the docs rows. Your merge notes for #2243 apply to #2342 almost as written:- In the shader, scrub the NaN before the
max(e, 0). GLSL'smaxwith a NaN operand is undefined. - Keep
kChainEngine's designated order valid (.hdrsits before.fsr_depth). - Regenerate
dlss_prep_comp_spv.h. - Check that bit 3 reaches
prep_hdrtoo.
- In the shader, scrub the NaN before the
- CI hasn't run. Only the title check ran, because there is no merge ref while the PR conflicts. Please rebase so the build and
tests/run_all.pyrun on Windows and Linux.
Smaller points:
- #1653 (reactive mask, still open) attaches its mask in
fsr3_upscale, which is now only theRX_VK_FSR3_CHAIN=0path. Whichever of the two lands second needs to carry the mask throughTemporalFrameintochain_evaluate, or FSR 3.1 loses it by default. Good that you flagged this in the merge notes. - Since this becomes the default for FSR 3.1, which is the upscaler for GPUs without DLSS/FSR 4 (and the Steam Deck default with #2110), one frame-time number on a weaker GPU or iGPU would help. The chain adds the actor-motion pass and a display-size copy. That costs nothing measurable on a 9070 XT, but low-end hardware is where FSR 3.1 is used.
The screenshots and numbers are there, thanks. The 67% flicker and outline numbers are convincing. Once it's rebased and CI is green I'd be happy to approve.
…/fsr3-temporal-chain
…ut network's log names it - vk_fsr3.cpp: fsr3::memory() adds up the internal images it made (history, luma, the RGBA16F output and the rest), and kChainEngine's memory reports it, so the chain's status line shows FSR 3.1's own memory instead of nothing (217 MB at 2560x1440 native, measured). - vk_draw.cpp temporal_rejected_network: a left-out network drawn by FSR 3.1 on the chain is logged as "FSR 3.1 (on the temporal chain)", not "the temporal upscaler". Review notes on NotRexed#2305 (hckrman101). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/fsr3-temporal-chain
|
Thanks for the thorough read. Where each point stands:
|
…/fsr3-temporal-chain # Conflicts: # docs/tested-configurations.md
… mask NotRexed#1653 attached its reactive mask in fsr3_upscale, which with this PR runs only with RX_VK_FSR3_CHAIN=0: by default FSR 3.1 drew on the chain without it while RX_VK_REACTIVE=1 still copied the scene every frame. chain_evaluate now makes the mask the same way DLSS's evaluation does on this chain (the chain's render size and input image) and hands it to FSR's passes as Inputs::reactive; without one, FSR's default reactivity, as before. test_reactive_mask.py checks that chain_evaluate makes the mask before fsr3::upscale; it fails without the call. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…/fsr3-temporal-chain
…/fsr3-temporal-chain # Conflicts: # docs/tested-configurations.md
…/fsr3-temporal-chain
Joey-Magnus
left a comment
There was a problem hiding this comment.
Every earlier blocker is handled: #2342 merged in, NaN scrubbed before max, kChainEngine order valid, SPIR-V header regenerated, bit 3 reaching both prep pipelines, and #1653's reactive mask carried into chain_evaluate. The retry path drops one frame, and projection failures are filtered before evaluate, so it doesn't spin. The 11 temporal/FSR/DLSS checks pass locally.
If you have one, a Steam Deck or iGPU frame-time number would help, since FSR 3.1 is the low-end upscaler and #2110 would make it the default.
Upstream's 30 commits since the last merge. AMD FSR 3.1 is an engine of the temporal chain now; vk_fsr3_inputs.cpp's own frame is still made for what is not on the chain (FSR 3.1's frame interpolation, AMD FSR 4's frame generation, the open neural rendering network's motion). The fork's side: - the Frame generation by row takes a value asked outright (the control bridge) where it runs here, not only the next one from the arrows (framegen_methods.h pick); - menu_init's moves of two older fork rows are a function of their own (fork_options_moved), which the checks that cut menu_init can stub; - the readme's fork sections sit after the menu's own, under one heading. Run in the game again on this tree (RTX 5060 Ti, Linux): FSR 3.1 on the chain, DLSS Frame Generation from FSR 3.1's inputs (600 of 600 sets) and from DLSS's at 3x (600 of 600), AMD FSR 3.1's interpolation with either upscaler (600 of 600), and the open neural rendering network at 50% before DLSS Frame Generation (600 of 600). Tests: 427 passed, 20 skipped (Windows-only or missing tools). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
AMD FSR 3.1 makes its own inputs (
vk_fsr3_inputs.cpp), and the only motion it gets is the camera's. So anything thatmoves differently from the camera is given the motion of what's behind it: Link while he walks, his swinging arm, his
shield, and the people around him. DLSS and FSR 4 already get those characters' own motion on
vk_temporal.cpp's chain.This PR puts FSR 3.1 on that chain too, as a third engine, so all three temporal upscalers share the same inputs.
What changed
vk_fsr3.cpp/.h:kChainEngine, FSR 3.1 as aTemporalEngine(slot 2).dlss_out.compas before.fsr3_chosen()is "the row orRX_VK_FSR=1picks FSR 3.1".fsr3_wanted()now means "FSR 3.1 on its own inputs",which is false while it's on the chain.
fsr3::prepare_sizesandfsr3::releaselet the chain size and free FSR's images.vk_temporal.cpp/.h:temporal_modereturnsFSR3for the row's FSR 3.1 before it falls back to the game's own;engine_ofgets a thirdslot.
TemporalEnginefields:fsr_depth: the chain reverses depth for FSR's passes (built withINVERTED_DEPTH) where the projection puts thenear plane at 0.
retry: a failed evaluation hands that frame to the network instead of turning the engine off for good. FSR 3.1'sconstants come from the frame's stream, which can run out for one frame.
TemporalFramecarries the unjittered projection and the jitter phase count.TRANSFER_DST.RX_VK_FSR_JITTER=0still turns FSR 3.1's jitter off on the chain.shaders/dlss_prep.comp:1 - d).fsr3_prep.comp's rule, because one NaN spreads through anupscaler's history.
glslangValidator -V --target-env vulkan1.3 --vn DLSS_PREP_COMP_SPV, the commandtest_dlss_actor.pychecks.RX_VK_FSR3_CHAIN=0gives FSR 3.1 its own inputs, as before, for comparisons (docs/development.md). The readme's FSR 3.1section describes the new inputs, and says what still trails: things that aren't skinned, such as Navi's body.
temporal_jitter_test.cppcovers FSR 3.1's slot and its failure gate, and a new run (fsr-jitter-off) where onlyRX_VK_FSR_JITTER=0is set: FSR 3.1 on the chain gets no jitter, and DLSS and FSR 4 keep theirs.test_upscaler_rows.pychecks that the row's FSR 3.1 runs on the chain before the fallback to the game's own, thatfsr3_wantedis off while it does, and thatengine_ofhas its slot.fsr-jitter-off.Testing
python -I -B tests/run_all.py: 363 passed, 0 failed, 11 skipped (Linux; the skips are Windows-only, need a Vulkan 1.3device, or need dotnet).
In game: RX 9070 XT 16 GB, Mesa RADV 26.2.6, Gentoo Linux (kernel 7.3.3), KDE Plasma Wayland, windowed, 2560x1440
output, 60 fps cap.
RX_VK_FSR3_CHAIN=0, which is what main does.vk: fsr3 on the chain: 600 frames drawn ... depth reversed for FSR by the chain, with noevaluation failures.
Market runs vary by about 0.9 fps between runs, so the chain costs nothing measurable.
Picture: consecutive 2560x1440 shots at Kokiri's lost-woods path, standing and walking, compared on the same build.
Edge flicker is the mean frame-to-frame change of edge pixels, 0-255, while standing.
Four independent reviewers of the walking sheets saw no visible difference at 100%. At 67%, they found the chain
slightly better: smoother edges on Link's shield and tunic hem, and slightly softer shield texture. No new artefacts.
The clearest evidence is the motion field itself (below). On main, Link isn't in it at all.
Not tested:
RX_VK_FSR_FG=1), which keeps usingvk_fsr3_inputs.cpp;Screenshots (from the game's output image before the present path, 2560x1440)
The motion vectors the upscaler gets while Link walks (
RX_VK_DLSS_DEBUG=motion). Left: camera motion only(
RX_VK_DLSS_ACTORS=0, which is what FSR 3.1 got on main). Right: on the chain, with Link's body, arm and shield carryingtheir own motion:
Link walking: own inputs vs the chain, at 100% and at 67% (2x zoom crops):
Merge notes (from test merges against every open PR that touches these files; textual conflicts show up in git, semantic ones don't)
TemporalFrame::depth_reversedsays so. Anything that reads the chain'sdepth must check it rather than assume plain depth:
depth_reversedtoNGX's
depthInverted, andtest_upscaler_rowschecks it.depthInvertedfromdepth_reversed, not ahard-coded false.
nr_prepassess.depthon, and the NGX backend'sDepthInvertedshould follow it too.bool native_aatoTemporalEngine::prepare.chain_prepareinvk_fsr3.cppneeds the sameparameter, or the build breaks. git merges the two cleanly. FSR 3.1 never has native AA.
chain_evaluatethe same way DLSS does on the chain, and
test_reactive_mask.pychecks it. Checked in game withRX_VK_REACTIVE=1: the mask ismade, and FSR 3.1 draws all 600 frames on the chain.
dlss_prep.compscrubs NaN first, then applies feat(dlss): DLSS for the game's native HDR, given linear light #2243'smax/kSquare/min, and keeps theflags & 8depth flip.The header was regenerated; glslang 16.4.0 gives the same SPIR-V words as CI's 16.6.0 for
dlss_prepanddlss_out.TemporalEngineis orderedmemory,hdr,fsr_depth,retry: DLSS's and FSR 4's engines are positional,and
kChainEngineis designated.hdryet, so it keeps the LDR pair. Giving it HDR colour on feat(dlss): DLSS for the game's native HDR, given linear light #2243's mechanism is a follow-up.fsr3_wanted()to ask whether FSR 3.1 is chosen. After this PR that question isfsr3_chosen(). Itsfsr4 = !fsr3_wanted() && fsr4_wanted()guard and its jitter guard should usefsr3_chosen(). The merge istextually clean, so git won't flag it.
vk_temporal.cpp, the output image's usage should becomeTRANSFER_DST | TRANSFER_SRC, and itsproj[16]andthis PR's
projection[16]should become one field.RX_VK_FSR3_CHAIN=0.docs/tested-configurations.mdanddocs/development.md: rows next to other PRs' rows; keep both.🤖 Generated with Claude Code