Skip to content

fix(vk): wait for the native scene when switching to DLAA - #2145

Merged
hckrman101 merged 3 commits into
NotRexed:mainfrom
Arkex-ai:dlss/native-transition-review-main
Nov 12, 2026
Merged

hckrman101 merged 3 commits into
NotRexed:mainfrom
Arkex-ai:dlss/native-transition-review-main

Conversation

@Arkex-ai

@Arkex-ai Arkex-ai commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

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

  • Bound the native-scene transition by texture-frame number. Repeated calls within one frame do not consume the budget; returning to DLSS resets it.
  • Preserve both HDR and native-AA compatibility when reusing the DLSS feature. Pass AA intent through the internal temporal callback, adapting the FSR 3.1 and FSR 4 callbacks without changing their behavior or the public renderer ABI.
  • Extend the extracted-source regression with a permanently undersized scene, same-frame calls, cancellation/re-entry and HDR on/off feature recreation.

Validation

Current head 278bea7, rebased on 51da046:

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.

@hckrman101 hckrman101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread gpu/vk/vk_temporal.cpp Outdated
// 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);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
…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 Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

hckrman101 pushed a commit that referenced this pull request Nov 11, 2026
…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:

![motion
vectors](https://raw.githubusercontent.com/unixman69/Hookshot/pr-screenshots/fsr3-chain/motion-vectors.png)

Link walking: own inputs vs the chain, at 100% and at 67% (2x zoom
crops):


![walking](https://raw.githubusercontent.com/unixman69/Hookshot/pr-screenshots/fsr3-chain/link-walking-own-vs-chain.png)

**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>
@Arkex-ai
Arkex-ai force-pushed the dlss/native-transition-review-main branch from 99ad842 to 278bea7 Compare November 12, 2026 14:29
@hckrman101
hckrman101 merged commit f3dd448 into NotRexed:main Nov 12, 2026
16 checks passed
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.

DLAA is created twice when Native is selected while a scaled scene is queued

3 participants