Repository navigation
fix(vk): wait for the native scene when switching to DLAA - #2145
Conversation
hckrman101
left a comment
There was a problem hiding this comment.
Thanks, the diagnosis is clear and the fix is small. On the question of stalls: there's no actual wait here, just a per-frame choice of which mode to draw with, made under the s.mu lock temporal_upscale already takes, with no new lock and nothing that blocks on the render thread. So I don't see a deadlock or a stalled frame. The feature-reuse change in vk_dlss.cpp (native_aa / feature_aa) also looks right: the one recreation happens when the full-size scene arrives, and an already-DLAA feature is reused.
One thing I'd like closed before this goes in (inline): the wait can latch for good. It only ends when rw == ow && rh == oh, and drawn_mode stays DLSS while it waits, so if the full-scale scene is ever smaller than the gameplay output, a DLSS -> DLAA switch keeps drawing DLSS indefinitely and nothing says so in the log. The native path clearly supports that case ("its picture then scaled to the output's by dlss_out.comp", and your fixture's startup case 5 draws DLAA at 1720x720 into 3440x1440). Bounding the wait by a few frames from the switch, plus a log line when it gives up, would make it safe whatever the sizes turn out to be.
Overlaps: #2193 rewrites the mode selection right below the why chain you add a line to, and #2118 touches temporal_upscale next to the prepare call and the s.last_frame = now line. Expect small textual conflicts with whichever merges first. I don't see a semantic clash: with #2193 mode is still DLAA when aa, so feature_aa keeps working.
CI is green on 99ad842, and the new test is picked up by run_all.py through RUN_ALL_ARGS.
| // Keep upscaling the queued small scene until the guest renders at the requested native size. | ||
| // Creating DLAA for that queued scene would replace the feature twice in successive frames. | ||
| const bool entering_dlaa = mode == ur::DLAA && s.drawing == engine.e && s.drawn_mode == ur::DLSS; | ||
| const bool waiting_for_native = entering_dlaa && (rw != ow || rh != oh); |
There was a problem hiding this comment.
This only ends when the scene is exactly the output's size. Because s.drawn_mode = draw_mode stores DLSS while waiting, entering_dlaa stays true on the next frame, so the wait has no other way out. If the full-scale scene is smaller than the output (a case the native path handles, see fixture case 5), DLAA never engages after a switch from DLSS, while starting directly in DLAA would use it. Could you cap it, e.g. record the frame the switch was first seen and stop waiting after 2 or 3 frames, with a logf when it gives up? A fixture step for 'DLSS -> DLAA where the scene stays below the output' would cover it.
There was a problem hiding this comment.
Fixed in 278bea7: the transition now allows three texture frames, then logs the fallback and uses DLAA at the available scene size. Same-frame calls do not consume the budget, and cancelling/re-entering starts a fresh budget. The permanently undersized-scene regression fails without the bound and passes with it on Windows/Linux.
Rebased on 51da046, preserving both feature_hdr == hdr and native-AA reuse checks, with HDR on/off coverage in the fixture. FSR 3.1/4 internal callbacks are adapted too. The NVIDIA-enabled backend links and the focused suite passes; exact-head CI is running.
…and a refused size keeps the feature (#2193) At output sizes that don't divide evenly by 3, Ultra Performance never runs DLSS. The game's third of the output rounds a pixel past DLSS's Ultra Performance size, no mode takes it, and the game's own upscale network runs in DLSS's place; only a log line shows it. And when a size is refused, `prepare` has already let its feature go, so DLSS rebuilds it a frame later. I found these while looking into reports in #dev-work of lower DLSS presets running worse than DLAA. Those turned out to be mostly something else: DLSS model M on an RTX 3060, and VRAM filling up over a session. So this PR doesn't claim to fix them; it fixes the two bugs above. **What happened** (RTX 5090, 4416x1864 output: upstream's 3840x2160-pixel widescreen picture on a 21:9 screen) - Ultra Performance's range at that output is 1472x621 to 1472x621, and the game's third of 1864 came out 622. The log said `NVIDIA DLSS: the game's upscale runs (no DLSS mode takes this render size)`. - `prepare` let the feature go *before* looking for a mode. So after a refused size the feature was made again on the next frame DLSS could take (`feature N made (again after it was let go ...)`, 10–25 ms of CPU each). **What changed** - `dlss_checks.h` `mode_for` picks the mode as a plain, testable function: - It takes the first mode whose range holds the render size. - If none does, it takes the first mode the size misses by a little: at most 1/64 of the range's largest size per axis, and 2 pixels at least. That range is widened to the size, and the widening is logged when the feature is made. - A size is widened only if DLSS can scale it at all (`scene_size_fits`: the output 1–3× the size). A size between two modes' ranges is still refused, for example 42% at 5120x2160, between Ultra Performance's third and the other modes' half. - `vk_dlss.cpp` `prepare` finds the mode first and lets the old feature go only once a mode takes the size. A refused size keeps the feature, and the log names the size (`no DLSS mode takes the render size WxH for OWxOH`). **Tested** - In game, RTX 5090, driver 621.14, Windows 11, 4416x1864 output, preset M, frame generation off: | Render resolution | before | after | |---|---|---| | Native (DLAA) | DLSS, ~60–64 fps | same | | Performance 2208x932 | DLSS, ~66–76 fps | same | | Ultra Performance 1472x622 | **the game's upscale**, ~85–95 fps | **DLSS** (`a little outside quality mode 3's range (1472x621 to 1472x621): taken by it`), ~76–78 fps | On this GPU the game's network is faster than DLSS at a third, so Ultra Performance goes down here; either way the player now gets DLSS, which is what they picked. - `tests/test_dlss_checks.py` / `.cpp` cover Performance's half, Ultra Performance's exact and rounded third, a size between the modes, one below a third, a 2560x1440 output's Ultra Performance at the menu's own size, and no widening into a range NGX didn't give. The source check confirms `prepare` uses `mode_for` and lets the old feature go after it. **Both fail on main.** - `tests/run_all.py` (Windows, llvm-mingw): the DLSS, upscaler and temporal checks pass. The only failure is `test_recorder_sync`'s exit case, which fails on main too. **Not tested** - Other output sizes in game (the test covers 2560x1440's rounding). - Linux. **Relation to #2145.** That PR (Arkex) makes the switch to Native wait for the native scene and touches the same `prepare`. This PR leaves the Native transition to it and changes only how the mode is found and when the old feature is let go, so whichever lands second needs a small rebase there. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: pmwork-personal <315945841+pmwork-personal@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Joey-Magnus
left a comment
There was a problem hiding this comment.
Needs a rebase: there are three conflicts in vk_dlss.cpp with #2342 (s.feature_hdr against s.feature_aa). Keep both: s.feature_hdr == hdr && (!native_aa || s.feature_aa). tests/test_dlss_native_transition.py cuts that condition out by text, so it will need stubs for hdr/s.feature_hdr.
The earlier blocking point is unfixed: waiting_for_native (vk_temporal.cpp:883) only ends when the render size equals the output size, and drawn_mode stays DLSS while it waits (:1000). If the full-scale scene is ever smaller than the output, DLAA never takes effect and nothing is logged. Please cap the wait at a few frames and log when it gives up.
…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>
Assisted-by: Codex
Assisted-by: Codex
Assisted-by: Codex
99ad842 to
278bea7
Compare
Summary
Switching from scaled DLSS to Native (DLAA) can create an AA feature for a queued small scene, then recreate it when the native scene arrives. Keep DLSS for up to three scene frames during the transition; if the scene remains smaller than the output, log the fallback and use DLAA at that scene size. Fixes #2144.
Technical changes
Validation
Current head 278bea7, rebased on 51da046:
git diff --checkpasses. Gameplay was not rerun on this head.Earlier-head local evidence and limits
At 8c5ea10, the bounded-wait regression passes on Windows and Linux and fails on the rebased unbounded parent. Its temporal/DLSS/fixture input hashes are unchanged on the final head; the final follow-up adapts the FSR 3.1 callback signature.
Full suites at 8c5ea10: Windows 451 passed, 7 failed, 38 skipped; Linux 442 passed, 6 failed, 48 skipped. Subsequent controls reran the failed checks on candidate and clean 51da046 with the same configuration: Windows 4 passed, 3 failed on each, with matching failure signatures; Linux 6 passed, 0 failed on each. These partial reruns are not a replacement full-suite result.
Earlier gameplay at 2ec8aa66 verified Performance-to-Native and return from inventory with one native DLAA feature creation. This predates the bounded-wait and HDR rebase changes. No universal stall reduction, other-GPU, Linux GPU, campaign or input-latency claim.