Skip to content

feat(dlss): DLSS for the game's native HDR, given linear light - #2243

Closed
j64esatxzdrhfg wants to merge 3 commits into
NotRexed:mainfrom
j64esatxzdrhfg:feat/dlss-native-hdr
Closed

j64esatxzdrhfg wants to merge 3 commits into
NotRexed:mainfrom
j64esatxzdrhfg:feat/dlss-native-hdr

Conversation

@j64esatxzdrhfg

Copy link
Copy Markdown
Contributor

With HDR on Native and DLSS as the upscaler, every highlight is cut at the paper white: DLSS's feature is made for LDR colour and the chain's inputs pass clamps the scene to 1 before DLSS sees it. The hole of light in the Deku Tree turns into a flat disc.

Builds on #1942 (which builds on #1941): this PR is the last commit. It is the DLSS-for-Native part that was split out of #1940, reworked as reviewed there: keyed on what the game presents, and given linear light.

What changed

  • vk_dlss.cpp, vk_temporal.cpp/.h: while present_native_pq says the game presents its own PQ picture, DLSS's feature is made with IsHDR | AutoExposure and made again when that changes. The gate is the presented picture, not the option and an HDR10 swapchain; with Native picked and an SDR picture shown, the scene is LDR. The engine reports it 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.
    • 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, at the same output scale as the LDR pair.
  • shaders/dlss_prep_comp_spv.h, dlss_out_comp_spv.h: glslang 16.6.0, --target-env vulkan1.3 (the command tests/test_dlss_actor.py checks them with).
  • RX_VK_DLSS_HDR=0 keeps the LDR feature and the clamped colour with Native too, for comparisons (docs/development.md).
  • 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 (and the switch), not on the option.

Testing

  • In game: RTX 5080, 3840x2160 HDR10, 800 nits peak, 330 nits paper white, Native at 100% contrast, DLSS Balanced, in a build of main (d7a7306) with 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 and this PR. Same spot, the Deku Tree's light shaft:
    • With RX_VK_DLSS_HDR=0, as on main, the brightest light was 328 nits, which is the paper white. The 99.9th percentile was 328.
    • With this change it reached 749 nits (99.9th percentile 654), and the light's structure is back.
    • The log says HDR colour, linear (the game's native HDR) when the feature is made.
    • This answers the review's question on fix(hdr): Sharpness with HDR, and DLSS for the game's native HDR #1940 about the largest scene value: in this scene the game's grade does go well past the paper white once DLSS stops clamping.
  • vk_dlss.cpp compiles with and without NGX; the touched files compile with the build's clang. tests/run_all.py with hdr_encode, dlss_actor, dlss_checks, upscale_independent, framegen_resources, present_, temporal_history, status_line and menu pass, except recorder_sync and present_semaphores, which fail the same way on main (this PC's toolchain).

Not tested: other NVIDIA generations, DLAA with Native, displays with another peak.

Screenshots (Native at 100%, DLSS Balanced; HDR10 captures tone-mapped to SDR for this page, paper white to white, highlights rolled off up to 800 nits)

Before (RX_VK_DLSS_HDR=0, as on main): 328 nits at most After: 749 nits at most
DLSS with Native HDR before: the light clipped at the paper white DLSS with Native HDR after: the light graded up to 749 nits

The hole of light at full size, before and after:

The light shaft: a flat disc before, its structure after

🤖 Generated with Claude Code

j64esatxzdrhfg and others added 3 commits November 11, 2026 11:12
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>

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this one is already merged, by another route. #2342 (FSR 4 for the game's native HDR) was stacked on this PR, and its squash merge, a0dfd28 on main (11 Nov), carried the whole stack: #1941, #1942 and this commit. I checked at this PR's head against main:

  • the shaders and their SPIR-V headers (dlss_prep, dlss_out, hdr_encode, fg_scale, the new fg_scale_pq_comp_spv.h) and vk_temporal.h are byte-identical on main;
  • the changes this PR makes to vk_dlss.cpp, vk_temporal.cpp, vk_replay.cpp and vk_framegen.cpp are exactly the ones a0dfd28 made;
  • tests/test_hdr_encode.py differs only by #2342's own check_fsr4 additions;
  • RX_VK_DLSS_HDR=0 and the present_native_pq gate in prepare are on main (gpu/vk/vk_dlss.cpp:308-310).

That is also why GitHub shows conflicts in vk_replay.cpp, docs/development.md and tests/test_hdr_encode.py: main has the same lines plus what came after. Unless there is something in this branch I missed that isn't in a0dfd28, this PR can be closed as merged through #2342, and #1941 and #1942 probably too (worth a quick check of each before closing). If a follow-up is still needed, a fresh branch from main is easier than rebasing this stack.

One note for the changelog: the release notes take their line from the squash title, so the DLSS part shipped under feat(fsr4): .... If DLSS for native HDR should appear in the release notes, it needs a line added there by hand.

@Joey-Magnus Joey-Magnus left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks already merged: #2342's squash (a0dfd28b) shipped it. All nine shader and SPIR-V header files and vk_temporal.h are identical to main, and a test merge leaves no difference in the files that merged cleanly. I think this can be closed. Since it went out under a feat(fsr4) title, DLSS with native HDR probably needs a hand-written line in the next release notes.

@j64esatxzdrhfg

Copy link
Copy Markdown
Contributor Author

Closing as landed: #2342's squash merge (a0dfd28) carried this commit, RX_VK_DLSS_HDR included. Thanks @Joey-Magnus for checking. Agreed on the release notes: since it went out under a feat(fsr4) title, "DLSS with Native HDR gets the game's HDR colour as linear light (highlights no longer clipped at the paper white)" deserves its own line.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants