Repository navigation
fix(vk): FSR 3.1 draws the fixed Render resolution steps where the game's networks are left out - #2166
Conversation
…me's networks are left out On a GPU that leaves the game's cooperative-matrix networks out (any GPU but NVIDIA's, and NVIDIA's that multiply in software), each fixed share of the Render resolution row has a network that does not translate: 67%, 58%, 50% (72ba090c70fe2614) and 33%. Its dispatch goes to upscale_stub, which offered it to DLSS and FSR 4 (temporal_rejected_network) but never to FSR 3.1: with FSR 3.1 chosen, Quality, Balanced, Performance and Ultra Performance were a plain stretch of the 3D, no FSR 3.1 and no anti-aliasing, while the menu said FSR 3.1 (issue NotRexed#1829: RX 9060 XT; also seen on an RX 5500M at Performance: 768x432 stretched, not one fsr3 line in the log). temporal_rejected_network now offers the network to FSR 3.1 first, as fsr3_stand_in comes before temporal_stand_in for the networks that translate: its upscale, or its frame interpolation's inputs while another upscaler draws. upscale_stub calls it when FSR 3.1 is asked for too. fsr3_upscale already takes a network it does not know only when its input is this frame's scene and its output the picture, so anything else is stretched as before. tests/test_upscaler_rows.py: upscale_stub offers it with FSR 3.1 asked for, and FSR 3.1 is offered before the temporal upscaler (fails on main). tests/test_upscale_fallback.py: a stub for fsr3_inputs::wanted.
hckrman101
left a comment
There was a problem hiding this comment.
Thanks, this is a clean fix and the write-up makes it easy to follow.
I went through the path at 5bbfed6: upscale_stub now also calls temporal_rejected_network when fsr3_inputs::wanted(), and that offers the network to fsr3_upscale(..., known=false, ...) before temporal_upscale. That's the same order as fsr3_stand_in before temporal_stand_in, and the unknown-network guards in fsr3_upscale (B10G11R11 in and out, output larger than input, is_scene(in)) keep the movies and anything else on the stretch. With frame interpolation on and DLSS/FSR 4 drawing, fsr3_upscale makes the FI inputs and returns false, so temporal_upscale still draws, as on the translated path. With only FI wanted and no upscaler, it falls through to the stretch like before. The new (temporal_wanted() || temporal_off()) guard inside temporal_rejected_network keeps temporal_upscale from running when only FSR 3.1 brought us in. The new check in test_upscaler_rows.py (FSR 3.1 offered before the temporal upscaler, on the definition via rindex) is a good one. Leaving Ultra Performance for a follow-up is fine.
CI: the only red job is Windows Python part 1/2, and that's tests/test_recorder_sync.py (93 s), which this PR doesn't touch. Everything else passes, including the llvm-mingw build.
Overlap: #2305 renames fsr3_wanted to fsr3_chosen and, by default, sends FSR 3.1 through the temporal chain. After both land, the left-out networks reach FSR 3.1 through temporal_upscale anyway, and this path matters for RX_VK_FSR3_CHAIN=0 and the FI inputs. Both PRs edit test_upscaler_rows.py, in different hunks, so whichever lands second may need a quick rebase. #2193 (merged) and #1943 don't touch these lines.
Two comment nits inline. Not blocking.
| // GPU without cooperative matrices they are left out): the temporal upscaler is offered it first, as temporal_stand_in | ||
| // offers the networks that did translate. It takes only the scene into the picture; anything else goes on below | ||
| if ((temporal_wanted() || temporal_off()) && temporal_rejected_network(cmd, key, src, &in, dst, &out)) return true; | ||
| // offers the networks that did translate, and so is FSR 3.1, as fsr3_stand_in. It takes only the scene into the picture; |
There was a problem hiding this comment.
Nit: this still reads as if the temporal upscaler goes first and FSR 3.1 after it, but temporal_rejected_network now tries FSR 3.1 first. Maybe "FSR 3.1 is offered it first, as fsr3_stand_in, then the temporal upscaler, as temporal_stand_in".
| /// texture it samples, and its output, the largest image it writes): the Render resolution row's fixed shares (67% | ||
| /// 6676619641320ba7, 58%, 50%, 33%) are cooperative-matrix networks, left out without cooperative matrices (AMD RDNA 2), so | ||
| /// without this DLSS and FSR would only ever see the game's 3/4. True: the temporal upscaler wrote the output | ||
| /// 6676619641320ba7, 58%, 50% 72ba090c70fe2614, 33%) are cooperative-matrix networks, left out on a GPU that is not NVIDIA's or multiplies in |
There was a problem hiding this comment.
Nit: the wrap went odd here: this line is ~140 columns, and the next is a short software (vk_shader.cpp, RX_VK_NETWORK_KEEP), so. Worth rewrapping the three lines.
…ters' motion (#2305) AMD FSR 3.1 makes its own inputs (`vk_fsr3_inputs.cpp`), and the only motion it gets is the camera's. So anything that moves 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 a `TemporalEngine` (slot 2). - It takes the chain's colour, depth, motion (with the actor pass drawn over it), jitter and jitter phase count. - It builds FSR's depth constants from the unjittered projection. - Its picture is copied into the chain's output and squared back by `dlss_out.comp` as before. - `fsr3_chosen()` is "the row or `RX_VK_FSR=1` picks 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_sizes` and `fsr3::release` let the chain size and free FSR's images. - `vk_temporal.cpp/.h`: - `temporal_mode` returns `FSR3` for the row's FSR 3.1 before it falls back to the game's own; `engine_of` gets a third slot. - Two new `TemporalEngine` fields: - `fsr_depth`: the chain reverses depth for FSR's passes (built with `INVERTED_DEPTH`) where the projection puts the near plane at 0. - `retry`: a failed evaluation hands that frame to the network instead of turning the engine off for good. FSR 3.1's constants come from the frame's stream, which can run out for one frame. - `TemporalFrame` carries the unjittered projection and the jitter phase count. - The chain's output image gets `TRANSFER_DST`. - `RX_VK_FSR_JITTER=0` still turns FSR 3.1's jitter off on the chain. - `shaders/dlss_prep.comp`: - Flag bit 3 reverses depth (`1 - d`). - A NaN in the scene colour is written as black, following `fsr3_prep.comp`'s rule, because one NaN spreads through an upscaler's history. - The header was regenerated with `glslangValidator -V --target-env vulkan1.3 --vn DLSS_PREP_COMP_SPV`, the command `test_dlss_actor.py` checks. - `RX_VK_FSR3_CHAIN=0` gives FSR 3.1 its own inputs, as before, for comparisons (`docs/development.md`). The readme's FSR 3.1 section describes the new inputs, and says what still trails: things that aren't skinned, such as Navi's body. - Tests: - `temporal_jitter_test.cpp` covers FSR 3.1's slot and its failure gate, and a new run (`fsr-jitter-off`) where only `RX_VK_FSR_JITTER=0` is set: FSR 3.1 on the chain gets no jitter, and DLSS and FSR 4 keep theirs. - `test_upscaler_rows.py` checks that the row's FSR 3.1 runs on the chain before the fallback to the game's own, that `fsr3_wanted` is off while it does, and that `engine_of` has its slot. - Both fail on main. A mutated jitter gate fails `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.3 device, 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. - Host v2.1.0-main-local (source fc2ed0d0). Renderer: main 49f75dc plus this branch. - "Own" means the same build with `RX_VK_FSR3_CHAIN=0`, which is what main does. - Every chain run logged `vk: fsr3 on the chain: 600 frames drawn ... depth reversed for FSR by the chain`, with no evaluation failures. | FSR 3.1 | Kokiri Forest, 28 s camera turn (4 runs each) | Castle Town market at noon (3 runs each) | |---|---|---| | Own inputs (as main) | 59.2 fps, 1% low 46.4 | 53.1 fps | | On the chain | 59.3 fps, 1% low 48.1 | 52.5 fps | 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. | FSR 3.1 | Edge flicker, standing | 1-px outlines per MP | Halos per MP | |---|---|---|---| | 100%, own | 2.17 | 32 | 45 | | 100%, chain | 2.20 | 29 | 50 | | Quality (67%), own | 3.01 | 12 | 25 | | Quality (67%), chain | 1.67 | 2 | 9 | 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:** - other GPUs and drivers; - Windows; - Steam Deck, where #2110 would make FSR 3.1 the default; - frame interpolation (`RX_VK_FSR_FG=1`), which keeps using `vk_fsr3_inputs.cpp`; - switching upscalers repeatedly while playing (release and remake are code-reviewed, not exercised in game); - HDR: the chain still clamps FSR 3.1's colour at the paper white, as main does (see below). **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 carrying their 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) - **Depth is reversed for FSR 3.1 on the chain.** `TemporalFrame::depth_reversed` says so. Anything that reads the chain's depth must check it rather than assume plain depth: - #2118 (DLSS frame generation through NGX, now on main): done in this PR. Commit ca2e302 passes `depth_reversed` to NGX's `depthInverted`, and `test_upscaler_rows` checks it. - #1944 (DLSS frame generation through Streamline) needs the same: `depthInverted` from `depth_reversed`, not a hard-coded false. - #1831/#1833: `nr_pre` passes `s.depth` on, and the NGX backend's `DepthInverted` should follow it too. - **#2145** adds `bool native_aa` to `TemporalEngine::prepare`. `chain_prepare` in `vk_fsr3.cpp` needs the same parameter, or the build breaks. git merges the two cleanly. FSR 3.1 never has native AA. - **#1653** (reactive mask) is on main now and merged into this branch. Commit 7ceae45 makes the mask in `chain_evaluate` the same way DLSS does on the chain, and `test_reactive_mask.py` checks it. Checked in game with `RX_VK_REACTIVE=1`: the mask is made, and FSR 3.1 draws all 600 frames on the chain. - **#2243** (DLSS HDR, linear light) is on main now (in #2342's squash, a0dfd28) and is merged into this branch. - `dlss_prep.comp` scrubs NaN first, then applies #2243's `max`/`kSquare`/`min`, and keeps the `flags & 8` depth flip. The header was regenerated; glslang 16.4.0 gives the same SPIR-V words as CI's 16.6.0 for `dlss_prep` and `dlss_out`. - `TemporalEngine` is ordered `memory`, `hdr`, `fsr_depth`, `retry`: DLSS's and FSR 4's engines are positional, and `kChainEngine` is designated. - FSR 3.1 doesn't report `hdr` yet, so it keeps the LDR pair. Giving it HDR colour on #2243's mechanism is a follow-up. - **#1831**: - It reads `fsr3_wanted()` to ask whether FSR 3.1 is chosen. After this PR that question is `fsr3_chosen()`. Its `fsr4 = !fsr3_wanted() && fsr4_wanted()` guard and its jitter guard should use `fsr3_chosen()`. The merge is textually clean, so git won't flag it. - In `vk_temporal.cpp`, the output image's usage should become `TRANSFER_DST | TRANSFER_SRC`, and its `proj[16]` and this PR's `projection[16]` should become one field. - **#2166** only matters with `RX_VK_FSR3_CHAIN=0`. - `docs/tested-configurations.md` and `docs/development.md`: rows next to other PRs' rows; keep both. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Problem
On a GPU that leaves the game's cooperative-matrix networks out, FSR 3.1's own Render resolution steps never reach FSR 3.1. That covers any GPU that isn't NVIDIA's, and NVIDIA's that multiply in software. With FSR 3.1 chosen, Quality, Balanced, Performance and Ultra Performance draw the 3D as a plain stretch, with no FSR 3.1 and no anti-aliasing, while the menu still says FSR 3.1. #1829 reports it on an RX 9060 XT ("FSR 3.1 looks noticeably worse. I don't think the upscaler is working"), and I see the same on an RX 5500M.
OoT draws its 3D at a share of the picture and scales it up with a network of its own for each share. In the logs, with the picture at 1536x864:
9dc98871eb7fc101fd029a317a9a4b946676619641320ba7f10782f9e58b6a7c72ba090c70fe26140775215f8da720e6On these GPUs only the first two are kept by default (
vk_shader.cpp,RX_VK_NETWORK_KEEP). The others don't translate ("a network left out on this card"), and their dispatch goes toupscale_stub. The stub already offers such a network to the temporal upscaler (DLSS, FSR 4) throughtemporal_rejected_network, but FSR 3.1 has a path of its own (vk_fsr3.cpp). With FSR 3.1 chosen,temporal_wanted()is false, so the network was never offered to it and the stub stretched the scene. The only way FSR 3.1 got used on these cards was Render resolution Auto, while the game's dynamic resolution sat at 3/4 or full.Change
temporal_rejected_networkoffers the network to FSR 3.1 first (fsr3_upscale, as a network it doesn't know), then to the temporal upscaler. That's the same order asfsr3_stand_inbeforetemporal_stand_infor the networks that do translate.upscale_stubcalls it when FSR 3.1 is asked for too (fsr3_inputs::wanted()).fsr3_upscalealready takes a network it doesn't know only when the input is this frame's scene and the output is the picture (B10G11R11, larger than the input), so anything else, the movies for example, is stretched as before. With FSR 3.1's frame interpolation on and another upscaler drawing, the same call now also makes the interpolation's inputs from these frames, asfsr3_stand_indoes for the networks that translate.Before and after
RX 5500M (RDNA1, 4 GB), Windows 11, AMD driver, Upscaler FSR 3.1. Counts come from the log:
rejected compute <network>when a share's network is stretched, andfsr3: N frames drawnevery 600 frames FSR 3.1 draws.rejected compute 6676619641320ba7in a session at Auto)network 6676619641320ba7 does not translate here: FSR 3.1 draws its upscale (1024x576 -> 1536x864), then 16,200 frames over about ten minutes of play, jitter over 18 phasesrejected compute f10782f9e58b6a7cin a session at Auto)network f10782f9e58b6a7c does not translate here: FSR 3.1 draws its upscale (896x504 -> 1536x864)(a short check)rejected compute 72ba090c70fe2614 ... 768x432 ... 1536x864)fsr3: 16800 frames drawn, 768x432 -> 1536x864, this one's jitter +0.125 -0.278 px of 32 phasesThe #1829 reporter's log (RX 9060 XT, v2.0.14) has the same
72ba090c70fe2614 not translated: a network left out on this cardandrejected compute 72ba090c....A note on jitter, since the logs above show
jitter -0.000 +0.000 pxon FSR 3.1's first frame: FSR 3.1 publishes its jitter once it has drawn (fsr3_inputs::upscaled), and the game's projection is jittered from the next frame on. Zero on the first frame is expected. Theframes drawnlines show it moving afterwards.Not fixed here: Ultra Performance
At 1/3 the game uses
0775215f8da720e6(512x288 into 1536x864). Its bindings hold a second 1536x864 image besides the picture, which the other shares' networks don't have. FSR 3.1 doesn't take it in this test: nofsr3line at 512x288 and noFSR 3.1 draws its upscaleline for that network.fsr3_upscaledeclines a network it doesn't know without logging why (the input not this frame's scene, formats, sizes), so I'll find out which check that is and send it separately. The other three steps don't depend on it.Tests
tests/test_upscaler_rows.py:upscale_stuboffers a network that didn't translate when FSR 3.1 is asked for, andtemporal_rejected_networkoffers it to FSR 3.1 before the temporal upscaler. This fails on current main.tests/test_upscale_fallback.py: a stub forfsr3_inputs::wantedin its harness.tests/run_all.pyon main0a28ff29with this change merged: 310 passed. The 6 that fail are the same 6 that fail on plain main on this PC.Seen while testing, not from this PR
On this card, a bright dotted line along distant mountain silhouettes gets stronger toward sunset. It shows the same with Game's own at 100% (no upscaling at all) as with FSR 3.1 at Quality, and Sharpness and the Window scaling filter don't change it, so it isn't from FSR 3.1 or this change. The build tested here is main
92907f7, from before #1657, which changes how quad operations in fragment shaders are handled on RDNA1. That may be it, but I haven't checked yet.Related: #1829, #1006 (AMD: no anti-aliasing, networks in software).