Repository navigation
fix(hdr): Native HDR scaled in 10 bits and shown as the game made it at 100% contrast - #1942
j64esatxzdrhfg wants to merge 2 commits into
Conversation
hckrman101
left a comment
There was a problem hiding this comment.
Thanks, this is the piece #1941 was missing, and it answers both Native points from my review there: Native is now actually taken, and with Sharpness, the scaling filter or ultrawide's sides the game's PQ codes stay in 10 bits instead of going through RGBA8.
I went through the last commit (9aeb580) and it reads right:
shown_formatcomes fromvk_format(t->d.format), anddirect_viewis made bytexture_view(tex, t, ...), which also picks the view format fromt. So the scale pass and the HDR pass both read the RGB10A2 picture through an RGB10A2 view, even when the cached image is RGBA8 (format_classaliasing, MUTABLE_FORMAT images).- The crop goes through
vkCmdCopyImagewhenpqis set. That's a raw bit copy between two 4-byte formats, so RGB10A2 bits held in an RGBA8 image come through intact, where a blit would have converted them. - sling86's warning on #1940 (copying frame generation's RGBA8 made frames as RGB10A2 posterises them) doesn't apply here. The display thread calls
present_imagewithcrop == nullptrfor the made frames, so the copy branch never runs for them. They're sampled as RGBA8 holding PQ values, which the pq scale and HDR passes handle fine. rgb10_a2storage writes aren't a new requirement, sincehdr_encode.compalready writes that format. Iffg_scale_pqcan't be built, the code logs once and falls back to the unscaled HDR pass, and the test covers that.- Switching
pqresets the cached sizes, so the images get made again (with the device wait) in the other format. That only happens when the mode changes. - The SPIR-V variant check in
test_fg_scale_sharpen.pyand the string checks intest_hdr_encode.py/test_present_aspect.pymatch the code.
Open points:
- The
|| tex->format == A2B10G10R10half of the Native test (inline). Is there a reason to keep it now thatshown_formatexists? - At 100% contrast Native is no longer passed through untouched. It now goes through decode, then the peak limit, then encode, so anything the game puts above the HDR peak row gets scaled down to the peak. sling86 saw the game's grade ignore the peak it's given, so this can flatten highlights that used to reach the display. That's the same point as my inline on #1941, and it now applies at 100% too. If the pass is only needed at 100% to read through the right view,
hdr_encode.compcould skip the peak limit for mode 1 when contrast and exposure are both 1. That would keep it a pure re-encode. This is a judgment call, so I'd at least note it in the description. - Nit: if
framegen_hdrreturns null under Native,present_imagestill falls back toblit_crop_to_rect(src, ...)on the image. That's exactly the format-converting blit the comment says garbles the colours. It only happens on a failure path, but it could takescaledor skip the blit. - Nit: the
RX_VK_SHOT_FXcall toframegen_scale(vk_replay.cpp ~3078) still uses the defaultpq = false. Under Native, every shot flipsscale.pq, so the images are made twice with a device wait each time, and the shot goes through the RGBA8 path. It's debug-only, but passingpresent_native_pqthere is a one-word fix. - Nit, and not this PR's fault: the
framegen_scalestub intests/present_aspect_vulkan.cppstill has 8 parameters. That fixture already doesn't compile against main'spresent_image(nodev.hdr/framegen_hdr/present_native_pqin it), and run_all skips it without a Vulkan device. It's worth fixing while the signature is changing anyway.
Stacking: this branch carries #1940 (eea8875) and #1941 (69f7d3e) as its first two commits, so it can only land after them or together with them. My earlier suggestion to land #1941 and #1942 together still stands. #1943 to #1946 don't touch these hunks. #1944 adds lines near the end of present_image, well away from this change. No CI has run on this branch (fork workflows are waiting for approval), so nothing is green or red yet. Whether the DLSS-for-Native part from #1940 goes in as is is still @NotRexed's call, so I'm leaving this as a comment and not an approval.
| const VkFormat shown_format = t ? vk_format(t->d.format).format : VK_FORMAT_UNDEFINED; | ||
| present_native_pq.store(tex && dev.hdr && opts.hdr == 2 && | ||
| (shown_format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 || | ||
| tex->format == VK_FORMAT_A2B10G10R10_UNORM_PACK32), |
There was a problem hiding this comment.
With the format the game presents now known, why keep this half? It's the reverse of the bug this PR fixes. If the window's image was first made as RGB10A2 (the game started in HDR) and the game later presents an RGBA8 picture through it, this still says Native. The SDR picture then gets decoded as PQ, and with a crop the new vkCmdCopyImage copies RGBA8 bits into an RGB10A2 image, so you get garbage, not just a wrong tone. shown_format alone (it falls back to VK_FORMAT_UNDEFINED only when t is null, and tex needs t anyway) looks like the accurate test. If there's a case where vk_format(t->d.format) doesn't say RGB10A2 but the picture is PQ, a comment naming it would help.
There was a problem hiding this comment.
No good reason; it's dropped. Native is now shown_format == VK_FORMAT_A2B10G10R10_UNORM_PACK32 alone, and the comment says why the image's format isn't asked. test_hdr_encode.py fails if tex->format comes back into that store. With main's 0a28ff2 the two can't disagree for the window's textures any more, but the presented format is the one the view is made in, so it's the one to ask.
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>
9aeb580 to
bfc7fae
Compare
|
Thanks. This now builds on #1941 alone, rebased on main d7a7306, which includes 0a28ff2 (#2038). That fix took care of the cache aliasing this PR first described, so the description now says what's left: the presented-format test and the 10-bit path. On your points:
In game (a build of main d7a7306 with this series): the log says |
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. > Builds on #2243 (which builds on #1942 and #1941). This PR is the last commit, 6b55c5b. Review and merge it after those. **What changed** - `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) instead of `FFX_UPSCALE_ENABLE_NON_LINEAR_COLORSPACE`. - The context is made again when that changes, as #2243 does for DLSS's feature. It's keyed on what the game presents, not on the option. - The dispatch drops `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. - The engine reports it through `TemporalEngine::hdr`, so the chain uses #2243's HDR pair: linear light in, unclamped, and the 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`, the readme's FSR 4 section). - Tests: `test_hdr_encode.py`'s new `check_fsr4` checks: - that `prepare` keys the context on `present_native_pq` and remakes it when that changes; - the HDR and LDR creation flags; - the dispatch's colour flag; - that the engine reports `hdr`. It fails on #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 + #1941 + #1942 + #2243 + this commit. The renderer builds with and without `HOOKSHOT_FSR`. - In game: RX 9070 XT 16 GB, Mesa RADV 26.2.6, Gentoo Linux (kernel 7.3.3), KDE Plasma Wayland, windowed. - FSR 4 came from fsr4-vk (FSR 4.1.1, FP8 path). Output was 2560x1440 HDR10, HDR on Native, 110 nits paper white and 400 nits peak, with FSR 4 Native AA, in Kokiri Forest. - Renderer: main 49f75dc + #1941 + #1942 + #2243 + this commit. #2243 is based on d7a7306, which has renderer API 14, while this host needs 15, so the stack was cherry-picked onto current main for testing. All four commits apply cleanly. | | Brightest pixel (PQ code, 8-bit) | 99.9th percentile | Rest of the scene | |---|---|---|---| | `RX_VK_FSR4_HDR=0` (as with #2243 alone) | 132, the paper white | 116 | reference | | This PR (two runs) | 135-145 | 116 | within one code value | - The log says `AMD FSR 4: FSR 4.1.1 Vulkan FP8 made for outputs of 2560x1440 ..., HDR colour, linear (the game's native HDR)`. - All 600 frames were drawn. Brightness was steady frame to frame while standing: the mean moved by 0.03 code values at most. - I also tried keeping the unit exposure instead of AMD's auto exposure. It reached 140, with the same tones and a less steady mean (0.11). That's why the PR uses auto exposure, as #2243 does for DLSS. - SDR is unchanged: FSR 4 Native AA flicker and outline numbers match the build without this change. **Not tested:** - Windows with AMD's signed FSR SDK 2.4 DLLs (FSR 4.1.2). They get the same creation and dispatch flags; only skipping the exposure image is Linux-specific. A Windows RX 7000/9000 run would be welcome before merging. - FSR 4's upscaling modes (render size below the output) in HDR. - Turning HDR on Native on or off mid-run, which makes the context again. - Scenes other than Kokiri Forest. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: j64esatxzdrhfg <355041498+j64esatxzdrhfg@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Joey-Magnus
left a comment
There was a problem hiding this comment.
This is on main through #2342's squash merge (a0dfd28), which carried #1941, #1942 and #2243. I checked each file of this PR against origin/main:
- On main exactly: hdr_encode.comp and hdr_encode_comp_spv.h, fg_scale.comp, fg_scale_comp_spv.h and fg_scale_pq_comp_spv.h, vk_framegen.h, vk_menu, and every test except test_hdr_encode.py.
- The three files left differ only by later changes on main. In vk_framegen.cpp it's #2243's DLSS lines next to the
fg_scale_pqinclude (line 36). In vk_replay.cpp, theRX_VK_SHOT_FXread moved toenv_on(#2042) and theframegen_scale(..., present_native_pq)call is there at :3247. test_hdr_encode.py's docstring gained the DLSS paragraph.
Nothing is left to merge, so I'd close it. #2243 is still open even though its commit also landed through #2342, so it's probably worth checking too.
|
This now conflicts with main in |
|
Closing as landed through #2342's squash merge (a0dfd28): the 10-bit scaling, the presented-format test and the 100% pass-through are all on main. Thanks @Joey-Magnus for the file-by-file check and @hckrman101 for the review. |
With HDR on Native and Sharpness, the Window scaling filter or ultrawide's menu sides on, the game's HDR10 picture goes through an RGBA8 image. Its PQ codes lose two bits, which shows as banding in the dark, and #1941's contrast row would grade that 8-bit picture.
Why, after the rebase: this PR first fixed Native never being taken: the texture cache shared one image between RGBA8 and RGB10A2 (
format_class), sotex->formatnever said RGB10A2. Main's 0a28ff2 (#2038, moosky73's fix) has since given RGB10A2 a class of its own, so Native is taken on main now. This PR keeps telling Native by the format the game presents, which no longer depends on the cache's classes, and keeps the picture in 10 bits.What changed (the last commit)
do_present: Native is told byvk_format(t->d.format)alone, the format the view is made in. The image's format is no longer asked: it would take an SDR picture for PQ in an image first made as RGB10A2. One log line names the presented format, its image's format and the HDR path when any of them changes.present_image: Native always goes through the HDR pass, which reads the picture through that view.framegen_scale(pq): with the scaling filter, Sharpness or ultrawide's sides, the game's picture is scaled into RGB10A2 images instead of RGBA8. Its crop is copied as bits, not blitted. TheRX_VK_SHOT_FXshot passespresent_native_pqtoo.shaders/fg_scale.comp:FG_SCALE_PQmakes the destinationrgb10_a2and the dither a 10-bit step.fg_scale_pq_comp_spv.hcomes fromtest_fg_scale_sharpen.py --write(glslang 16.6.0); the RGBA8 header's code is unchanged.hdr_encode_comp_spv.his regenerated with glslang 16.6.0.test_fg_scale_sharpen.pycovers both headers.upscale_independent_test.cppruns the 10-bit path: pipeline, images, crop copy, the switch back, and a failed pipeline.test_hdr_encode.pychecks the format test (the presented format alone), the pass-through at 100%, thatpresent_imagenever blits Native's image, and the shot'spq.test_present_aspect.pycovers Native without the pass: its scaled picture, else black.present_aspect_vulkan.cpp's stubs now followpresent_image, so the real-Vulkan check compiles again. It passes here; on main it does not compile.Testing: RTX 5080, 3840x2160 HDR10, 800 nits peak, 300 nits paper white. With this PR's earlier version, before main's class fix, Native showed the game's own HDR picture instead of a flat, grey one. That ran in a build of main (8c92fe3) with this series and HDR contrast at 150% (screenshots), and earlier on v2.0.13 at 100%. Rebased on main d7a7306: it compiles with the build's clang.
tests/run_all.pywith hdr_encode, fg_scale, dlss_actor, dlss_checks, upscale_independent, framegen_resources, present_, upscaler_rows and menu: 28 pass, includingpresent_aspect_vulkanon the RTX 5080.present_semaphoresfails the same way on main, because this PC has no g++.In game after the review changes (a build of main d7a7306 with this series): the present log says
the game's PQ picture (Native)(format 64, its image's 64); at 100% contrast the game's own picture shows untouched, topping out at about 700 nits under the 800-nit peak row, and graded at 150% at 784 nits. Not triggered in game: the black fallback.Not tested: AMD/Intel, Linux.
Screenshots (Native, in a build of main d7a7306 with this series)
HDR10 captures, tone-mapped to SDR for this page (paper white to white, highlights rolled off up to 800 nits).
🤖 Generated with Claude Code