fix(vk): FSR 3.1 draws the fixed Render resolution steps where the game's networks are left out #2166

Merged
rakefire-impala merged 1 commit from fix/fsr3-fixed-steps into main 2026-11-11 17:06:38 +00:00
rakefire-impala commented 2026-11-11 05:33:12 +00:00 (Migrated from github.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:

share network its input
full 9dc98871eb7fc101 1536x864
3/4 fd029a317a9a4b94 1152x648
2/3 (Quality) 6676619641320ba7 1024x576
7/12 (Balanced) f10782f9e58b6a7c 896x504
1/2 (Performance) 72ba090c70fe2614 768x432
1/3 (Ultra Performance) 0775215f8da720e6 512x288

On 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 to upscale_stub. The stub already offers such a network to the temporal upscaler (DLSS, FSR 4) through temporal_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_network offers 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 as fsr3_stand_in before temporal_stand_in for the networks that do translate.
  • upscale_stub calls it when FSR 3.1 is asked for too (fsr3_inputs::wanted()).

fsr3_upscale already 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, as fsr3_stand_in does 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, and fsr3: N frames drawn every 600 frames FSR 3.1 draws.

Render resolution main this PR
Quality (2/3) stretched (by the code; rejected compute 6676619641320ba7 in a session at Auto) FSR 3.1 draws it: 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 phases
Balanced (7/12) stretched (by the code; rejected compute f10782f9e58b6a7c in a session at Auto) FSR 3.1 draws it: network f10782f9e58b6a7c does not translate here: FSR 3.1 draws its upscale (896x504 -> 1536x864) (a short check)
Performance (1/2) stretched: about 18,700 game frames, 0 drawn by FSR 3.1 (rejected compute 72ba090c70fe2614 ... 768x432 ... 1536x864) FSR 3.1 draws it: fsr3: 16800 frames drawn, 768x432 -> 1536x864, this one's jitter +0.125 -0.278 px of 32 phases
Ultra Performance (1/3) stretched: at most 600 of about 7,350 game frames drawn by FSR 3.1 still stretched: no FSR 3.1 frames at 512x288 over a minute (see below)

The #1829 reporter's log (RX 9060 XT, v2.0.14) has the same 72ba090c70fe2614 not translated: a network left out on this card and rejected compute 72ba090c....

A note on jitter, since the logs above show jitter -0.000 +0.000 px on 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. The frames drawn lines 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: no fsr3 line at 512x288 and no FSR 3.1 draws its upscale line for that network. fsr3_upscale declines 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_stub offers a network that didn't translate when FSR 3.1 is asked for, and temporal_rejected_network offers it to FSR 3.1 before the temporal upscaler. This fails on current main.
  • tests/test_upscale_fallback.py: a stub for fsr3_inputs::wanted in its harness.
  • tests/run_all.py on main 0a28ff29 with 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).

## 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: | share | network | its input | |---|---|---| | full | `9dc98871eb7fc101` | 1536x864 | | 3/4 | `fd029a317a9a4b94` | 1152x648 | | 2/3 (Quality) | `6676619641320ba7` | 1024x576 | | 7/12 (Balanced) | `f10782f9e58b6a7c` | 896x504 | | 1/2 (Performance) | `72ba090c70fe2614` | 768x432 | | 1/3 (Ultra Performance) | `0775215f8da720e6` | 512x288 | On 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 to `upscale_stub`. The stub already offers such a network to the temporal upscaler (DLSS, FSR 4) through `temporal_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_network` offers 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 as `fsr3_stand_in` before `temporal_stand_in` for the networks that do translate. - `upscale_stub` calls it when FSR 3.1 is asked for too (`fsr3_inputs::wanted()`). `fsr3_upscale` already 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, as `fsr3_stand_in` does 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, and `fsr3: N frames drawn` every 600 frames FSR 3.1 draws. | Render resolution | main | this PR | |---|---|---| | Quality (2/3) | stretched (by the code; `rejected compute 6676619641320ba7` in a session at Auto) | FSR 3.1 draws it: `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 phases | | Balanced (7/12) | stretched (by the code; `rejected compute f10782f9e58b6a7c` in a session at Auto) | FSR 3.1 draws it: `network f10782f9e58b6a7c does not translate here: FSR 3.1 draws its upscale (896x504 -> 1536x864)` (a short check) | | Performance (1/2) | stretched: about 18,700 game frames, 0 drawn by FSR 3.1 (`rejected compute 72ba090c70fe2614 ... 768x432 ... 1536x864`) | FSR 3.1 draws it: `fsr3: 16800 frames drawn, 768x432 -> 1536x864, this one's jitter +0.125 -0.278 px of 32 phases` | | Ultra Performance (1/3) | stretched: at most 600 of about 7,350 game frames drawn by FSR 3.1 | **still stretched**: no FSR 3.1 frames at 512x288 over a minute (see below) | The #1829 reporter's log (RX 9060 XT, v2.0.14) has the same `72ba090c70fe2614 not translated: a network left out on this card` and `rejected compute 72ba090c...`. A note on jitter, since the logs above show `jitter -0.000 +0.000 px` on 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. The `frames drawn` lines 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: no `fsr3` line at 512x288 and no `FSR 3.1 draws its upscale` line for that network. `fsr3_upscale` declines 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_stub` offers a network that didn't translate when FSR 3.1 is asked for, and `temporal_rejected_network` offers it to FSR 3.1 before the temporal upscaler. This fails on current main. - `tests/test_upscale_fallback.py`: a stub for `fsr3_inputs::wanted` in its harness. - `tests/run_all.py` on main `0a28ff29` with 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).
hckrman101 (Migrated from github.com) approved these changes 2026-11-11 16:50:43 +00:00
hckrman101 (Migrated from github.com) left a 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.

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.
@ -6919,1 +6918,3 @@
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;
// anything else goes on below
if ((temporal_wanted() || temporal_off() || fsr3_inputs::wanted()) && temporal_rejected_network(cmd, key, src, &in, dst, &out))
hckrman101 (Migrated from github.com) commented 2026-11-11 16:50:42 +00:00

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".

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".
@ -7213,7 +7216,11 @@ static bool temporal_rejected_network(VkCommandBuffer cmd, uint64_t key, Texture
if (!in_view || !out_view || out_format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 ||
(in_format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 && in_format != VK_FORMAT_E5B9G9R9_UFLOAT_PACK32))
return false;
hckrman101 (Migrated from github.com) commented 2026-11-11 16:50:42 +00:00

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.

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.
Commenting is not possible because the repository is archived.
No description provided.