Skip to content

fix(hdr): Sharpness with HDR, and DLSS for the game's native HDR - #1940

Closed
j64esatxzdrhfg wants to merge 1 commit into
NotRexed:mainfrom
j64esatxzdrhfg:fix/hdr-menu-sharpness-native
Closed

j64esatxzdrhfg wants to merge 1 commit into
NotRexed:mainfrom
j64esatxzdrhfg:fix/hdr-menu-sharpness-native

Conversation

@j64esatxzdrhfg

Copy link
Copy Markdown
Contributor

With HDR on, the picture goes black as soon as Sharpness, the Window scaling filter or ultrawide's menu sides are used, and with HDR on Native, DLSS clips every highlight at the paper white.

What changed

  • vk_framegen.cpp: framegen_scale's image is created with SAMPLED usage. The HDR pass samples it through framegen_scaled_view; without the flag it read black. (feat(vk): DLSS frame generation (NGX), more actor motion for DLSS, FidelityFX FSR 4 fallback (builds with #1833, #1834) #1831 carries the same one-line fix as part of its series; whichever lands first, the other drops it.)
  • vk_dlss.cpp, vk_temporal.cpp/.h: with Native HDR on an HDR10 swapchain, DLSS's feature is created with IsHDR | AutoExposure and recreated when that changes (TemporalEngine::hdr). The chain then runs dlss_prep.comp with its colour ceiling, the new specialization constant kColorMax (1 by default), at 16384. FSR 4 has no hdr() and keeps the clamped colour.
  • shaders/dlss_prep_comp_spv.h: regenerated with glslang 16.6.0 (the version CI pins).
  • vk_replay.cpp: one log line when the presented format or the HDR path changes, for HDR reports.
  • Tests: test_hdr_encode.py checks the SAMPLED usage and the native-HDR DLSS path; upscale_independent_test.cpp expects the new usage.

Testing

  • RTX 5080, Windows 11, 3840x2160 HDR10 120 Hz, DLSS Balanced. Sharpness 100% with HDR Expanded: the picture shows (screenshot). Before the fix it was black.
  • Run in a build of main (8c92fe3) with this series; earlier on v2.0.13.
  • tests/run_all.py: the checks touching this pass. The ones failing here fail the same on untouched main (local toolchain: present_aspect_vulkan, fpx_sticky).

Not tested: AMD/Intel GPUs, FSR 4 with Native HDR, Linux.

A question for review: DLSS gets the chain's existing (square-root encoded) colour with IsHDR, not linear light. It looked right in game, but you may prefer linear input.

Screenshots (HDR Expanded, Sharpness 100%; before the fix this picture was black)

Sharpness 100% with HDR The picture
Sharpness row at 100% with HDR on The game with HDR Expanded and Sharpness 100%

HDR10 captures, tone-mapped to SDR for this page (paper white to white, highlights rolled off up to 800 nits).

🤖 Generated with Claude Code

With HDR on, the picture went black whenever Sharpness, the Window scaling
filter or ultrawide's menu sides were on. With HDR on Native, DLSS was made
for LDR colour and its inputs pass clamped the scene to 1, so every
highlight was cut at the paper white: a flat, SDR-looking picture.

- vk_framegen.cpp: framegen_scale's picture is made with SAMPLED usage. The
  HDR pass (framegen_hdr) samples it through framegen_scaled_view; made
  without it, the pass read black.
- vk_dlss.cpp, vk_temporal.cpp/.h: with the game's native HDR (the Insert
  menu's HDR on Native, on an HDR10 swapchain) DLSS's feature is made with
  IsHDR | AutoExposure, and made again when that turns on or off. The engine
  says so through TemporalEngine::hdr, and the chain then runs
  dlss_prep.comp with its colour ceiling (kColorMax, a specialization
  constant, 1 by default) at 16384. FSR 4 has no hdr() and keeps the
  clamped colour.
- shaders/dlss_prep_comp_spv.h: glslangValidator 16.6.0 -V --target-env
  vulkan1.3 --vn DLSS_PREP_COMP_SPV from dlss_prep.comp (the version CI pins).
- vk_replay.cpp: one log line when the presented format or the HDR path
  changes, for HDR reports.
- tests: test_hdr_encode.py checks the scaled picture's SAMPLED usage and
  the native-HDR DLSS path; upscale_independent_test.cpp expects the new
  usage.

The local patch's menu part is left out: main draws the menu, the FPS
counter and the start-up screen with Dear ImGui into a view of the
swapchain in its own format, and on an HDR10 swapchain their colours and
the logo are PQ coded at the paper white (vk_imgui.cpp hdr10). No 8-bit
bitmap is copied into the 10-bit image any more.

Tested before the port, on v2.0.13: RTX 5080, 3840x2160 HDR10, Sharpness
0 and 50 with Expanded. The port compiles with the build's clang and passes
the game-free checks; it has not been run in the game.

Assisted-by: Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sling86

sling86 commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

Confirming the scale.scaled part of this on an RTX 5090, with the validation layer naming it.

Setup: v2.0.14 release (source 3a9998c3), Windows 11 Pro 26200, RTX 5090 at driver 621.14, a single 3840x2160 HDR10 display, DLSS Quality.

Before: HDR Expanded gave a black picture and Native gave a flat one, with frame generation off or on, as in #1613. Run with RX_VK_VALIDATION=1 and the Khronos layer, the HDR pass reports:

VUID-VkWriteDescriptorSet-descriptorType-00337
vkCmdPushDescriptorSetKHR(): pDescriptorWrites[0].pImageInfo[0].imageView references an image which was not created
with VK_IMAGE_USAGE_SAMPLED_BIT, but descriptorType is VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER.
VkImage ... was created with VK_IMAGE_USAGE_2_TRANSFER_SRC_BIT_KHR|VK_IMAGE_USAGE_2_STORAGE_BIT_KHR.

That's scale.scaled (vk_framegen.cpp:518 in v2.0.14), which framegen_hdr samples through framegen_scaled_view(). With HDR on, the present always takes the scaled route.

After: with only VK_IMAGE_USAGE_SAMPLED_BIT added to that make_img (hookshot_vk.dll rebuilt from the v2.0.14 source), Expanded shows a correct picture, with frame generation off and at 3x. No other validation errors came from the present path. The DenormFlushToZero / DenormPreserve ones in the same log come from the translated game shaders.

Native, as data for #1942 / #1941: I tried the same cause #1942 describes on v2.0.14 (Native also told by vk_format(t->d.format), then the game's RGB10A2 picture copied bit for bit with vkCmdCopyImage when the sizes match). The log confirms that path is taken (format 0x3d, 3840x2160). The picture is right but looks almost the same as SDR. Changing HDR peak brightness from 1200 to 4000 nits made no visible difference, so the game's own grade seems to ignore the peak it gets from QueryDisplayHdrStatus. Two notes on doing it that way:

  • The copy has to be skipped while frame generation is on. Its made frames are RGBA8, and copying them as RGB10A2 gives posterised blue and pink colours.
  • The game's SetLayerNextHdr*Metadata calls are still stubbed, so no content light levels reach the display.

@ghost ghost left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks. The SAMPLED fix in framegen_scale is correct and ready. It's the exact error @sling86 got from the validation layer on a 5090 (VUID-VkWriteDescriptorSet-descriptorType-00337 on scale.scaled), and they saw the same one-flag change fix Expanded. The test changes cover it.

The DLSS-for-Native half reads fine as code. The feature is made again when feature_hdr flips. The second dlss_prep pipeline with the specialization constant is wired correctly (map entry 0/0/4, layout and push constants match). hdr() follows what prepare made in the same frame. FSR 4's kEngine still compiles, since hdr defaults to null. I rebuilt dlss_prep_comp_spv.h with glslang 16.6.0 (--target-env vulkan1.3) and it matches the PR byte for byte. Two things before I'd call that half done:

  • Nothing shows yet what it does in game. The testing and screenshots cover Sharpness with HDR Expanded only. There's no Native-with-DLSS before/after. sling86's data from the Native path (the picture looks almost like SDR, and going from 1200 to 4000 nits peak changes nothing) suggests the game's own grade may rarely go past 1. A before/after pair, or the largest scene value seen with Native on, would back up "clips every highlight at the paper white".
  • Your question about linear input: NVIDIA's guide documents IsHDR as linear radiance, and AutoExposure works out its exposure from that signal. With the square-root encoding, DLSS's internal tonemap and auto exposure both see a compressed signal. That probably explains why it still looks fine, but it isn't what the network was trained on. In the HDR variant you could square in dlss_prep and skip the square in dlss_out (one more specialization constant on each), so DLSS gets linear light. That's not a blocker, but I'd lean towards linear. Details inline.

Stacking: #1941 and #1942 carry this commit (eea8875) as their first, so they'll rebase cleanly once this lands. #1831 carries the same SAMPLED line, as the description says. No CI ran on this branch (fork workflows not approved yet), so nothing is green or red here.

The Sharpness fix alone could merge today. Whether the DLSS-Native part goes in as is, or waits for an in-game comparison, is @NotRexed's call.

Comment thread gpu/vk/vk_dlss.cpp
const char** fail) {
State& s = st();
const unsigned preset = wanted_preset();
const bool hdr = dev.hdr && opts.hdr == 2; // (the game's native HDR: State::feature_hdr)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A small one: this gate is the option and the swapchain, not what the game actually presents. With Native picked but the game's picture not taking the PQ path (present_native_pq false, the case #1942 is about), DLSS still gets an IsHDR | AutoExposure feature fed LDR content. That's probably harmless, but it's a different network mode with its own exposure. Once #1942 settles how Native is detected, it may be worth keying this on the same test.

Comment thread gpu/vk/vk_dlss.cpp
// (LDR colour, already tonemapped: no exposure; motion at the render size, not jittered; depth not reversed. With the
// game's native HDR the colour goes past 1: an HDR feature, with DLSS's own exposure)
cp.InFeatureCreateFlags = NVSDK_NGX_DLSS_Feature_Flags_MVLowRes |
(hdr ? NVSDK_NGX_DLSS_Feature_Flags_IsHDR | NVSDK_NGX_DLSS_Feature_Flags_AutoExposure : 0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

On the linear-input question: with IsHDR, NVIDIA expects linear scene colour, and AutoExposure reads exposure from it. This passes the square-root encoded value, so both see a compressed signal. A kSquare constant in dlss_prep (c*c for the HDR pipeline) plus a matching one in dlss_out (no c *= c after) would hand DLSS linear light and give the same output scale. Not blocking.

@j64esatxzdrhfg

Copy link
Copy Markdown
Contributor Author

Closing this: the fix landed in main with #2038 (0a28ff2), the same SAMPLED usage on scale.scaled. Thanks @sling86 for confirming it with the validation layer on the 5090, and @hckrman101 for the review.

The DLSS-for-Native part will come as its own PR after an in-game comparison. As reviewed, it gives DLSS linear input and is gated on the format the game presents. #1941 and #1942 are rebased straight onto main without this commit.

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