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

Closed
j64esatxzdrhfg wants to merge 1 commit from fix/hdr-menu-sharpness-native into main
j64esatxzdrhfg commented 2026-11-10 21:23:26 +00:00 (Migrated from github.com)

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. (#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 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. (#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](https://raw.githubusercontent.com/j64esatxzdrhfg/Hookshot/pr-screenshots/hdr-sharpness/menu-sharpness-100.jpg) | ![The game with HDR Expanded and Sharpness 100%](https://raw.githubusercontent.com/j64esatxzdrhfg/Hookshot/pr-screenshots/hdr-sharpness/expanded-sharpness-100.jpg) | _HDR10 captures, tone-mapped to SDR for this page (paper white to white, highlights rolled off up to 800 nits)._ 🤖 Generated with [Claude Code](https://claude.com/claude-code)
sling86 commented 2026-11-11 00:33:42 +00:00 (Migrated from github.com)

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.
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.
hckrman101 (Migrated from github.com) reviewed 2026-11-11 02:18:22 +00:00
hckrman101 (Migrated from github.com) left a comment

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.

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.
@ -291,3 +296,4 @@
else if (s.feature_hdr != hdr) snprintf(why, sizeof why, "the game's native HDR turned %s", hdr ? "on" : "off");
else if (s.feature_ow != ow || s.feature_oh != oh)
snprintf(why, sizeof why, "the output went from %ux%u to %ux%u", s.feature_ow, s.feature_oh, ow, oh);
else if (s.feature_preset != preset)
hckrman101 (Migrated from github.com) commented 2026-11-11 02:18:22 +00:00

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.

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.
hckrman101 (Migrated from github.com) commented 2026-11-11 02:18:22 +00:00

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.

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 commented 2026-11-11 10:39:20 +00:00 (Migrated from github.com)

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.

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.
Commenting is not possible because the repository is archived.
No description provided.