feat(fsr4): FSR 4 for the game's native HDR, given linear light #2342

Merged
unixman69 merged 7 commits from feat/fsr4-native-hdr into main 2026-11-11 18:19:17 +00:00
unixman69 commented 2026-11-11 15:15:48 +00:00 (Migrated from github.com)

With HDR on Native and AMD FSR 4 as the upscaler, every highlight is cut at the paper white. #2243 gives DLSS the game's HDR colour as linear light, but FSR 4 keeps its LDR context and the chain's clamped colour ("FSR 4 has none and keeps the LDR pair"). This PR gives FSR 4 the same treatment, on #2243's mechanism.

Builds on #2243 (which builds on #1942 and #1941). This PR is the last commit, 6b55c5ba. Review and merge it after those.

What changed

  • vk_fsr4.cpp: while present_native_pq says the game presents PQ, FSR 4's context is made with FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE | FFX_UPSCALE_ENABLE_AUTO_EXPOSURE (linear colour, AMD's own exposure) instead of FFX_UPSCALE_ENABLE_NON_LINEAR_COLORSPACE.
    • The context is made again when that changes, as #2243 does for DLSS's feature. It's keyed on what the game presents, not on the option.
    • The dispatch drops FFX_UPSCALE_FLAG_NON_LINEAR_COLOR_SRGB. On Linux, the unit exposure image the LDR context needs isn't passed: the HDR context works out its own.
    • The engine reports it through TemporalEngine::hdr, so the chain uses #2243's HDR pair: linear light in, unclamped, and the linear picture out as it is.
  • RX_VK_FSR4_HDR=0 keeps the LDR context and the clamped colour with Native too, for comparisons (docs/development.md, the readme's FSR 4 section).
  • Tests: test_hdr_encode.py's new check_fsr4 checks:
    • that prepare keys the context on present_native_pq and remakes it when that changes;
    • the HDR and LDR creation flags;
    • the dispatch's colour flag;
    • that the engine reports hdr.
      It fails on #2243 without this commit.
  • docs/tested-configurations.md: the run below.

Testing

  • tests/run_all.py: 363 passed, 0 failed, 11 skipped (Linux), on main 49f75dc8 + #1941 + #1942 + #2243 + this commit. The renderer builds with and without HOOKSHOT_FSR.

  • In game: RX 9070 XT 16 GB, Mesa RADV 26.2.6, Gentoo Linux (kernel 7.3.3), KDE Plasma Wayland, windowed.

    • FSR 4 came from fsr4-vk (FSR 4.1.1, FP8 path). Output was 2560x1440 HDR10, HDR on Native, 110 nits paper white and 400 nits peak, with FSR 4 Native AA, in Kokiri Forest.
    • Renderer: main 49f75dc8 + #1941 + #1942 + #2243 + this commit. #2243 is based on d7a7306, which has renderer API 14, while this host needs 15, so the stack was cherry-picked onto current main for testing. All four commits apply cleanly.
    Brightest pixel (PQ code, 8-bit) 99.9th percentile Rest of the scene
    RX_VK_FSR4_HDR=0 (as with #2243 alone) 132, the paper white 116 reference
    This PR (two runs) 135-145 116 within one code value
    • The log says AMD FSR 4: FSR 4.1.1 Vulkan FP8 made for outputs of 2560x1440 ..., HDR colour, linear (the game's native HDR).
    • All 600 frames were drawn. Brightness was steady frame to frame while standing: the mean moved by 0.03 code values at most.
    • I also tried keeping the unit exposure instead of AMD's auto exposure. It reached 140, with the same tones and a less steady mean (0.11). That's why the PR uses auto exposure, as #2243 does for DLSS.
    • SDR is unchanged: FSR 4 Native AA flicker and outline numbers match the build without this change.

Not tested:

  • Windows with AMD's signed FSR SDK 2.4 DLLs (FSR 4.1.2). They get the same creation and dispatch flags; only skipping the exposure image is Linux-specific. A Windows RX 7000/9000 run would be welcome before merging.
  • FSR 4's upscaling modes (render size below the output) in HDR.
  • Turning HDR on Native on or off mid-run, which makes the context again.
  • Scenes other than Kokiri Forest.

🤖 Generated with Claude Code

With HDR on Native and AMD FSR 4 as the upscaler, every highlight is cut at the paper white. #2243 gives DLSS the game's HDR colour as linear light, but FSR 4 keeps its LDR context and the chain's clamped colour ("FSR 4 has none and keeps the LDR pair"). This PR gives FSR 4 the same treatment, on #2243's mechanism. > Builds on #2243 (which builds on #1942 and #1941). This PR is the last commit, 6b55c5ba. Review and merge it after those. **What changed** - `vk_fsr4.cpp`: while `present_native_pq` says the game presents PQ, FSR 4's context is made with `FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE | FFX_UPSCALE_ENABLE_AUTO_EXPOSURE` (linear colour, AMD's own exposure) instead of `FFX_UPSCALE_ENABLE_NON_LINEAR_COLORSPACE`. - The context is made again when that changes, as #2243 does for DLSS's feature. It's keyed on what the game presents, not on the option. - The dispatch drops `FFX_UPSCALE_FLAG_NON_LINEAR_COLOR_SRGB`. On Linux, the unit exposure image the LDR context needs isn't passed: the HDR context works out its own. - The engine reports it through `TemporalEngine::hdr`, so the chain uses #2243's HDR pair: linear light in, unclamped, and the linear picture out as it is. - `RX_VK_FSR4_HDR=0` keeps the LDR context and the clamped colour with Native too, for comparisons (`docs/development.md`, the readme's FSR 4 section). - Tests: `test_hdr_encode.py`'s new `check_fsr4` checks: - that `prepare` keys the context on `present_native_pq` and remakes it when that changes; - the HDR and LDR creation flags; - the dispatch's colour flag; - that the engine reports `hdr`. It fails on #2243 without this commit. - `docs/tested-configurations.md`: the run below. **Testing** - `tests/run_all.py`: 363 passed, 0 failed, 11 skipped (Linux), on main 49f75dc8 + #1941 + #1942 + #2243 + this commit. The renderer builds with and without `HOOKSHOT_FSR`. - In game: RX 9070 XT 16 GB, Mesa RADV 26.2.6, Gentoo Linux (kernel 7.3.3), KDE Plasma Wayland, windowed. - FSR 4 came from fsr4-vk (FSR 4.1.1, FP8 path). Output was 2560x1440 HDR10, HDR on Native, 110 nits paper white and 400 nits peak, with FSR 4 Native AA, in Kokiri Forest. - Renderer: main 49f75dc8 + #1941 + #1942 + #2243 + this commit. #2243 is based on d7a7306, which has renderer API 14, while this host needs 15, so the stack was cherry-picked onto current main for testing. All four commits apply cleanly. | | Brightest pixel (PQ code, 8-bit) | 99.9th percentile | Rest of the scene | |---|---|---|---| | `RX_VK_FSR4_HDR=0` (as with #2243 alone) | 132, the paper white | 116 | reference | | This PR (two runs) | 135-145 | 116 | within one code value | - The log says `AMD FSR 4: FSR 4.1.1 Vulkan FP8 made for outputs of 2560x1440 ..., HDR colour, linear (the game's native HDR)`. - All 600 frames were drawn. Brightness was steady frame to frame while standing: the mean moved by 0.03 code values at most. - I also tried keeping the unit exposure instead of AMD's auto exposure. It reached 140, with the same tones and a less steady mean (0.11). That's why the PR uses auto exposure, as #2243 does for DLSS. - SDR is unchanged: FSR 4 Native AA flicker and outline numbers match the build without this change. **Not tested:** - Windows with AMD's signed FSR SDK 2.4 DLLs (FSR 4.1.2). They get the same creation and dispatch flags; only skipping the exposure image is Linux-specific. A Windows RX 7000/9000 run would be welcome before merging. - FSR 4's upscaling modes (render size below the output) in HDR. - Turning HDR on Native on or off mid-run, which makes the context again. - Scenes other than Kokiri Forest. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
hckrman101 (Migrated from github.com) approved these changes 2026-11-11 17:35:41 +00:00
hckrman101 (Migrated from github.com) left a comment

Reviewed the last commit (6b55c5ba); the rest of the diff is the #1941/#1942/#2243 stack, so this has to merge after those (or along with them). Nobody has reviewed #2243 yet either.

This looks right to me, and it closely follows what #2243 does for DLSS:

  • The context is keyed on present_native_pq, not on the option, so Native with a non-PQ picture stays LDR. It's made again when that changes, with the same reason in the log as DLSS's.
  • HDR context: FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE | FFX_UPSCALE_ENABLE_AUTO_EXPOSURE with no NON_LINEAR_COLORSPACE, and the dispatch flags go to 0. That matches what the chain feeds it through TemporalEngine::hdr: #2243's prep_hdr/out_hdr pair, linear light in an R16G16B16A16_SFLOAT colour image and linear back out (out_hdr_square = false). preExposure stays 1, which is right with auto exposure. Skipping the Linux unit exposure image for the HDR context is correct; leaving it in would mean an exposure of 1 on top of the auto-exposure flag.
  • hdr() reads the context that prepare just made, and the temporal pass calls prepare before choosing the prep/out pipelines, so the chain and the context can't disagree within a frame.
  • The LDR path (flags, sRGB dispatch flag, unit exposure on Linux) is byte for byte what it was, so SDR doesn't change, and your Native AA flicker numbers back that up.

The test on real RDNA4 hardware (9070 XT, fsr4-vk 4.1.1 FP8) with the A/B through RX_VK_FSR4_HDR=0 is convincing: highlights go past 132, the rest of the scene stays within a code value, and brightness holds steady. The auto vs. unit exposure comparison is useful too.

Things that are still open, all listed under Not tested:

  • Windows with AMD's signed 2.4 DLLs. Same flags and no exposure image (as before), so I'd expect it to work, but a run there would be nice.
  • Upscaling modes in HDR, and switching Native on and off mid-run (see the inline note).

Duplication with #2243 is small and fine for now: both engines repeat the extern present_native_pq and the hdr_allowed && present_native_pq key. If a third engine ever needs this, it could move into vk_temporal.h, or the chain could pass it to prepare. Not for this PR.

CI only has the Conventional Commit title check here (pending), the same as on #2243; the stack's test run is the one in the description. Approving this commit; it's good to go once #1941, #1942 and #2243 are in.

Reviewed the last commit (6b55c5ba); the rest of the diff is the #1941/#1942/#2243 stack, so this has to merge after those (or along with them). Nobody has reviewed #2243 yet either. This looks right to me, and it closely follows what #2243 does for DLSS: - The context is keyed on `present_native_pq`, not on the option, so Native with a non-PQ picture stays LDR. It's made again when that changes, with the same reason in the log as DLSS's. - HDR context: `FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE | FFX_UPSCALE_ENABLE_AUTO_EXPOSURE` with no `NON_LINEAR_COLORSPACE`, and the dispatch flags go to 0. That matches what the chain feeds it through `TemporalEngine::hdr`: #2243's `prep_hdr`/`out_hdr` pair, linear light in an R16G16B16A16_SFLOAT colour image and linear back out (`out_hdr_square = false`). `preExposure` stays 1, which is right with auto exposure. Skipping the Linux unit exposure image for the HDR context is correct; leaving it in would mean an exposure of 1 on top of the auto-exposure flag. - `hdr()` reads the context that `prepare` just made, and the temporal pass calls `prepare` before choosing the prep/out pipelines, so the chain and the context can't disagree within a frame. - The LDR path (flags, sRGB dispatch flag, unit exposure on Linux) is byte for byte what it was, so SDR doesn't change, and your Native AA flicker numbers back that up. The test on real RDNA4 hardware (9070 XT, fsr4-vk 4.1.1 FP8) with the A/B through `RX_VK_FSR4_HDR=0` is convincing: highlights go past 132, the rest of the scene stays within a code value, and brightness holds steady. The auto vs. unit exposure comparison is useful too. Things that are still open, all listed under *Not tested*: - Windows with AMD's signed 2.4 DLLs. Same flags and no exposure image (as before), so I'd expect it to work, but a run there would be nice. - Upscaling modes in HDR, and switching Native on and off mid-run (see the inline note). Duplication with #2243 is small and fine for now: both engines repeat the `extern present_native_pq` and the `hdr_allowed && present_native_pq` key. If a third engine ever needs this, it could move into `vk_temporal.h`, or the chain could pass it to `prepare`. Not for this PR. CI only has the Conventional Commit title check here (pending), the same as on #2243; the stack's test run is the one in the description. Approving this commit; it's good to go once #1941, #1942 and #2243 are in.
hckrman101 (Migrated from github.com) commented 2026-11-11 17:35:41 +00:00

Nit (shared with #2243's DLSS prepare): present_native_pq is stored as false on any present without a texture (tex && ... in do_present). So if the game ever presents a frame with no texture, or switches to a non-PQ picture for a moment (a movie or a loading frame) while Native is on, the FSR 4 context is torn down and made twice, which costs a hitch and drops the history each time. I don't think it happens in normal play, but it would be worth looking for repeated made again: the game's native HDR turned lines in a longer HDR session. If they show up, a few frames of hysteresis would fix it for both engines.

Nit (shared with #2243's DLSS `prepare`): `present_native_pq` is stored as `false` on any present without a texture (`tex && ...` in `do_present`). So if the game ever presents a frame with no texture, or switches to a non-PQ picture for a moment (a movie or a loading frame) while Native is on, the FSR 4 context is torn down and made twice, which costs a hitch and drops the history each time. I don't think it happens in normal play, but it would be worth looking for repeated `made again: the game's native HDR turned` lines in a longer HDR session. If they show up, a few frames of hysteresis would fix it for both engines.
unixman69 (Migrated from github.com) reviewed 2026-11-11 18:12:48 +00:00
unixman69 (Migrated from github.com) commented 2026-11-11 18:12:47 +00:00

Checked rather than changed: three 90 s HDR Native sessions with FSR 4 Native AA (RX 9070 XT, RADV 26.2.6, two with a warp to Castle Town and its loading frames) each made FSR 4's context once and logged no made again: the game's native HDR turned line. The presented picture changed from the SDR expansion to PQ only at start, before FSR 4 drew, so no hysteresis for now; if it shows up later it fits best in the shared present_native_pq (both engines).

🤖 Addressed by Claude Code

Checked rather than changed: three 90 s HDR Native sessions with FSR 4 Native AA (RX 9070 XT, RADV 26.2.6, two with a warp to Castle Town and its loading frames) each made FSR 4's context once and logged no `made again: the game's native HDR turned` line. The presented picture changed from the SDR expansion to PQ only at start, before FSR 4 drew, so no hysteresis for now; if it shows up later it fits best in the shared `present_native_pq` (both engines). _🤖 Addressed by [Claude Code](https://claude.com/claude-code)_
Commenting is not possible because the repository is archived.
No description provided.