feat(vk): FSR 3.1 on the chain keeps the game's native HDR highlights #2614

Open
unixman69 wants to merge 4 commits from unixman69/feat/fsr3-chain-hdr into main
unixman69 commented 2026-11-12 02:20:26 +00:00 (Migrated from github.com)

With HDR on Native, the temporal chain gives DLSS (#2243) and FSR 4 (#2342) the game's colour as linear light, past 1.
FSR 3.1 on the chain (#2305) still got it clamped at 1, so with FSR 3.1 every highlight stopped at the paper white. This
gives FSR 3.1 the same HDR colour. Its passes are already built for HDR colour (FFX_FSR3UPSCALER_OPTION_HDR_COLOR_INPUT)
and work out their own exposure, so no pass or shader changes: only the chain engine says it takes HDR.

What changed

  • vk_fsr3.cpp, the chain engine:
    • chain_prepare decides HDR each frame from present_native_pq, as DLSS and FSR 4 do: what the game presents, not
      the option. RX_VK_FSR3_HDR=0 keeps the clamped colour, for comparisons.
    • chain_hdr tells the chain (TemporalEngine::hdr), so the chain's existing HDR prep/out pair runs for it: linear
      light in (kSquare, ceiling 16384), its picture taken back linear.
    • When that changes, the next evaluation starts a new history (reset), since the history kept was in the other
      encoding. DLSS and FSR 4 get the same by making their feature or context again.
    • The log says vk: fsr3 on the chain: HDR colour, linear (the game's native HDR) when it turns on.
  • Docs: the readme's FSR 3.1 section, docs/development.md (RX_VK_FSR3_HDR=0), and a row in docs/tested-configurations.md.
  • Tests: test_hdr_encode.py gets check_fsr3, beside check_dlss and check_fsr4: HDR decided from
    present_native_pq (not opts.hdr), the switch, the history reset, the engine's .hdr, and the passes built for HDR
    colour. Removing the reset or the engine's .hdr each fails it.

Not changed:

  • SDR, and HDR with the SDR picture expanded: chain_hdr is false there, so the chain runs the same LDR passes as on main.
  • FSR 3.1 with its own inputs (RX_VK_FSR3_CHAIN=0) stays LDR.

Testing

  • python -I -B tests/run_all.py: 449 passed, 2 failed, 21 skipped (Linux). The two failures fail on main too:
    test_env_on (HOS_FPS_LOG in host/oot_high_fps.c) and test_linux_saves_move.
  • In game: RX 9070 XT 16 GB, Mesa RADV 26.2.6, Gentoo Linux (kernel 7.3.3), KDE Plasma Wayland, windowed, 2560x1440 HDR10
    (HDR Native, paper white 110 nits = PQ code 132, peak 400). Host v2.1.0-main-local; renderer main ef81aee7 plus this
    branch. "Off" is RX_VK_FSR3_HDR=0, which is what main does.
    • Every HDR run logged vk: fsr3 on the chain: HDR colour, linear (the game's native HDR) once, then drew FSR 3.1 on
      every frame (600 of 600 per log line), with no device lost and no validation messages. The only fallback to the
      network was one frame at the teleport (no scene camera this frame), in every FSR 3.1 run, with or without this.

    • Castle Town market at noon, the still frames' PQ codes (0-255 of the HDR10 window's 8-bit shot):

      Upscaler Brightest pixel 99.9th percentile Pixels above the paper white, per million
      FSR 3.1 100%, this change (2 runs) 163, 162 139 16745, 16510
      FSR 3.1 100%, off (2 runs) 147, 147 138 14465, 15115
      FSR 3.1 Quality (67%), this change 160 138 14344
      FSR 3.1 Quality (67%), off 147 138 13857
      The game's own upscaler, 100% (reference) 182 139 17578
      FSR 4 Native AA, HDR (#2342) 156 138 13276

      Off, every run stops at the same code, 147. With the change, the sunlit highlights go past it, towards the game's
      own. The rest of the picture is the same: mean code 63.5 in both, and no colour shift side by side.

    • Kokiri Forest showed no difference: nearly all the pixels there above the paper white are Navi's, as bright with the
      change as without it.

    • The Quality and off rows were measured on an earlier revision of this branch that also made FSR 3.1's luma history
      RGBA16F (see below); with the history as it is here, 100% gave the same highlights (163, 162 against 163, 162).

Left out, a possible follow-up: FSR 3.1's luma history (rw_luma_history, the luma instability pass) is RGBA8,
because AMD's GLSL callbacks declare it rgba8, so it holds luma up to 1 only. I tried an RGBA16F history (a patched copy
of the callbacks header for scripts/fsr3_shaders.py). It made no measurable difference to highlights or flicker in HDR,
nor to flicker in SDR (5 alternated pairs, within run-to-run noise), so it isn't in this PR. If someone sees highlights
flicker with FSR 3.1 in HDR, that's where to look.

Not tested:

  • other GPUs and drivers;
  • Windows;
  • Steam Deck;
  • switching HDR on and off while FSR 3.1 is drawing (the history reset is code-reviewed, not exercised in game).

🤖 Generated with Claude Code

With HDR on **Native**, the temporal chain gives DLSS (#2243) and FSR 4 (#2342) the game's colour as linear light, past 1. FSR 3.1 on the chain (#2305) still got it clamped at 1, so with FSR 3.1 every highlight stopped at the paper white. This gives FSR 3.1 the same HDR colour. Its passes are already built for HDR colour (`FFX_FSR3UPSCALER_OPTION_HDR_COLOR_INPUT`) and work out their own exposure, so no pass or shader changes: only the chain engine says it takes HDR. **What changed** - `vk_fsr3.cpp`, the chain engine: - `chain_prepare` decides HDR each frame from `present_native_pq`, as DLSS and FSR 4 do: what the game presents, not the option. `RX_VK_FSR3_HDR=0` keeps the clamped colour, for comparisons. - `chain_hdr` tells the chain (`TemporalEngine::hdr`), so the chain's existing HDR prep/out pair runs for it: linear light in (`kSquare`, ceiling 16384), its picture taken back linear. - When that changes, the next evaluation starts a new history (`reset`), since the history kept was in the other encoding. DLSS and FSR 4 get the same by making their feature or context again. - The log says `vk: fsr3 on the chain: HDR colour, linear (the game's native HDR)` when it turns on. - Docs: the readme's FSR 3.1 section, `docs/development.md` (`RX_VK_FSR3_HDR=0`), and a row in `docs/tested-configurations.md`. - Tests: `test_hdr_encode.py` gets `check_fsr3`, beside `check_dlss` and `check_fsr4`: HDR decided from `present_native_pq` (not `opts.hdr`), the switch, the history reset, the engine's `.hdr`, and the passes built for HDR colour. Removing the reset or the engine's `.hdr` each fails it. **Not changed:** - SDR, and HDR with the SDR picture expanded: `chain_hdr` is false there, so the chain runs the same LDR passes as on main. - FSR 3.1 with its own inputs (`RX_VK_FSR3_CHAIN=0`) stays LDR. **Testing** - `python -I -B tests/run_all.py`: 449 passed, 2 failed, 21 skipped (Linux). The two failures fail on main too: `test_env_on` (`HOS_FPS_LOG` in `host/oot_high_fps.c`) and `test_linux_saves_move`. - In game: RX 9070 XT 16 GB, Mesa RADV 26.2.6, Gentoo Linux (kernel 7.3.3), KDE Plasma Wayland, windowed, 2560x1440 HDR10 (HDR Native, paper white 110 nits = PQ code 132, peak 400). Host v2.1.0-main-local; renderer main ef81aee7 plus this branch. "Off" is `RX_VK_FSR3_HDR=0`, which is what main does. - Every HDR run logged `vk: fsr3 on the chain: HDR colour, linear (the game's native HDR)` once, then drew FSR 3.1 on every frame (600 of 600 per log line), with no device lost and no validation messages. The only fallback to the network was one frame at the teleport (`no scene camera this frame`), in every FSR 3.1 run, with or without this. - Castle Town market at noon, the still frames' PQ codes (0-255 of the HDR10 window's 8-bit shot): | Upscaler | Brightest pixel | 99.9th percentile | Pixels above the paper white, per million | |---|---|---|---| | FSR 3.1 100%, this change (2 runs) | 163, 162 | 139 | 16745, 16510 | | FSR 3.1 100%, off (2 runs) | 147, 147 | 138 | 14465, 15115 | | FSR 3.1 Quality (67%), this change | 160 | 138 | 14344 | | FSR 3.1 Quality (67%), off | 147 | 138 | 13857 | | The game's own upscaler, 100% (reference) | 182 | 139 | 17578 | | FSR 4 Native AA, HDR (#2342) | 156 | 138 | 13276 | Off, every run stops at the same code, 147. With the change, the sunlit highlights go past it, towards the game's own. The rest of the picture is the same: mean code 63.5 in both, and no colour shift side by side. - Kokiri Forest showed no difference: nearly all the pixels there above the paper white are Navi's, as bright with the change as without it. - The Quality and off rows were measured on an earlier revision of this branch that also made FSR 3.1's luma history RGBA16F (see below); with the history as it is here, 100% gave the same highlights (163, 162 against 163, 162). **Left out, a possible follow-up:** FSR 3.1's luma history (`rw_luma_history`, the luma instability pass) is RGBA8, because AMD's GLSL callbacks declare it `rgba8`, so it holds luma up to 1 only. I tried an RGBA16F history (a patched copy of the callbacks header for `scripts/fsr3_shaders.py`). It made no measurable difference to highlights or flicker in HDR, nor to flicker in SDR (5 alternated pairs, within run-to-run noise), so it isn't in this PR. If someone sees highlights flicker with FSR 3.1 in HDR, that's where to look. **Not tested:** - other GPUs and drivers; - Windows; - Steam Deck; - switching HDR on and off while FSR 3.1 is drawing (the history reset is code-reviewed, not exercised in game). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Owner

@Joey-Magnus reviewed on GitHub · 2026-11-12 14:33 UTC · original

I merged this into current main by hand. The two conflicts are trivial. In docs/tested-configurations.md, rows were added at the top. In tests/test_hdr_encode.py, main already loosened the DLSS engine check to the "hdr" substring, so main's version can be kept and the regex dropped. With that, test_hdr_encode, test_fsr3_math, test_fsr3_shader_options, test_temporal_history and test_temporal_jitter all pass on Linux.
The change itself looks right. chain_prepare decides HDR from present_native_pq the same way DLSS and FSR 4 do, chain_hdr hands the chain its HDR prep/out pair, and a change of encoding resets the history (ai.reset = f.reset || c.hdr_changed). SDR and expanded-SDR HDR keep the LDR passes because chain_hdr is false there. The passes were already built with HDR_COLOR_INPUT=1, so no shader changes are needed. The PQ-code numbers from the Castle Town market are the measurement this needs.
Please rebase. Once it's rebased, I'd merge it.

> [@Joey-Magnus](https://github.com/Joey-Magnus) reviewed on GitHub · 2026-11-12 14:33 UTC · [original](https://github.com/NotRexed/Hookshot/pull/2614#pullrequestreview-5876221659) I merged this into current main by hand. The two conflicts are trivial. In docs/tested-configurations.md, rows were added at the top. In tests/test_hdr_encode.py, main already loosened the DLSS engine check to the "hdr" substring, so main's version can be kept and the regex dropped. With that, test_hdr_encode, test_fsr3_math, test_fsr3_shader_options, test_temporal_history and test_temporal_jitter all pass on Linux. The change itself looks right. chain_prepare decides HDR from present_native_pq the same way DLSS and FSR 4 do, chain_hdr hands the chain its HDR prep/out pair, and a change of encoding resets the history (ai.reset = f.reset || c.hdr_changed). SDR and expanded-SDR HDR keep the LDR passes because chain_hdr is false there. The passes were already built with HDR_COLOR_INPUT=1, so no shader changes are needed. The PQ-code numbers from the Castle Town market are the measurement this needs. Please rebase. Once it's rebased, I'd merge it. <!-- github-sync:r:5876221659 -->
Commenting is not possible because the repository is archived.
No description provided.