Repository navigation
feat(dlss): DLSS for the game's native HDR, given linear light - #2243
j64esatxzdrhfg wants to merge 3 commits into
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>
Joey-Magnus
left a comment
There was a problem hiding this comment.
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 newfg_scale_pq_comp_spv.h) andvk_temporal.hare byte-identical on main; - the changes this PR makes to
vk_dlss.cpp,vk_temporal.cpp,vk_replay.cppandvk_framegen.cppare exactly the ones a0dfd28 made; tests/test_hdr_encode.pydiffers only by #2342's owncheck_fsr4additions;RX_VK_DLSS_HDR=0and thepresent_native_pqgate inprepareare 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
left a comment
There was a problem hiding this comment.
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.
|
Closing as landed: #2342's squash merge (a0dfd28) carried this commit, |
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.
What changed
vk_dlss.cpp,vk_temporal.cpp/.h: whilepresent_native_pqsays the game presents its own PQ picture, DLSS's feature is made withIsHDR | AutoExposureand 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 throughTemporalEngine::hdr; FSR 4 has none and keeps the LDR pair.present_native_pqmoves out ofvk_replay.cpp's anonymous namespace for that.dlss_prep.comp'skSquare(constant 1) squares the square-root encoded scene into linear light, which NVIDIA documents forIsHDRand from whichAutoExposureworks out its exposure.kColorMax(constant 0) is 16384 in place of 1, well inside the colour image's RGBA16F.dlss_out.comp'skSquare(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 commandtests/test_dlss_actor.pychecks them with).RX_VK_DLSS_HDR=0keeps the LDR feature and the clamped colour with Native too, for comparisons (docs/development.md).test_hdr_encode.pychecks the two constants in each shader, the HDR pipelines and their specialization data, thattemporal_upscalebinds the HDR pair for an engine whosehdr()says so, and thatpreparekeys the feature onpresent_native_pq(and the switch), not on the option.Testing
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.HDR colour, linear (the game's native HDR)when the feature is made.vk_dlss.cppcompiles with and without NGX; the touched files compile with the build's clang.tests/run_all.pywithhdr_encode,dlss_actor,dlss_checks,upscale_independent,framegen_resources,present_,temporal_history,status_lineandmenupass, exceptrecorder_syncandpresent_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)
RX_VK_DLSS_HDR=0, as on main): 328 nits at mostThe hole of light at full size, before and after:
🤖 Generated with Claude Code