Repository navigation
feat(vk): reactive mask for DLSS and FSR 3.1 from the scene's transpa… - #1653
Conversation
…rent draws (RX_VK_REACTIVE=1) The temporal upscalers follow a pixel by its depth and motion. What the scene blends over itself without writing depth (water, smoke, fire, glows) has neither, so it is taken for the surface behind and smears or ghosts. With RX_VK_REACTIVE=1 the scene's colour is copied before its transparent draws (blended, depth tested, depth not written: reactive_policy.h), and at the upscale the scene's target is compared with that copy (shaders/reactive_mask.comp). The difference goes to FSR 3.1's reactivity pass as its reactive mask and to DLSS as its bias-current-colour mask. Off by default: which draws are transparent is told from their state alone and has not been checked in the game. RX_VK_REACTIVE_DEBUG=1 shows the mask. Co-Authored-By: Claude <noreply@anthropic.com>
hckrman101
left a comment
There was a problem hiding this comment.
Thanks, the idea is sound and the policy/copy/compare split is easy to follow. The reactive_policy.h unit test is a good touch. I read the change through; I haven't run it, and it's off by default so the risk is contained. One real blocker:
This is based on a pre-#1008 main and no longer merges (mergeable_state: dirty). The upscaler code the hooks attach to has moved:
dlss_jitter_size()no longer exists on main (the DLSS side now lives in the TemporalEngine:temporal_jitter_sizein vk_temporal.h, andtemporal_scene_size()/temporal_note_scene_draw()in vk_draw.cpp).reactive_scene_draw()callsdlss_jitter_size(0/1), so it won't compile after a rebase as is. It probably wantstemporal_scene_size(at.width, at.height)(ortemporal_jitter_size) for the DLSS/FSR 4 side, keepingfsr3_inputs::jitter_sizefor FSR 3.1.- DLSS evaluation is now
evaluate(cmd, const TemporalFrame& f)in vk_dlss.cpp (ep.InReset = f.reset...); thedlss_upscale(... s.color.view, s.feature_rw ...)hunk where you setpInBiasCurrentColorMaskhas nothing to apply to. The mask call needs to go into that function using theTemporalFrame's colour view and size. - vk_fsr3.cpp and the
in.reactiveswap into P_PREPARE_REACTIVITY look like they still apply (main still passestex(g.default_reactive)there).
Also worth deciding while you rebase: with FSR 4 now going through the same TemporalEngine, does it have a reactive input you'd want to feed too, or should the doc say explicitly DLSS + FSR 3.1 only? (#1654 is open in the same area, so a quick look at how it lands is worthwhile.)
Things I checked that look fine: copy/mask recorded outside a pass (end_pass before the copy), full_barrier around the copy and the dispatch, descriptors_disturbed() after the push-descriptor pass, destroy-generation check before touching the scene texture, deferred freeing of replaced images, constant-initialised thread_locals (no llvm-mingw TLS-init cost), tests picked up by the run_all glob, R8 storage-image check in wanted().
Nit: the mask is the whole difference between the copy and the final scene target, so a blended no-depth-test draw that comes after the last transparent run (a full-screen fade, say) will also light the mask. You say it's unverified in-game, so fine for an opt-in switch, just worth a line in the doc once you've looked at a few scenes with RX_VK_REACTIVE_DEBUG=1.
CI: no checks reported on the branch, so a rebase will also give it a run.
On the frame chain (NotRexed#1008): the scene is found by temporal_jitter_size, and DLSS's engine makes the mask in its evaluate from the TemporalFrame's colour and sizes. DLSS and FSR 3.1 only: AMD FSR 4 is given no mask, and nothing is copied while it draws. The doc says so, and that the mask is the whole difference since the copy. tests/test_reactive_mask.py is given run_all.py's compiler. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Heads-up: this conflicts with main now (a lot landed today). Could you merge main into the branch (or rebase) and resolve the conflicts? Thanks! |
tests/run_all.py's ARGS table is retired on main: tests/test_reactive_mask.py now declares its compiler itself (RUN_ALL_ARGS). Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Done: main is merged in again and the conflicts are resolved. Each point from your review is answered below.
Fixed.
Done. The mask is made in
They did; those hunks are unchanged.
DLSS + FSR 3.1 only for now, and the doc says so. FSR 4 is given no mask, and no scene copy is made while it draws. I'd add its reactive input as a follow-up once this has been looked at in-game.
Added a line to the doc saying so. Also
Checked
|
hckrman101
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround. All the points from my last review are covered:
dlss_jitter_sizeis gone:reactive_scene_draw()usestemporal_jitter_sizefor the frame chain and keepsfsr3_inputs::jitter_sizefor FSR 3.1. Fixed.- The DLSS mask is now made in
evaluate(cmd, const TemporalFrame&)from the frame's colour view and sizes. Fixed. - FSR 4 gets no mask and no scene copy while it draws (
is_fsr4(temporal_mode()), and that enum matchesupscaler_rows). The doc says DLSS + FSR 3.1 only. Answered; a follow-up for FSR 4 sounds right. - The doc now mentions the full-screen-fade case. Done.
- The test declares
RUN_ALL_ARGSitself, which matches main since #1822.
Still blocking: it conflicts with main again (mergeable_state: dirty). A lot landed after your 18:15 merge, and the overlaps with this PR are gpu/vk/vk_dlss.cpp, gpu/vk/vk_draw.cpp, gpu/vk/vk_fsr3.cpp and docs/development.md. One of them is more than a textual conflict:
- The recorder (#1473,
RX_VK_RECORDER) put::mkwvk::rec::boundary(::mkwvk::rec::B_DIRECT);inevaluate()right beforeNGX_VULKAN_EVALUATE_DLSS_EXT, at exactly the line where your mask block goes. When you resolve it, thereactive::mask(...)call has to come before that boundary.mask()records its barrier, bind, push and dispatch throughvk_work.h, so withRX_VK_RECORDER=defer/threadthose packets are only flushed intocmdby the boundary. If the mask ends up after it, NGX would read the mask before the dispatch that writes it reaches the command buffer. See the inline comment. vk_reactive.cppalready includesvk_work.hlast, so its own commands go through the recorder correctly. Nothing to change there.docs/development.md: main added theRX_VK_COMPACTrow betweenRX_VK_VRAM_MBandRX_VK_NO_WRITEBACK, right where your row goes. Keep both.vk_draw.cpp/vk_fsr3.cpp: everything your hunks use (barrier_every,clear_after_draws,default_reactivein P_PREPARE_REACTIVITY,ai.reset = f.reset) is still on main, so those should resolve mechanically.
CI: still no checks reported on the branch. A clean merge should give it a run.
Once it's merged and the mask sits before the B_DIRECT boundary, I'm happy with this as an opt-in switch.
| // DLSS's mask of where to lean on this frame's colour, as large as the chain's inputs. Without one the parameter stays | ||
| // null, as before) | ||
| NVSDK_NGX_Resource_VK bias{}; | ||
| if (reactive::Mask m; reactive::mask(cmd, f.render_w, f.render_h, f.color.width, f.color.height, f.color.view, m)) { |
There was a problem hiding this comment.
Heads-up for the merge: main now has ::mkwvk::rec::boundary(::mkwvk::rec::B_DIRECT); just before NGX_VULKAN_EVALUATE_DLSS_EXT here (the recorder, #1473). This block needs to stay above that line. reactive::mask records through vk_work.h, and under RX_VK_RECORDER=defer/thread it only reaches cmd at the boundary, so NGX has to come after it.
There was a problem hiding this comment.
Thanks. Main is merged in again and the conflicts are resolved.
- The mask block in
evaluate()now sits directly aboverec::boundary(B_DIRECT), so its dispatch reachescmdbefore NGX reads the mask. docs/development.mdkeeps both theRX_VK_COMPACTrow and this one.vk_draw.cppandvk_fsr3.cppmerged without a conflict, as you expected.
tests/run_all.py fails only where main does on my machine. I couldn't compile vk_dlss.cpp this time (no NGX headers here); the resolution there only keeps the existing block above main's new line. Still not run in-game.
There was a problem hiding this comment.
Confirmed: the mask block is right above the B_DIRECT boundary on the new head. Thanks!
Conflicts: docs/development.md (main's RX_VK_COMPACT row and this branch's RX_VK_REACTIVE row both kept) and gpu/vk/vk_dlss.cpp, where the mask is made before the recorder's B_DIRECT boundary (NotRexed#1473): reactive::mask records through vk_work.h, so under RX_VK_RECORDER=defer/thread its dispatch only reaches cmd at that boundary, and NGX has to read the mask after it. Co-Authored-By: Claude <noreply@anthropic.com>
hckrman101
left a comment
There was a problem hiding this comment.
Thanks, the merge looks right. I compared the PR's patches before and after it file by file: apart from the merge context nothing in your hunks changed.
- The mask block in
evaluate()now sits directly above::mkwvk::rec::boundary(::mkwvk::rec::B_DIRECT), so underRX_VK_RECORDER=defer/threadthe mask's dispatch reachescmdbefore NGX reads it. Fixed. docs/development.mdkeeps both theRX_VK_COMPACTrow and yours. Fixed.vk_draw.cpp/vk_fsr3.cpp: unchanged hunks, andfsr3_upscaleon main still buildsaifrom the samefsr3_inputs::Frame(f.color,f.width/height) after #2147. With the new Upscaler Off choice (#2097) both jitter sizes are 0, soreactive_scene_draw()copies nothing there. Fine.- It merges cleanly now.
Heads-up for later, not for this PR: #2305 moves FSR 3.1 onto the temporal chain (chain_evaluate in vk_fsr3.cpp). If it lands, reactive_scene_draw() will see temporal_jitter_size != 0 with a non-FSR 4 mode and make the copy, but the chain's FSR 3.1 path doesn't call reactive::mask, so the copy goes unused. Whichever of the two merges second should add the reactive::mask(...) → ai.reactive line to chain_evaluate. #1654 touches the same files but you already checked that the two merge cleanly together.
CI: the build and test workflows on this head are waiting for a maintainer to approve them (action_required), so nothing has actually built yet, vk_dlss.cpp included. Only the PR title check ran. It's off by default and the change since my last review is a placement-only move, so I'm approving. Someone with access should still approve the runs and look at them before merging.
… 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>
…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>
…rent draws (RX_VK_REACTIVE=1)
The temporal upscalers follow a pixel by its depth and motion. What the scene blends over itself without writing depth (water, smoke, fire, glows) has neither, so it is taken for the surface behind and smears or ghosts.
With RX_VK_REACTIVE=1 the scene's colour is copied before its transparent draws (blended, depth tested, depth not written: reactive_policy.h), and at the upscale the scene's target is compared with that copy (shaders/reactive_mask.comp). The difference goes to FSR 3.1's reactivity pass as its reactive mask and to DLSS as its bias-current-colour mask.
Off by default: which draws are transparent is told from their state alone and has not been checked in the game. RX_VK_REACTIVE_DEBUG=1 shows the mask.