feat(hdr): PsychoV35 scene tone mapper for Native HDR (RenoDX) #2481
No reviewers
Labels
No labels
accessibility
autorelease: pending
autorelease: tagged
bug
dependencies
documentation
duplicate
enhancement
github_actions
good first issue
help wanted
invalid
question
testing wanted
wontfix
No milestone
No assignees
1 participant
Due date
No due date set.
Dependencies
No dependencies set
Reference
MagicBeanDev/Hookshot!2481
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/hdr-psychov35"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Native HDR currently keeps the game's own tone curve, which clips highlights at the same point the SDR picture does. This adds the scene tone mapper from ShortFuse's RenoDX mod for this game (PsychoV35, MIT) as an opt-in row, so highlights roll off into the display's peak instead.
What changes
b485da0). The game picks R11G11B10F or RGBA16F for its scene buffer inf_80672260;nx2/oot_patches.pymakes that pick followhos_scene_rgba16f, whichhost/nn_vi.csets from a new option,HOS_OPT_HDR_TONEMAP(47,scene_tonemap_on(): Native HDR with the row on PsychoV35). That gives the curve unclamped values to work with, as the RenoDX mod does. Native HDR on Vanilla keeps R11G11B10F, as on main, because the temporal upscalers only take that scene format.hdr_tone_mapper.ir_opt/scene_tonemap_pass.hfinds the game's Narkowicz fit in the translated IR and replaces target 0's colour with PsychoV35. It matches on the full structure: threeFPFma(x, 2.51, 0.03)whose input is the scene times one constant-buffer exposure, each channel identified by the luminance weight (0.2989, 0.5866, 0.1144) it meets downstream, and an output clamped under a peak word from the same bank. It reuses the game's own exposure and peak reads; anything that doesn't match is left unchanged, with the reason logged.ir_opt/psychov35.his RenoDX's PsychoV35 reduced to the path the mod takes at its defaults: midgray matched to the game's, the game's tone slope, hue-clip emulation at 50%, neutral grading, the exact BT.2020 fit. Both anchors are grey, so the cone response reduces to one logistic. It is written once over a small set of operations, so the pass emits it as IR and the tests run it on floats.Testing
tests/test_psychov35.py: the header against RenoDX'sscene_tonemap.slangi+psychov/test35.hlslpath ported line by line (log-domain response, residual normalization, matrices from their XYZ definitions): 3009 colours, worst relative difference 2.1e-6 in float32. It also checks the baked matrices against their definitions.tests/test_scene_tonemap_pass.py: builds Eden's IR library, runs the pass on a synthetic scene-mapping program with the channels' fits emitted out of order (and one where red is written before the other fits are made, checking every operand is defined before its use), interprets the resulting IR, and compares it with the header. Programs with no luminance weights, or with different exposures per channel, are left untouched (instruction count and outputs unchanged). It also checks the wiring: key, translation, pick reset, and row visibility.tests/run_all.pyon Linux: same results asmainon this machine (its 27 failures are missing headers and tools here, and fail onmaintoo).seed_variant_cleanup_test.cpp's stubs were updated forgraphics_key's new argument.vk: program 672e593076ac390a: scene tone map PsychoV35). Frame rate is unchanged (about 105 fps in both, same scene). Decoding the PQ screenshots to nits at a 200-nit paper white and 1000-nit peak:The mid-tones and exposure match the game's (midgray matching works), and the highlights move into the HDR range. No pixel exceeded the 1000-nit peak.
Screenshots
Native HDR, the PQ shots decoded to nits and shown through one fixed SDR mapping (an SDR page cannot show the HDR range itself); Vanilla on the left, PsychoV35 on the right.
Reach and what is not here
c46f8bcon a real HDR display (Windows 11, RTX 4070, 400-nit peak), including switching between Vanilla and PsychoV35 during play: it looked right, with no flicker or black screens. Their log is also what showed the upscalers falling back under Native, whichb485da0fixes for Vanilla.b485da0itself has not been run in game yet.Thanks, this is a careful piece of work. The pass matches on structure rather than a hash: three
FPFma(x, 2.51, 0.03), the luminance weights, a single cbuf exposure, and a peak from the same bank. Anything else is left alone, with the reason logged. The math header is shared between the IR emitter and the float test, which is a nice way to keep the two honest. With HDR off I see nothing that changes for players.scene_tonemap_on()is false, so the key isn't salted, andhos_scene_rgba16fstays 0, so the lifted pick is the original 0x1a. Apart from the translator stamp's one-time rebuild, that path is identical to main.Licence. RenoDX is MIT, which is fine inside a GPL-3.0 repo.
psychov35.hkeeps Carlos Lopez's copyright line and the MIT SPDX tag, and PROVENANCE.md credits both source files. Nit:tests/test_psychov35.pyis a line-by-line port too, so it should carry the same copyright/SPDX line. It would also be worth putting the MIT permission text in the tree, asthird_party/fidelityfx/LICENSE.mddoes for AMD's headers.The scene buffer format is the one thing I'd hold this for (inline on nn_vi.c).
hos_scene_rgba16ffollows Native HDR alone, not the new row. So every Native HDR player gets an RGBA16F scene buffer even with the tone mapper on Vanilla, and the "Nothing changes unless HDR is set to Native and the new row is set to PsychoV35" in the description isn't quite right. That matters because the temporal upscalers recognise the scene by its format:temporal_note_scene_draw(vk_temporal.cpp) andskin_check(vk_draw.cpp) require the scene target to beB10G11R11.temporal_upscalerequires that format for an unknown network's input and output.vk_fsr3_inputs.cpp,vk_fsr3.cpp) and the TAAU replacement (vk_draw.cpp) gate on it too.If the buffer f_80672260 makes is the scene target or the network's input, DLSS, FSR 4, FSR 3.1 and TAAU will quietly fall back to the game's network under Native HDR. The PR's in-game numbers don't say which upscaler was on. Could you run Native HDR with DLSS (or FSR 4) and check that the log still shows the upscaler taking the gameplay network? If it doesn't, there are two ways out: tie the format to the PsychoV35 row and say upscalers don't combine with it, or teach those gates RGBA16F. Either way, please state the Vanilla/Native change in the description. #2243 (DLSS for Native HDR, open) works in the same area, so the two will need to agree.
IR pass.
Select, and the pass runs beforeGuardFragmentOutputs, so anything left is guarded (the wiring test checks that order).Keys/caches. The salted
graphics_keykeeps the PsychoV35 variants in their own disk-cache entries, so toggling the row can't pick a stale pipeline. Clearing the thread-localpicks/prog_faston a change is right too. Settings use the ini keyhdr_tone_mapperwith no new numeric option id, so nothing collides with other PRs' ids.Tests. The three new tests are declared with
RUN_ALL_ARGS, need no game, and the scene-format fixture uses made-up lift text. CI is still queued (no runs have finished yet).Leaving this as a comment for now. Once the upscaler question is answered and the description matches, I'd be happy with it.
@ -0,0 +1,204 @@// SPDX-License-Identifier: GPL-3.0-or-laterMinor robustness point: this inserts PsychoV35 before the first write, but it uses
scene[2]/gainfrom the other channels' fits.MadeOfonly checks that each write comes from its own channel's fit, so a program that writes R before computing the B fit would get a use-before-def. The game's shape is safe because each write goes through the luminance of all three fits. Inserting before the last of the three writes (all in one block, already checked) would be safe for any order, since every write's operands dominate it.This follows
HOS_OPT_HDR_NATIVEonly, so Native HDR with the row on Vanilla also gets the RGBA16F scene buffer (see the main comment about the upscalers'B10G11R11gates). If the format is only needed for PsychoV35,HOS_OPT_HDR_NATIVE && hdr_tone_mapper == 1would keep Vanilla Native exactly as on main. That would need a host-visible option for the row. If it's meant for every Native HDR player (less banding), please say so in the description and confirm the upscalers still engage.Tested PR #2481 at commit
c46f8bc1aeon a real HDR display.Windows 11, i7-13700F, RTX 4070 12GB, Gigabyte G34WQC A at 3440×1440. Native HDR, 200-nit paper white, 400-nit peak.
PsychoV35 looked great during gameplay. Tested with DLSS, frame generation, and switching between Vanilla and PsychoV35 during play. Everything looked good in each case, with no observed flickering or black screens.
This resolves the Native HDR flickering I experienced on v2.0.14 in the scenarios I tested.
hookshot_log.txt
I merged main into the branch (
2df79c0) so this is mergeable again. The only conflict wasnx2/oot_patches.py: main moved the per-file patching intopatch_file()/report()(#2442), so the scene-format hook now sits inpatch_file()right after the free camera one, counting intocounts["scene format"], andreport()startsdone["scene format"]with the others. Nothing else of the PR changed (same 16 files, +1098/-7 against main). The game-free suite passes locally apart from two Tk-only checks that fail the same way on main here.Thanks @futondestroyer for testing on a real HDR screen. Your log actually answers the upscaler question from my last review, and not in the PR's favour yet:
NVIDIA DLAA: the game's upscale runs (unexpected output)(earlier(no scene camera this frame)), and the DLSS-G summaries all say0 made by DLSS-G ... 600 without new inputs (no DLSS/DLAA this frame).R16G16B16A16_SFLOAT) under Native, where the upscalers expect the B10G11R11 scene.So what looked right in play was the game's own upscale plus the renderer's own frame generation, not DLSS/DLAA or DLSS-G. That's the format gate I described on
nn_vi.c:hos_scene_rgba16ffollows Native alone, so this also affects Native players who keep the tone mapper on Vanilla.Still COMMENT for now. Either of the two ways out from before works: tie the RGBA16F pick to the PsychoV35 row and say upscalers don't combine with it yet, or teach the temporal/FSR/TAAU gates RGBA16F (and check with #2243). Then a run with DLSS on under Native should show a
feature ... madeline and DLSS-G frames. The licence nits and the small IR ordering point from the first review are still open.@hckrman101
b485da0addresses all three open points.Scene buffer format. I took your first option.
host/nn_vi.cnow setshos_scene_rgba16ffrom a new option,HOS_OPT_HDR_TONEMAP(47), which the renderer answers withscene_tonemap_on(). So the buffer is RGBA16F only with Native HDR and PsychoV35. Native HDR on Vanilla keeps R11G11B10F, as on main, and the B10G11R11 gates (DLSS/DLAA, DLSS-G, FSR 4, FSR 3.1, TAAU) see the scene they expect. With PsychoV35 on, those upscalers don't engage yet. The readme row and the description now say so. Teaching the gates RGBA16F is a follow-up that has to agree with #2243. I checked every open PR'svk_api.h: 47 isn't claimed (#2345 has 49/50, #2357 has 51, #2261 has 60).test_scene_format.pynow checks the host reads 47, the renderer answers it withscene_tonemap_on(), and the fixture is labelled for PsychoV35.IR ordering. The pass now emits PsychoV35 before the last of the three writes. Inserting there alone wasn't enough: the earlier two writes would then read values made after them, so they're moved down to just before the last write (same
Insts, spliced in the intrusive list).scene_tonemap_ir.cpphas a new shape where red is written before blue's and green's fits exist. It checks that every operand in the block is defined before its use, for that shape and the game's. The old first-write insertion fails it on the new shape, and the new code passes both.Licence. RenoDX's MIT text is in
ir_opt/LICENSE-renodx.md, andPROVENANCE.mdpoints to it.tests/test_psychov35.pycarries the MIT SPDX tag and the copyright line. I also correctedpsychov35.h's line to match the upstream licence: "Copyright (c) 2025 Carlos Lopez Jr." (it said 2026).test_scene_format,test_scene_tonemap_passandtest_psychov35pass.run_allgives 384 passed. The 1 failure istest_lift_cache, which needs numpy, missing on this machine.vk_replay.cppalso builds without errors. I haven't runb485da0in game yet. @futondestroyer, if you have time, a Native HDR run with the row on Vanilla and DLSS on should now show thefeature ... madeline and DLSS-G frames.Done in
b485da0: it'srx_option(HOS_OPT_HDR_TONEMAP, 0)now (option 47, answered byscene_tonemap_on()), so Native HDR on Vanilla keeps R11G11B10F.@ -0,0 +1,204 @@// SPDX-License-Identifier: GPL-3.0-or-laterDone in
b485da0: PsychoV35 is emitted before the last of the three writes, and the other two move down to it. The new red-first shape inscene_tonemap_ir.cppchecks every operand is defined before its use.Follow-up test on
b485da0, same RTX 4070 / Windows 11 system. During one session I cycled through HDR Off, Expanded, and Native with both Vanilla and PsychoV35. DLAA and FG were set to On throughout.The log shows DLAA recreating its feature as Native HDR turns off/on, correctly reporting HDR linear-light input when Native resumes. It records a full 600-frame interval with DLAA processing every frame. Later
unexpected outputfallbacks are followed byNVIDIA DLSS draws the upscale again, confirming re-engagement during the cycle.The tone-mapper changes aren't explicitly marked in the log, so I can't associate every fallback/resumption with an exact switch.
DLSS-G remains unsuccessful. Its feature initializes, but evaluation repeatedly returns
0xbad00005, and all summaries report zero frames made by DLSS-G.hookshot_log.txt
Thanks @futondestroyer, that's what
b485da0was meant to do: DLAA makes its HDR feature each time Native turns on and comes back after the fallbacks.The DLSS-G
0xbad00005refusals aren't from this PR: they happen with HDR set to Off too (the summary at HDR 0 has 600 of 600 refused), where #2481 changes nothing. Your build predates #2118 (DLSS-G through NGX, merged today), so it'd be worth retrying on current main and, if it still refuses, opening an issue for @pmwork-personal with this log.github-sync referenced this pull request2026-11-12 06:22:54 +00:00