Repository navigation
feat(fsr4): FSR 4 for the game's native HDR, given linear light - #2342
Merged
Merged
Conversation
Players asked for a contrast setting for HDR, as the RenoDX HDR mods have.
- shaders/hdr_encode.comp: after its SDR expansion (mode 0, Expanded, as
before) or the game's own PQ picture decoded (mode 1, Native), the light
is graded as RenoDX's Contrast does it: on the luminance relative to the
paper white, pow(Y * exposure / 0.18, contrast) * 0.18, the hue kept, and
it is PQ coded again. Exposure is pushed as 1 (none) for now. Push
constants: 48 bytes (crop, paper white, peak, knee, mode, contrast,
exposure, two pads).
- A shoulder eases the graded light back onto the picture's top, the
peak / paper white (3.5 at 800 and 230 nits): above a knee (min(1, half
the top)) with Reinhard's rational shape, meeting the graded curve at the
knee with its slope and reaching the peak at the graded top. A contrast
above 1 lifted the top far past the peak (about 3500 nits at 150%), and a
plain limit there cut every highlight flat. With the top its own target
the shoulder is the identity at 100%, so the grade is continuous with the
ungraded picture.
- Expanded maps the SDR white to the peak; after the shoulder the
display's peak still limits it by luminance.
- Native is fitted under the peak the game is told: the HDR peak row,
which Hookshot passes to the game as its display's peak, and which the
game's own light stays under (about 700 nits at most with an 800-nit
row). Light the game puts above it is not limited by the pass, as at
100%: past the top the shoulder goes on rising, and there is no peak
limit for Native. At 800 nits peak and 330 paper white, a 700-nit
highlight goes to about 720 nits at 105% and 780 at 150%.
At 100% the grading does not run, and the picture is the one
hdr_encode.comp made before.
- HDR contrast row (Graphics tab, with the other HDR rows, shown with HDR
on, in row_shown's paper white / peak clause): 50 to 200% in steps of 5,
100% by default, kept as hdr_contrast. With Native the game's picture
goes through the pass only when the row is not 100%; at 100% it is shown
as it is, as before.
- shaders/hdr_encode_comp_spv.h: glslangValidator 16.6.0 -V --target-env
vulkan1.2 --vn HDR_ENCODE_COMP_SPV, with the source's hash on its first
line as before.
- tests: test_hdr_encode.py runs the shader's own grade() and shoulder() in
Python. Expanded: nothing at 100%; 50-200% rising from black, the top
exactly on the peak, at most 3% of the picture at the peak. Native: the
identity at 100%; every PQ code from 0 to 1 rising strictly at 50-200%,
above the peak row too (not cut flat); the peak landing on itself; light
at or under the peak row staying at or under it; at 95% and 105% the
paper white and 0.9 of the peak within a fifth of the 100% picture. It
also checks the push constants against framegen_hdr's and the row. The
present fixtures take framegen_hdr's pq argument, the 48 bytes and the
graded Native case; test_upscaler_rows.py checks the row is hidden with
HDR off.
With Sharpness, the Window scaling filter or ultrawide's menu sides on,
Native's picture still reaches this pass through framegen_scale's RGBA8
image, 8 bits of PQ, until the next commit (fix/hdr-native-format) scales
it in 10 bits: the two are meant to land together.
Tested in game on the local v2.0.13 build, whose HLSL pass has this math
and this shoulder: RTX 5080, 3840x2160 HDR10, 800 nits peak and 230 nits
paper white, Expanded at 50/100/150%. Native graded under the peak row
has not been run in the game in this form; this GLSL port on main compiles
with the build's clang and passes the game-free checks.
Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…at 100% contrast With the Insert menu's HDR on Native, the game's HDR10 picture lost two bits whenever Sharpness, the Window scaling filter or ultrawide's menu sides were on: framegen_scale wrote its PQ codes into an RGBA8 image. Native was also told by the cached image's format, which said RGBA8 for the game's RGB10A2 picture while the two shared an image by format class; main's 0a28ff2 (NotRexed#2038) since gives RGB10A2 a class of its own. - do_present: Native is told by vk_format(t->d.format) alone, the format the game presents and the one its view is made in, not by the image's (which would take an SDR picture for PQ in an image first made as RGB10A2). One log line when the presented format, its image's format or the HDR path changes, for HDR reports. - present_image: Native always goes through the HDR pass, which reads the picture through that view. At 100% contrast and exposure 1 the pass writes the game's PQ codes as they are (hdr_encode.comp: no decode and encode in between), so the game's highlights reach the display as it made them. Graded off 100% (the previous commit), Native is fitted under the peak the game is told, is not limited by the pass above it, and moves on smoothly from this picture. When the pass cannot be made, Native shows its scaled picture, else black (said once), never its image blitted, which converts from the image's own format. - framegen_scale(pq): with the Window scaling filter, Sharpness or ultrawide's menu sides, the game's picture is scaled into RGB10A2 images (the scaled one, the crop and the 1/8 picture of the blurred sides) by fg_scale_pq, made at its first use, not into RGBA8 ones. Its crop is copied as bits, not blitted. The RX_VK_SHOT_FX shot passes present_native_pq too, so a shot neither scales Native as SDR nor makes the images again in the other format. - shaders/fg_scale.comp: FG_SCALE_PQ makes its destination rgb10_a2 and its dither a 10-bit step. fg_scale_pq_comp_spv.h is fg_scale.comp compiled with -DFG_SCALE_PQ; both headers come from tests/test_fg_scale_sharpen.py --write (glslangValidator 16.6.0), and the RGBA8 one's code is unchanged. hdr_encode_comp_spv.h: glslangValidator 16.6.0 -V --target-env vulkan1.2, with its source's hash line. - tests: test_fg_scale_sharpen.py checks both headers against the shader and what FG_SCALE_PQ changes; upscale_independent_test.cpp runs framegen_scale's 10-bit path (its pipeline, RGB10A2 images, the crop copied, back to RGBA8 after it, a pipeline that cannot be made); test_hdr_encode.py checks do_present's format test (the presented format alone), the pass-through at 100%, that present_image never blits Native's image and the shot's pq; test_present_aspect.py takes Native through the HDR pass, and without it to its scaled picture or black; present_aspect_vulkan.cpp's stubs follow present_image (framegen_scale's pq, framegen_hdr, dev.hdr), so it compiles and runs again. Tested in game on the local v2.0.13 build with the format detection and the 10-bit scaling: RTX 5080, 3840x2160 HDR10, 800 nits peak and 230 nits paper white; Native looks right. The pass-through at 100% and the black fallback are new and have not been run in the game; the port on main compiles with the build's clang and passes the game-free checks. Assisted-by: Claude Code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the game presents its own HDR10 picture (the Insert menu's HDR on Native), its scene colour goes past 1. DLSS's feature was made for LDR colour and the chain's inputs pass clamped the scene to 1, so every highlight above the paper white was cut before DLSS saw it. - vk_dlss.cpp, vk_temporal.cpp/.h: while present_native_pq says the game presents PQ (not merely the option and an HDR10 swapchain: with Native picked and an SDR picture shown, the scene is LDR), DLSS's feature is made with IsHDR | AutoExposure, and made again when that changes. The engine says so through TemporalEngine::hdr; FSR 4 has none and keeps the LDR pair. present_native_pq moves out of vk_replay.cpp's anonymous namespace for that. - The chain then runs a second pair of its passes. dlss_prep.comp's kSquare (constant 1) squares the square-root encoded scene into linear light, which NVIDIA documents for IsHDR and from which AutoExposure works out its exposure, and its kColorMax (constant 0) is 16384 in place of 1, well inside the colour image's RGBA16F. dlss_out.comp's kSquare (constant 0) is off for it: the network's picture is linear already and is written as it is, the same output scale as the LDR pair, which still squares the square-root encoded picture. - shaders/dlss_prep_comp_spv.h, dlss_out_comp_spv.h: glslangValidator 16.6.0 -V --target-env vulkan1.3 (the version CI pins, the command tests/test_dlss_actor.py checks them with). - tests: test_hdr_encode.py checks the two constants in each shader, the HDR pipelines and their specialization data, that temporal_upscale binds the HDR pair for an engine whose hdr() says so, and that prepare keys the feature on present_native_pq, not on the option. RX_VK_DLSS_HDR=0 keeps the LDR feature and the clamped colour with Native too, for comparisons (docs/development.md). Tested in game: RTX 5080, 3840x2160 HDR10, 800 nits peak and 330 nits paper white, Native at 100% contrast, DLSS Balanced, the Deku Tree's light shaft. With RX_VK_DLSS_HDR=0 (as on main) the brightest light was 328 nits, the paper white, and the hole of light a flat disc; with this change it reached 749 nits, the 99.9th percentile 654 against 328, with the light's structure back. The log says "HDR colour, linear (the game's native HDR)". Assisted-by: Claude Code Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With HDR on Native, AMD FSR 4 kept the LDR context and the chain's clamped colour (NotRexed#2243 left it so), so every highlight was cut at the paper white, as DLSS's were before NotRexed#2243. - 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) in place of FFX_UPSCALE_ENABLE_NON_LINEAR_COLORSPACE, and made again when that changes, as DLSS's feature is. Its dispatch then drops FFX_UPSCALE_FLAG_NON_LINEAR_COLOR_SRGB, and on Linux the unit exposure the LDR context needs is not passed. The engine reports it through TemporalEngine::hdr, so the chain gives it NotRexed#2243's HDR pair: linear light in, unclamped, and its 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, readme's FSR 4 section). - tests: test_hdr_encode.py checks that prepare keys the context on present_native_pq (not on the option) and makes it again when that changes, the HDR and LDR creation flags, the dispatch's colour flag, and that the engine reports hdr. It fails on NotRexed#2243 without this commit. - docs/tested-configurations.md: the run below. Tested in game: RX 9070 XT, Mesa RADV 26.2.6, Linux, fsr4-vk (FSR 4.1.1, FP8), 2560x1440 HDR10 at 110 nits paper white and 400 nits peak, FSR 4 Native AA, Kokiri Forest. With RX_VK_FSR4_HDR=0 (as with NotRexed#2243 alone) the brightest pixel was PQ code 132, the paper white; with this change 135-145 (two runs). The rest of the scene stays within one code value, and the frame-to-frame brightness is steady. The log says "HDR colour, linear (the game's native HDR)" when the context is made. SDR is unchanged. Builds with and without HOOKSHOT_FSR. Not tested: Windows with AMD's signed FSR SDK 2.4 DLLs (FSR 4.1.2), which get the same creation and dispatch flags; FSR 4's upscaling modes (render size below the output) in HDR; HDR on Native turned on or off mid-run (the context made again). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # gpu/vk/vk_replay.cpp
# Conflicts: # gpu/vk/vk_replay.cpp
…/fsr4-native-hdr # Conflicts: # docs/tested-configurations.md
This was referenced Nov 11, 2026
This was referenced Nov 11, 2026
This was referenced Nov 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
What changed
vk_fsr4.cpp: whilepresent_native_pqsays the game presents PQ, FSR 4's context is made withFFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE | FFX_UPSCALE_ENABLE_AUTO_EXPOSURE(linear colour, AMD's own exposure) instead ofFFX_UPSCALE_ENABLE_NON_LINEAR_COLORSPACE.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.TemporalEngine::hdr, so the chain uses feat(dlss): DLSS for the game's native HDR, given linear light #2243's HDR pair: linear light in, unclamped, and the linear picture out as it is.RX_VK_FSR4_HDR=0keeps the LDR context and the clamped colour with Native too, for comparisons (docs/development.md, the readme's FSR 4 section).test_hdr_encode.py's newcheck_fsr4checks:preparekeys the context onpresent_native_pqand remakes it when that changes;hdr.It fails on feat(dlss): DLSS for the game's native HDR, given linear light #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 49f75dc + feat(hdr): HDR contrast row, also grading the game's native HDR #1941 + fix(hdr): Native HDR scaled in 10 bits and shown as the game made it at 100% contrast #1942 + feat(dlss): DLSS for the game's native HDR, given linear light #2243 + this commit. The renderer builds with and withoutHOOKSHOT_FSR.In game: RX 9070 XT 16 GB, Mesa RADV 26.2.6, Gentoo Linux (kernel 7.3.3), KDE Plasma Wayland, windowed.
RX_VK_FSR4_HDR=0(as with feat(dlss): DLSS for the game's native HDR, given linear light #2243 alone)AMD FSR 4: FSR 4.1.1 Vulkan FP8 made for outputs of 2560x1440 ..., HDR colour, linear (the game's native HDR).Not tested:
🤖 Generated with Claude Code