diff --git a/README.md b/README.md index 4be87aa3..ee8f3f57 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md). the game paces movies itself, and takes the limit back while it is minimised and whenever RivaTuner is not seen holding it; where frame generation or low latency keeps it out, the game says when a limit set in RivaTuner itself holds the game as well -- HDR on HDR displays, two kinds: the game's scene tone mapped to the display's peak, matched to the game's mid grey and tone, with true blacks, or the game's own native HDR; peak, game and HUD brightness in the menu +- HDR on HDR displays, two kinds: the game's scene tone mapped to the display's peak, matched to the game's mid grey and tone, with true blacks, or the game's own native HDR; peak, game and HUD brightness in the menu, and for the tone mapped picture the tone mapper, gamma correction, exposure, highlights, shadows, contrast and saturation - The game's known shaders prepared when the game is built, and the pipelines met in play built again in the background at each start, so play rarely stops to compile them; a shader or pipeline met for the first time can still hold a frame, and a draw that must wait for its pipeline has it built on worker threads, never on the game's diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index ed3ecc46..0b1a845f 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -37,8 +37,8 @@ Code copied or adapted from another project is listed here with its source, the | libvpx 1.17.0 (MSYS2 `mingw-w64-ucrt-x86_64-libvpx`), static link (`libvpx.a`), no DLL shipped; the release notices must carry its BSD licence text and the WebM Project's `PATENTS` grant | https://chromium.googlesource.com/webm/libvpx, Copyright (c) 2010, The WebM Project authors | BSD-3-Clause | `runtime/ov_movie.c` (VP9 decoding of the `Event/Movie` WebM files) | | MacNativeGames NX2navi resolution patches: output, swapchain and scene size sites and instruction words for 1.2.1 and 1.4.3, the data-page layout, the HeapDef heap names and sizes | https://github.com/ly05010419/MacNativeGames @ `aaf6ba1f494fabaa209d049e528d6f923dfe822a` (`runtime/game_patches.c`, `game_patches_v143.c`, `game_version.h`, `scripts/make_output_res_overlay.py`), Copyright (c) 2026 MacNativeGames contributors | MIT | `recomp/patches-v121.txt`, `recomp/patches-v143.txt`, `runtime/resolution.c` | | AMD FidelityFX Super Resolution 1.0 (`ffx_a.h`, `ffx_fsr1.h`, unmodified; licence text in `runtime/shaders/ffx/LICENSE.txt`) | https://github.com/GPUOpen-Effects/FidelityFX-FSR @ `a21ffb8f6c13233ba336352bdff293894c706575`, Copyright (c) 2021 Advanced Micro Devices, Inc. | MIT | `runtime/shaders/ffx/`, `runtime/shaders/fsr_easu.comp`, `runtime/shaders/fsr_rcas.comp`, `runtime/shaders/dlaa_sharpen.comp` (compiled into `runtime/fsr_easu_spv.h`, `runtime/fsr_rcas_spv.h`, `runtime/dlaa_sharpen_spv.h`) | -| RenoDX shader library: the swap chain pass's gamma correction, gamut compression, peak clamp and PQ encode, and the gamma correction of the tone mapper's peak, ported from HLSL to GLSL | https://github.com/clshortfuse/renodx @ `bc0ead39917634e95c534f7e8cfb59ab82880747` (`src/shaders/draw.hlsl`, `tonemap.hlsl`, `colorcorrect.hlsl`, `color/pq.hlsl`, `color/srgb.hlsl`, `color/gamma.hlsl`), Copyright (c) 2025 Carlos Lopez Jr. | MIT | `runtime/shaders/hdr_tonemap.frag` and `runtime/shaders/hdr_encode.comp` (compiled into `runtime/hdr_tonemap_spv.h`, `runtime/hdr_encode_spv.h`) | -| RenoDX scene mapper for this game and its PsychoV35 tone mapper: the finite cone response in LMS with its mean, deficit and midpoint chroma, the exact fit to the target gamut, the mid grey anchor and tone slope of the game's curve, and the hue clip emulation, ported from HLSL and Slang to GLSL (the path of its default settings; without eye adaptation) | https://github.com/clshortfuse/renodx @ `59bd6f606a5d105bc6de52627332703c840443be` (`src/shaders/tonemap/psychov/test35.hlsl`, `src/games/zelda-oot-oz/scene_tonemap.slangi`, `shared.h`, `addon.cpp`), Copyright (C) 2026 Carlos Lopez | MIT | `runtime/shaders/hdr_tonemap.frag` (compiled into `runtime/hdr_tonemap_spv.h`) | +| RenoDX shader library: the RenoDRT tone mapper (Daniele curve, OkLab hue and chrominance, highlights, shadows, contrast, flare, saturation, highlight saturation, blowout), the luminance upgrade of an SDR grade, its colour matrices, the swap chain pass's gamma correction, gamut compression, peak clamp and PQ encode, and the gamma correction of the tone mapper's peak, ported from HLSL to GLSL | https://github.com/clshortfuse/renodx @ `bc0ead39917634e95c534f7e8cfb59ab82880747` (`src/shaders/tonemap.hlsl`, `tonemap/reno_drt.hlsl`, `tonemap/daniele.hlsl`, `draw.hlsl`, `colorgrade.hlsl`, `colorcorrect.hlsl`, `color/oklab.hlsl`, `color/rgb.hlsl`, `color/pq.hlsl`, `color/srgb.hlsl`, `color/gamma.hlsl`; setting names, ranges and defaults of `src/templates/settings.hpp`), Copyright (c) 2025 Carlos Lopez Jr. | MIT | `runtime/shaders/hdr_tonemap.frag` and `runtime/shaders/hdr_encode.comp` (compiled into `runtime/hdr_tonemap_spv.h`, `runtime/hdr_encode_spv.h`); the HDR settings of `runtime/cfg.c`, `runtime/vk_hdr.c` and `runtime/ui/menu.cpp` | +| RenoDX scene mapper for this game and its PsychoV35 tone mapper: the finite cone response in LMS with its mean, deficit and midpoint chroma, the highlights, shadows and purity grading, the exact fit to the target gamut, the mid grey anchor and tone slope of the game's curve, and the hue clip emulation, ported from HLSL and Slang to GLSL (the paths its scene mapper takes; without eye adaptation) | https://github.com/clshortfuse/renodx @ `59bd6f606a5d105bc6de52627332703c840443be` (`src/shaders/tonemap/psychov/test35.hlsl`, `src/games/zelda-oot-oz/scene_tonemap.slangi`, `shared.h`; setting names, ranges and defaults of its `addon.cpp`), Copyright (C) 2026 Carlos Lopez | MIT | `runtime/shaders/hdr_tonemap.frag` (compiled into `runtime/hdr_tonemap_spv.h`); the HDR settings of `runtime/cfg.c`, `runtime/vk_hdr.c` and `runtime/ui/menu.cpp` | | AMD Display Library (ADL) declarations: `AdapterInfo`, `ADLSingleSensorData`, `ADLPMLogDataOutput`, the `ADL_PMLOG_*` sensor numbers and the `ADL2_*` function signatures | https://github.com/GPUOpen-LibrariesAndSDKs/display-library @ `84c881cb2136f04287eb3af12de00a9e2867aa9e` (`include/adl_structures.h`, `include/adl_defines.h`, `include/adl_sdk.h`), Copyright (c) 2016 - 2022 Advanced Micro Devices, Inc. | MIT | `runtime/perfmon.c` | | Zstandard 1.5.7 (MSYS2 `mingw-w64-ucrt-x86_64-zstd`), static link | https://github.com/facebook/zstd, Copyright (c) Meta Platforms, Inc. and affiliates | BSD-3-Clause | `runtime/resolution.c` (the HeapDef table), `runtime/ov_zstd.c` (the game's zstd frames decoded on the host) | | musl `fma.c`, `fmaf.c` and `scalbn.c`, adapted into one file for the x86-64-v2 build | https://github.com/kraj/musl @ `5a56ad13e1e0790c06122978aad2a14d6b9efc1a` (mirror of https://git.musl-libc.org/cgit/musl), Copyright (c) 2005-2020 Rich Felker and contributors | MIT | `runtime/fma.c` (the correctly rounded fma/fmaf the FP lowering needs on CPUs without FMA3) | diff --git a/docs/README-BuildKit.md b/docs/README-BuildKit.md index 734f6c2c..aa995a3a 100644 --- a/docs/README-BuildKit.md +++ b/docs/README-BuildKit.md @@ -179,7 +179,7 @@ beside the game. FSR 3.1 on any card where `amd_fidelityfx_vk.dll` loads, FSR 4 on the Radeon cards AMD's FSR 4 libraries list it for), Frame generation (NVIDIA DLSS on RTX 40 and 50 cards, with a DLSS multiplier of 2x, or 3x and 4x on RTX 50 cards; FSR 3.1 on any card where it loads; it needs DLSS or FSR 3.1 - upscaling on; the frame limit counts the frames the game draws and the generated ones come on top), HDR (the game's picture tone mapped to your display, or Native, the game's own HDR, with peak, game and HUD brightness; turn HDR on in Windows first) and the + upscaling on; the frame limit counts the frames the game draws and the generated ones come on top), HDR (the game's picture tone mapped to your display, or Native, the game's own HDR, with peak, game and HUD brightness, and under the tone mapped picture its tone mapper and grading rows; turn HDR on in Windows first) and the resolution. A setting that takes effect only when the game starts again says "after restart" once you change it, until you set it back, and the Restart button at the bottom then counts the settings waiting for it. - **Controls** in the menu has Button prompts: the game's own, or Xbox, PlayStation, Steam Deck or keyboard and mouse ones when diff --git a/docs/architecture.md b/docs/architecture.md index 0a26c142..5dce8aaa 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -251,11 +251,14 @@ keeps per-core data without locks and relies on that, so the host models it (`ru target and copies that into the 8-bit sRGB window texture it presents. With `NX2NAVI_HDR=1` the tonemap shader gets the function of `shaders/hdr_tonemap.frag` spliced in where its SDR curve clamps each channel, so the float picture holds the HDR picture relative to the HUD's white (the function gets the values the curve takes, and the scene - before the game's exposure gain with that gain, which it anchors mid grey on); the game's FSR 1 pass loses its output - clamp; the present reads the float picture in place of the window texture and `shaders/hdr_encode.comp` writes PQ - for an HDR10 swapchain, showing the picture as a display of gamma 2.2 would (the game is graded for one: decoded as - its sRGB encode, the darkest tones would sit far above black). The look is fixed; the settings are the peak, game and - HUD whites. + before the game's exposure gain with that gain, which its default tone mapper anchors mid grey on); the game's FSR 1 + pass loses its output clamp; the present reads the float picture in place of the window texture and + `shaders/hdr_encode.comp` writes PQ for an HDR10 swapchain, showing the picture as a display of gamma 2.2 would (the + game is graded for one: decoded as its sRGB encode, the darkest tones would sit far above black). The settings are + the peak, game and HUD whites, the tone mapper (`NX2NAVI_HDR_TONEMAP`: the observer-response mapper, the game's own + curve or the luminance mapper), what each tone mapper takes, the grading sliders and the gamma correction + (`NX2NAVI_HDR_GAMMA`, with every tone mapper); they travel in five vec4 the support buffer gains, and at their + defaults the shader leaves every grading step out. `NX2NAVI_HDR=native` reports an HDR10 display to the game, which then renders and presents its own PQ picture. The menu switches between off and `1` during a run: the splice, the HDR extensions and the settings in the support buffer are there from the start whatever is set (`vk_hdr_armed`), the spliced tonemap returns the game's SDR result diff --git a/docs/releases/v0.10.2.md b/docs/releases/v0.10.2.md index 20342bab..c69fc2c3 100644 --- a/docs/releases/v0.10.2.md +++ b/docs/releases/v0.10.2.md @@ -35,6 +35,13 @@ cards). uploaded and decoded again each time, one of them 29 times in five seconds. On the same run that was 49,660 second uploads in three minutes, now 214, and the graphics card's upload work fell from 0.41 to 0.14 ms a frame (`tex_same_big = 0` brings the old behaviour back for a comparison). +- **The tone mapped HDR picture has its settings back** in the Screen tab, under *HDR output* on its tone mapped + choice (not *Native*) and the three brightness rows: *Tone mapper* and *Gamma correction*, then the rows of the tone + mapper chosen, with *Exposure*, *Highlights*, *Shadows*, *Contrast* and *Saturation* among them. Each change shows + at once. Left alone they give the picture HDR had from 0.9.0 to 0.10.1, true blacks included. *Gamma correction* now + counts with every tone mapper; its *Off* shows the dark tones lighter. Lines for these settings that your + nx2navi.ini still holds from before 0.9.0 (`hdr_tonemap`, `hdr_exposure`, `hdr_gamma` and the others + `nx2navi.ini.example` lists) count again: remove them for the default look. ## Removed diff --git a/nx2navi.ini.example b/nx2navi.ini.example index 1dc1fb5e..468ef455 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -836,11 +836,10 @@ # low_latency = 0 # HDR output, two kinds (both need HDR on in Windows, "Use HDR", or a Wayland compositor with colour management; -# without it the picture stays SDR, the game's own): hdr = 1 sends the game's scene tone mapped to your display, one -# fixed look, and applies at once; hdr = native shows the game's own HDR mode, which the game takes up at the next -# start. hdr_peak_nits is your display's peak, hdr_game_nits the picture's white (paper white, 203 is the HDR -# reference) and hdr_ui_nits the HUD's and the menu's. The settings of the look of earlier versions -# (hdr_tonemap, hdr_exposure, hdr_gamma and the rest) are read by nothing now and do no harm. +# without it the picture stays SDR, the game's own): hdr = 1 sends the game's scene tone mapped to your display and +# applies at once; hdr = native shows the game's own HDR mode, which the game takes up at the next start. +# hdr_peak_nits is your display's peak, hdr_game_nits the picture's white (paper white, 203 is the HDR reference) and +# hdr_ui_nits the HUD's and the menu's. # hdr = 0 # hdr_live = 0 leaves the HDR path out of a run that starts with hdr = 0, as before hdr followed the menu: switching # HDR on then takes a restart. Only worth setting if the picture differs with it at 1. @@ -848,6 +847,40 @@ # hdr_peak_nits = 1000 # hdr_game_nits = 203 # hdr_ui_nits = 203 +# The settings from here to the end of this section shape the picture of hdr = 1 and apply at once; hdr = native does +# not take them. Left as they are, they give the one look hdr = 1 had from 0.9.0 to 0.10.1, which read none of them: a +# value your file still holds from an older version counts again. +# The tone mapper: vanilla is the game's own curve at SDR brightness; psychov35 sends the scene through a model of the +# eye's response, matched to the game's mid grey and tone, up to the peak; renodrt carries the game's SDR look up to +# the peak by brightness. +# hdr_tonemap = psychov35 +# Gamma correction, with every tone mapper: 2.2 or 2.4 shows the picture as a display of that power would, as the game +# was made to be seen; off keeps the sRGB curve, which leaves the darkest tones well above black. +# hdr_gamma = 2.2 +# With psychov35: where the scene's mid grey lands (vanilla: where the game's curve puts it; render: at the scene's +# value); whether the mid tones get the contrast of the game's curve; the tone curve's steepness through mid grey from +# 0 to 100 (50 unchanged); and in percent how much bright colours turn as where the game's SDR picture clips (fire +# towards yellow and white). +# hdr_midgray = vanilla +# hdr_match_slope = 1 +# hdr_tone_slope = 50 +# hdr_hue_clip = 50 +# With psychov35 and renodrt: exposure in percent (100 unchanged); highlights, shadows, contrast and saturation from 0 +# to 100 (50 unchanged). +# hdr_exposure = 100 +# hdr_highlights = 50 +# hdr_shadows = 50 +# hdr_contrast = 50 +# hdr_saturation = 50 +# With renodrt: highlight saturation from 0 to 100 (50 unchanged); blowout (bright parts towards white) and flare +# (lifted blacks) from 0 to 100 (0 none); hue correction in percent (how much bright colours keep their hue); grade, +# the percentage of the game's own colour grading the picture keeps. +# hdr_highlight_sat = 50 +# hdr_blowout = 0 +# hdr_flare = 0 +# hdr_hue = 100 +# hdr_grade = 100 + [language] # Language: a Windows locale such as de-DE, or a Switch code (en-US, ja, zh-Hant, ...). Default: the Windows locale. # locale = en-US diff --git a/runtime/cfg.c b/runtime/cfg.c index f75f19fa..2f0df555 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -56,8 +56,6 @@ static const char *const known[] = { "FFX_PIPE_CACHE", "FLIGHT_SPIKE_MS", "FREEZE_QUIET_MS", "FREEZE_LOAD_HOLD_MS", "STUTTER_MAP", "PAR_JOBS", "PIPE_RELEASE", "FLIGHT_THUMBS", "FLIGHT_EVERY", "FLIGHT_FLASH", "FLIGHT_FLASH_DIFF", "FLIGHT_FLASH_CELLS", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOG_CULL", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FG_MULT", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREEZE_ASYNC", "FREEZE_BUDGET_US", "FREE_CORES", "FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "GPA_TWIN_MAX_KB", "MIRROR_CAPTURE_KB", "WB_SKIP_MB", "GPL", "GPL_DRAW_BUILD", "GPL_FEEDBACK", "GPL_MESH", "PIPE_AHEAD", "PIPE_FAST_WORKERS", "PIPE_FRAME_WAIT_MS", "PIPE_LINK_DRAW_MS", "PIPE_SKIP_STEADY", "DRAW_WRITERS_WAIT", "PROG_LEAD", "GPU", "GPU_INDEX", "GPU_PASSES", "GPU_NAME", "HDR", "HDR_GAME_NITS", "HDR_LIVE", "HEAP_LAZY", "HDR_PEAK_NITS", "HDR_UI_NITS", - // retired: the HDR look's settings of earlier versions, now fixed (vk_hdr.c, shaders/hdr_tonemap.frag); read by - // nothing, listed so that a file which still sets them logs no unknown key "HDR_BLOWOUT", "HDR_CONTRAST", "HDR_EXPOSURE", "HDR_FLARE", "HDR_GAMMA", "HDR_GRADE", "HDR_HIGHLIGHT_SAT", "HDR_HIGHLIGHTS", "HDR_HUE", "HDR_HUE_CLIP", "HDR_MATCH_SLOPE", "HDR_MIDGRAY", "HDR_SATURATION", "HDR_SHADOWS", "HDR_TONEMAP", "HDR_TONE_SLOPE", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD", @@ -132,7 +130,11 @@ static const struct { const char *key, *section; } sections[] = { {"COLOR_PRESET", "display"}, {"COLOR_BRIGHTNESS", "display"}, {"COLOR_CONTRAST", "display"}, {"COLOR_SATURATION", "display"}, {"COLOR_GAMMA", "display"}, {"HDR", "display"}, {"HDR_LIVE", "display"}, {"HDR_PEAK_NITS", "display"}, {"HDR_GAME_NITS", "display"}, - {"HDR_UI_NITS", "display"}, {"SHADOW_RES", "display"}, + {"HDR_UI_NITS", "display"}, {"HDR_GAMMA", "display"}, {"HDR_TONEMAP", "display"}, {"HDR_EXPOSURE", "display"}, + {"HDR_HIGHLIGHTS", "display"}, {"HDR_SHADOWS", "display"}, {"HDR_CONTRAST", "display"}, {"HDR_SATURATION", "display"}, + {"HDR_HIGHLIGHT_SAT", "display"}, {"HDR_BLOWOUT", "display"}, {"HDR_FLARE", "display"}, {"HDR_HUE", "display"}, + {"HDR_GRADE", "display"}, {"HDR_MIDGRAY", "display"}, {"HDR_MATCH_SLOPE", "display"}, {"HDR_TONE_SLOPE", "display"}, + {"HDR_HUE_CLIP", "display"}, {"SHADOW_RES", "display"}, {"DLAA", "display"}, {"FSR4", "display"}, {"FSR4_FG", "display"}, {"DLSS", "display"}, {"DLSS_PRESET", "display"}, {"DLSS_IDLE_FREE", "display"}, {"DLSS_RANGE", "display"}, {"DLSS_PREBLOOM", "display"}, {"DLSS_CHAR_MASK", "display"}, {"FXAA", "display"}, {"AA", "display"}, {"UPSCALER_IDLE_WAIT", "display"}, {"FRAMEGEN", "display"}, {"FRAMEGEN_MULTIPLIER", "display"}, {"FG_MULT", "display"}, {"SKIN_MOTION", "display"}, @@ -252,13 +254,23 @@ static const CfgMeta metas[] = { {"COLOR_PRESET", "default", "default|vivid|clarity|custom", LIVE}, {"COLOR_BRIGHTNESS", "0", "-30..30", LIVE}, {"COLOR_CONTRAST", "100", "70..150", LIVE}, {"COLOR_SATURATION", "100", "50..200", LIVE}, {"COLOR_GAMMA", "1", "0.8..1.2", LIVE}, - // HDR output (vk_hdr.c): on and off apply at once (the HDR tonemap is spliced into the game's as shaders load - // whatever is set, vk_hdr_armed); native is the game's own and is asked for once, at start - // (vk_hdr_needs_restart). HDR_LIVE = 0 leaves the HDR path out of a run that starts with HDR off. The look is - // fixed: only the three whites are settings. + // HDR output (vk_hdr.c): on, off, the tone mapper and its grading apply at once (the HDR tonemap is spliced into + // the game's as shaders load whatever is set, vk_hdr_armed); native is the game's own and is asked for once, at + // start (vk_hdr_needs_restart). HDR_LIVE = 0 leaves the HDR path out of a run that starts with HDR off. + // The grading sliders: 50 leaves the picture as it is, exposure in percent. HDR_GAMMA is the display the present + // shows the picture as, with every tone mapper; off shows the sRGB code as it decodes, the dark tones lighter. {"HDR", "0", "0|1|native", LIVE}, {"HDR_LIVE", "1", "0|1", RESTART}, {"HDR_PEAK_NITS", "1000", "400..10000", LIVE}, {"HDR_GAME_NITS", "203", "48..500", LIVE}, - {"HDR_UI_NITS", "203", "48..500", LIVE}, + {"HDR_UI_NITS", "203", "48..500", LIVE}, {"HDR_GAMMA", "2.2", "off|2.2|2.4", LIVE}, + {"HDR_TONEMAP", "psychov35", "vanilla|psychov35|renodrt", LIVE}, {"HDR_EXPOSURE", "100", "0..200", LIVE}, + // the observer-response mapper's: where scene grey lands, its slope there matched to the game's curve, that slope + // (50 unchanged) and how much of the game's hue bend at its clamp bright colours get + {"HDR_MIDGRAY", "vanilla", "render|vanilla", LIVE}, {"HDR_MATCH_SLOPE", "1", "0|1", LIVE}, + {"HDR_TONE_SLOPE", "50", "0..100", LIVE}, {"HDR_HUE_CLIP", "50", "0..100", LIVE}, + {"HDR_HIGHLIGHTS", "50", "0..100", LIVE}, {"HDR_SHADOWS", "50", "0..100", LIVE}, {"HDR_CONTRAST", "50", "0..100", LIVE}, + {"HDR_SATURATION", "50", "0..100", LIVE}, {"HDR_HIGHLIGHT_SAT", "50", "0..100", LIVE}, + {"HDR_BLOWOUT", "0", "0..100", LIVE}, {"HDR_FLARE", "0", "0..100", LIVE}, {"HDR_HUE", "100", "0..100", LIVE}, + {"HDR_GRADE", "100", "0..100", LIVE}, {"DEVICE_PROFILE", "auto", "auto|desktop|deck", RESTART}, {"SHADOW_RES", "game", "game|256|512|768|1024|1536|2048", RESTART}, {"SHADOW_SCALE", "1", "1..4", RESTART}, {"SHADOW_COMPARE", "1", "0|1", RESTART}, // vk_texture.c diff --git a/runtime/shaders/hdr_tonemap.frag b/runtime/shaders/hdr_tonemap.frag index a0029d5e..dad241e8 100644 --- a/runtime/shaders/hdr_tonemap.frag +++ b/runtime/shaders/hdr_tonemap.frag @@ -26,36 +26,215 @@ // main only exists so the compiler keeps the functions; vk_hdr.c drops it. build_shaders.py compiles this file without // optimisation ("keep functions" above), as inlining would leave no function to splice. // -// The HDR picture (scene_tonemap, psycho35): the scene light goes through a finite cone response in LMS, anchored so -// that scene grey 0.18 lands where the game's curve puts it, and rising through it as steeply as the game's curve -// does at its steepest; the response saturates at the display's peak. Chroma is the response's residual around its -// mean, scaled back just enough to fit Rec. 709 (the game's scene target is r11g11b10f, which holds no negative -// component, so a wider fit would be clipped channel by channel). Bright colours get half of the hue bend the game's -// clamp gives them. It takes the scene without local exposure. The present then shows the picture as a 2.2 display -// would (hdr_encode.comp); the peak given to the response is lowered by that correction's inverse, so that the -// brightest light still lands on the display's peak. -// Settings (vk_hdr.c vk_hdr_support, support buffer bytes 1440..1455): p0 = peak nits, game nits (white of the -// picture), HUD nits, and -1 for the game's own SDR picture (as when the swapchain is not HDR10) or 1 for the HDR one. +// Three tone mappers (p0.w): +// - 9, the observer-response mapper (scene_tonemap, psycho35), the default: the scene light goes through a finite cone +// response in LMS, anchored so that scene grey 0.18 lands where the game's curve puts it (or at 0.18 times the gain) +// and, with the tone slope matched, rising through it as steeply as the game's curve does at its steepest; the +// response saturates at the display's peak. Chroma is the response's residual around its mean, scaled back just +// enough to fit Rec. 709 (the game's scene target is r11g11b10f, which holds no negative component, so a wider fit +// would be clipped channel by channel). Bright colours get a share of the hue bend the game's clamp gives them. It +// takes the scene without local exposure. +// - 0, the game's curve: its SDR result at the picture's white. +// - 3, the luminance mapper (drt): the game's SDR result is the colour grade, upgraded to the HDR range by luminance +// against a neutral SDR curve of the same input, then a luminance curve towards the peak with exposure, +// highlights, shadows, contrast and flare, and in OkLab the hue of the input, saturation, highlight saturation and +// blowout. +// The present then shows the picture as a display of gamma 2.2 or 2.4 would, unless that is switched off +// (hdr_encode.comp); the peak given to a tone mapper is the display's taken through that correction's inverse, so that +// the brightest light still lands on the display's peak. +// Settings (vk_hdr.c vk_hdr_support, support buffer bytes 1440..1519), their defaults in brackets. A grading value +// of 1 is compared for and its step left out, so the defaults give the observer-response mapper's plain result and +// not that result times factors that only round to 1: +// p0: peak nits, game nits (white of the picture), HUD nits, tone mapper (-1: the game's own SDR picture, as when the +// swapchain is not HDR10; else as above) [9] +// p1: exposure [1], highlights [1], shadows [1], contrast [1] +// p2: saturation [1], highlight saturation [1], blowout [0], flare [0] +// p3: hue correction [1], grade strength [1], gamma correction (0 off, 1: 2.2, 2: 2.4; applied on the present) [1], +// tone slope [1] +// p4: hue clip emulation (0..1) [0.5], mid grey (0: 0.18 times the gain, 1: the game's curve at 0.18) [1], match tone +// slope (0 or 1) [1], unused layout(location = 0) in vec4 a0; layout(location = 1) in vec4 a1; layout(location = 2) in vec4 a2; +layout(location = 3) in vec4 a3; +layout(location = 4) in vec4 a4; +layout(location = 5) in vec4 a5; +layout(location = 6) in vec4 a6; layout(location = 0) out vec4 o; // Matrices are written row by row; mulm(m, v) is the row-vector product of that layout. vec3 mulm(mat3 m, vec3 v) { return v * m; } +const mat3 BT709_TO_AP1 = mat3(0.6130974024, 0.3395231462, 0.0473794514, + 0.0701937225, 0.9163538791, 0.0134523985, + 0.0206155929, 0.1095697729, 0.8698146342); +const mat3 AP1_TO_BT709 = mat3(1.7050509927, -0.6217921207, -0.0832588720, + -0.1302564175, 1.1408047366, -0.0105483191, + -0.0240033568, -0.1289689761, 1.1529723329); +const mat3 BT709_TO_OKLABLMS = mat3(0.4122214708, 0.5363325363, 0.0514459929, + 0.2119034982, 0.6806995451, 0.1073969566, + 0.0883024619, 0.2817188376, 0.6299787005); +const mat3 OKLABLMS_TO_OKLAB = mat3(0.2104542553, 0.7936177850, -0.0040720468, + 1.9779984951, -2.4285922050, 0.4505937099, + 0.0259040371, 0.7827717662, -0.8086757660); +const mat3 OKLAB_TO_OKLABLMS = mat3(1.0, 0.3963377774, 0.2158037573, + 1.0, -0.1055613458, -0.0638541728, + 1.0, -0.0894841775, -1.2914855480); +const mat3 OKLABLMS_TO_BT709 = mat3(4.0767416621, -3.3077115913, 0.2309699292, + -1.2684380046, 2.6097574011, -0.3413193965, + -0.0041960863, -0.7034186147, 1.7076147010); + float y709(vec3 c) { return dot(c, vec3(0.2126390059, 0.7151686788, 0.0721923154)); } +float divide_safe(float a, float b, float fallback) { return b == 0.0 ? fallback : a / b; } + vec3 copy_sign(vec3 mag, vec3 s) { vec3 a = abs(mag); return mix(a, -a, lessThan(s, vec3(0.0))); } +vec3 cbrt(vec3 x) { return copy_sign(pow(abs(x), vec3(1.0 / 3.0)), x); } + +vec3 oklab_from_bt709(vec3 c) { return mulm(OKLABLMS_TO_OKLAB, cbrt(mulm(BT709_TO_OKLABLMS, c))); } + +vec3 bt709_from_oklab(vec3 lab) { + vec3 lms = mulm(OKLAB_TO_OKLABLMS, lab); + return mulm(OKLABLMS_TO_BT709, lms * lms * lms); +} + +vec3 clamp_ap1(vec3 c) { return mulm(AP1_TO_BT709, max(mulm(BT709_TO_AP1, c), vec3(0.0))); } + +// The hue (OkLab a, b direction) of correct with the chrominance of incorrect. +vec3 hue_oklab(vec3 incorrect, vec3 correct) { + vec3 il = oklab_from_bt709(incorrect), cl = oklab_from_bt709(correct); + float pre = length(il.yz); + vec2 ab = cl.yz; + ab *= divide_safe(pre, length(ab), 1.0); + il.yz = ab; + return clamp_ap1(bt709_from_oklab(il)); +} + float srgb_decode(float c) { return c <= 0.04045 ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4); } // A value of a gamma-g picture as the sRGB value of the same code (the peak's correction). float gamma_to_srgb(float c, float g) { return srgb_decode(pow(c, 1.0 / g)); } +// The display's peak (in picture whites) as a tone mapper is to reach it: what the present's gamma correction (0 none, +// 1: 2.2, 2: 2.4) brings to the peak. +float peak_before_gamma(float peak, float gamma) { + return gamma == 0.0 ? peak : gamma_to_srgb(peak, gamma == 1.0 ? 2.2 : 2.4); +} + +float smoother(float t) { return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); } // both mappers' grading blends with it + +// ---- the luminance mapper + +float highlights(float x, float h, float mid) { + if (h == 1.0) return x; + float t = 0.0; + if (x > mid) t = clamp(log2(x / mid) / log2(1.0 / mid), 0.0, 1.0); + t = smoother(t); + if (h > 1.0) return mix(x, mid * pow(x / mid, h), t); + float b = mid * pow(x / mid, 2.0 - h); + return divide_safe(x * x, mix(x, b, t), x); +} + +float shadows(float x, float s, float mid) { + if (s == 1.0) return x; + float ratio = max(divide_safe(x, mid, 0.0), 0.0); + float base_term = x * mid; + float base_scale = divide_safe(base_term, ratio, 0.0); + float shadow_floor = mid / 16.0; + float t = 1.0; + if (x > shadow_floor) t = clamp(log2(x / mid) / log2(shadow_floor / mid), 0.0, 1.0); + t = smoother(t); + if (s > 1.0) { + float raised = x * (1.0 + divide_safe(base_term, pow(ratio, s), 0.0)); + return x + (raised - x * (1.0 + base_scale)) * t; + } + float lowered = x * (1.0 - divide_safe(base_term, pow(ratio, 2.0 - s), 0.0)); + return x + (lowered - x * (1.0 - base_scale)) * t; +} + +// The luminance curve: n peak nits, g contrast, c_d mid grey in nits, t_1 flare; 100 nits reference, scene grey 0.18. +float curve(float x, float n, float g, float c_d, float t_1) { + const float n_r = 100.0, c = 0.18, w_g = 0.0, r_hit_min = 128.0, r_hit_max = 256.0; + float m_0 = n / n_r; + float m_1 = 0.5 * (m_0 + sqrt(m_0 * (m_0 + 4.0 * t_1))); + float r_hit = r_hit_min + (r_hit_max - r_hit_min) * (log(m_0) / log(10000.0 / 100.0)); + float u = pow((r_hit / m_1) / ((r_hit / m_1) + 1.0), g); + float m = m_1 / u; + float w_i = log(n / 100.0) / log(2.0); + float c_t = (c_d / n_r) * (1.0 + w_i * w_g); + float g_ip = 0.5 * (c_t + sqrt(c_t * (c_t + 4.0 * t_1))); + float g_ipp2 = -m_1 * pow(g_ip / m, 1.0 / g) / (pow(g_ip / m, 1.0 / g) - 1.0); + float w_2 = c / g_ipp2; + float s_2 = w_2 * m_1; + float u_2 = pow((r_hit / m_1) / ((r_hit / m_1) + w_2), g); + float m_2 = m_1 / u_2; + float ts = pow(max(0.0, x) / (x + s_2), g) * m_2; + return max(0.0, (ts * ts) / (ts + t_1)); +} + +struct Drt { + float nits_peak, exposure, highlights, shadows, contrast, saturation, dechroma, flare, hue, blowout; + float mid_gray_nits; + bool clamp_zero; // clamp to Rec. 709 (no negative channel) before the peak +}; + +// The tone mapper by luminance, then hue, saturation and blowout in OkLab; clamped to the peak in Rec. 709. +vec3 drt(vec3 bt709, Drt d) { + const float mid = 0.18; + float peak = d.nits_peak / 100.0; + float y_original = y709(bt709); + float y = y_original * d.exposure; + y = highlights(y, d.highlights, mid); + y = shadows(y, d.shadows, mid); + float y_new = clamp(curve(y, d.nits_peak, d.contrast, d.mid_gray_nits, d.flare), 0.0, peak); + vec3 color = bt709 * (y_original > 0.0 ? y_new / y_original : 0.0); + if (d.dechroma != 0.0 || d.saturation != 1.0 || d.hue != 0.0 || d.blowout != 0.0) { + vec3 lab = oklab_from_bt709(color); + if (d.hue != 0.0) { + vec3 old_lab = oklab_from_bt709(bt709); + float pre = length(lab.yz); + lab.yz = mix(lab.yz, old_lab.yz, d.hue); + lab.yz *= divide_safe(pre, length(lab.yz), 1.0); + } + if (d.dechroma != 0.0) lab.yz *= mix(1.0, 0.0, clamp(pow(y_original / 100.0, 1.0 - d.dechroma), 0.0, 1.0)); + if (d.blowout != 0.0) { + float percent_max = clamp(y_original * 100.0 / 10000.0, 0.0, 1.0); + float change = pow(1.0 - percent_max, 100.0 * abs(d.blowout)); + if (d.blowout < 0.0) change = 2.0 - change; + lab.yz *= change; + } + lab.yz *= d.saturation; + color = bt709_from_oklab(lab); + } + if (d.clamp_zero) color = max(color, vec3(0.0)); + return min(color, vec3(peak)); +} + +// The same curve at SDR range with nothing changed: what the SDR grade is measured against. +vec3 neutral_sdr(vec3 c) { + Drt d = Drt(100.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 18.0, true); + return drt(c, d); +} + +// The graded SDR colour carried to the HDR range: its luminance gains what the untonemapped colour has above the +// neutral SDR curve, in the graded colour's hue. +vec3 upgrade(vec3 untonemapped, vec3 neutral, vec3 graded, float strength) { + float yu = y709(untonemapped), yn = y709(neutral), yg = y709(graded); + float ratio; + if (yu < yn) { + ratio = yu / yn; + } else { + float y_new = yg + max(0.0, yu - yn); + ratio = yg > 0.0 ? y_new / yg : 0.0; + } + vec3 scaled = hue_oklab(graded * ratio, graded); + return mix(untonemapped, scaled, strength); +} + vec3 aces_fitted(vec3 x) { // the game's curve and clamp return clamp(x * (2.51 * x + 0.03) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0); } @@ -70,6 +249,7 @@ const mat3 PSY_BT709_TO_LMS = mat3(0.2896057766, 0.6972466442, 0.0763712786, const mat3 PSY_LMS_TO_BT709 = mat3(4.9676196505, -4.9223795294, 0.3391991812, -0.6196828788, 2.0517506515, -0.3587439844, -0.0861520040, -0.0658193830, 1.9889544833); +const vec3 PSY_YF_WEIGHTS = vec3(0.68990272, 0.34832189, 0.0); // luminance of the observer from L and M const vec3 PSY_D65_WHITE_LMS = vec3(1.0632236993, 0.9097359297, 0.5789358779); // PSY_BT709_TO_LMS of white // Exact mean on equal-component vectors. @@ -93,12 +273,51 @@ void psy_response_logs(float log_q, float k, float n, out float log_u, out float // Extended linear Rec. 709 in, relative to reference white; the same out, inside Rec. 709 and below peak. // anchor_in is the scene grey the eye is adapted to, anchor_out where it lands; cone is the response's exponent. -vec3 psycho35(vec3 bt709, float peak, float cone, float anchor_in_gray, float anchor_out_gray) { +// highlights, shadows, contrast and purity_scale (saturation) grade the input; each is left out at 1. +vec3 psycho35(vec3 bt709, float peak, float highlights, float shadows, float contrast, float purity_scale, float cone, + float anchor_in_gray, float anchor_out_gray) { + vec3 input_lms = mulm(PSY_BT709_TO_LMS, bt709); vec3 anchor_in = mulm(PSY_BT709_TO_LMS, vec3(anchor_in_gray)); vec3 anchor_out = mulm(PSY_BT709_TO_LMS, vec3(anchor_out_gray)); - vec3 source_ratio = mulm(PSY_BT709_TO_LMS, bt709) / anchor_in; // the input against its adaptation + if (highlights != 1.0 || shadows != 1.0) { // on the observer's luminance, the colour scaled with it + float input_yf = dot(PSY_YF_WEIGHTS, input_lms); + float anchor_yf = dot(PSY_YF_WEIGHTS, anchor_in); + float graded_y = input_yf; + if (highlights != 1.0) { + float ratio = graded_y / anchor_yf; + float blend = graded_y > anchor_yf ? clamp(log2(ratio) / log2(1.0 / anchor_yf), 0.0, 1.0) : 0.0; + blend = smoother(blend); + float shaped = pow(ratio, abs(highlights - 1.0)); + graded_y = highlights > 1.0 ? graded_y * mix(1.0, shaped, blend) : graded_y / mix(1.0, shaped, blend); + } + if (shadows != 1.0) { + float ratio = graded_y / anchor_yf; + float blend = graded_y > anchor_yf * exp2(-4.0) ? clamp(log2(ratio) / -4.0, 0.0, 1.0) : 1.0; + blend = smoother(blend); + float target_y = shadows > 1.0 + ? graded_y * (1.0 - anchor_yf * anchor_yf) + anchor_yf * anchor_yf * anchor_yf * pow(ratio, 2.0 - shadows) + : graded_y * (1.0 + anchor_yf * anchor_yf) - anchor_yf * anchor_yf * anchor_yf * pow(ratio, shadows); + graded_y = mix(graded_y, target_y, blend); + } + if (input_yf != 0.0) input_lms *= graded_y / input_yf; + } + vec3 source_ratio = input_lms / anchor_in; // the input against its adaptation + // purity (saturation) around the neutral of the same luminance, relative to the adaptation + if (purity_scale != contrast) { + float neutral_ratio = dot(PSY_YF_WEIGHTS, input_lms) / dot(PSY_YF_WEIGHTS, anchor_in); + vec3 purity_direction = source_ratio - neutral_ratio; + float purity_amount = purity_scale / contrast; + // more purity must not turn a valid cone input negative: the common amount is bounded, not single cones + if (all(greaterThanEqual(source_ratio, vec3(0.0))) && neutral_ratio > 0.0 && purity_amount > 1.0) { + for (int c = 0; c < 3; ++c) + if (purity_direction[c] < 0.0) purity_amount = min(purity_amount, neutral_ratio / -purity_direction[c]); + source_ratio = max(neutral_ratio + purity_amount * purity_direction, 0.0); + } else { + source_ratio = neutral_ratio + purity_amount * purity_direction; + } + } vec3 anchor_power = anchor_out / (PSY_D65_WHITE_LMS * peak); - vec3 input_exponent = cone / (1.0 - anchor_power); + vec3 input_exponent = contrast * cone / (1.0 - anchor_power); if (all(equal(source_ratio, vec3(0.0)))) return vec3(0.0); // the exposure factored out: source_ratio = drive * ratio, the largest component of ratio 1 float drive = max(abs(source_ratio.x), max(abs(source_ratio.y), abs(source_ratio.z))); @@ -165,26 +384,37 @@ vec3 hue_clip(vec3 untonemapped, float amount) { return untonemapped; } -// The scene (before the exposure gain) mapped onto the display, in picture whites. peak: the display's peak in -// picture whites, as the response is to reach it. -vec3 scene_tonemap(vec3 untonemapped, float gain, float peak) { - untonemapped = hue_clip(untonemapped, 0.5); - // the log-log slope x V'(x) / V(x) of the game's curve where V''(x) = 0, at x = 0.120305925 - const float cone = 1.3355037565264822; - // scene grey where the game's curve puts it: the curve's value at 0.18 times the gain - float anchor_out = aces_fitted(vec3(0.18)).x * gain; - return psycho35(untonemapped, peak, cone, 0.18, anchor_out); +// The scene (before the exposure gain) through the observer-response mapper, in picture whites. peak: the display's +// peak in picture whites, as the response is to reach it. +vec3 scene_tonemap(vec3 untonemapped, float gain, float peak, vec4 p1, vec4 p2, vec4 p3, vec4 p4) { + if (p1.x == 0.0) return vec3(0.0); // zero exposure is black; the mapper needs a positive anchor + if (p4.x > 0.0) untonemapped = hue_clip(untonemapped, p4.x); + // matched: the log-log slope x V'(x) / V(x) of the game's curve where V''(x) = 0, at x = 0.120305925 + float cone = p4.z != 0.0 ? 1.3355037565264822 : 1.0; + // scene grey where the game's curve puts it (the curve's value at 0.18) or at its own value, times the gain and + // the exposure + float anchor_out = (p4.y == 1.0 ? aces_fitted(vec3(0.18)).x : 0.18) * gain * p1.x; + return psycho35(untonemapped, peak, p1.y, p1.z, p1.w, p2.x, cone * p3.w, 0.18, anchor_out); } -vec3 nx_tonemap(vec3 x, vec4 scene, vec4 p0) { +vec3 nx_tonemap(vec3 x, vec4 scene, vec4 p0, vec4 p1, vec4 p2, vec4 p3, vec4 p4) { vec3 vanilla = aces_fitted(x); if (p0.w < 0.0) return vanilla; // the game's own SDR picture - // the mapper takes the light before the gain; without a gain there is no light - if (!(scene.a > 0.0)) return vec3(0.0); - float peak = gamma_to_srgb(max(p0.x, p0.y) / p0.y, 2.2); // in picture whites, before the present's correction - vec3 color = scene_tonemap(scene.rgb / scene.a, scene.a, peak); + vec3 color = vanilla; // tone mapper 0: the game's curve, at the picture's white + if (p0.w == 9.0) { + // the mapper takes the light before the gain; without a gain there is no light + if (!(scene.a > 0.0)) return vec3(0.0); + float peak = peak_before_gamma(max(p0.x, p0.y) / p0.y, p3.z); + color = scene_tonemap(scene.rgb / scene.a, scene.a, peak, p1, p2, p3, p4); + } else if (p0.w > 0.0) { + vec3 graded = p3.y == 0.0 ? x : upgrade(x, neutral_sdr(x), vanilla, p3.y); + float peak = peak_before_gamma(p0.x / p0.y, p3.z); + Drt d = Drt(peak * 100.0, p1.x, p1.y, p1.z, p1.w, p2.x, p2.z, 0.10 * pow(p2.w, 10.0), p3.x, -(p2.y - 1.0), + 18.0, false); + color = drt(graded, d); + } color *= p0.y / p0.z; // the picture's white against the HUD's return mix(color, vec3(0.0), isnan(color)); } -void main() { o = vec4(nx_tonemap(a0.xyz, a1, a2), 1.0); } +void main() { o = vec4(nx_tonemap(a0.xyz, a1, a2, a3, a4, a5, a6), 1.0); } diff --git a/runtime/ui/menu.cpp b/runtime/ui/menu.cpp index a6d6795b..75c4a27a 100644 --- a/runtime/ui/menu.cpp +++ b/runtime/ui/menu.cpp @@ -1110,9 +1110,12 @@ static const char *hdr_note(void) { return note; } -// HDR (vk_hdr.c), two kinds: the game's picture tone mapped to the display (1, one fixed look), switched on and off at -// once, or the game's own HDR (native), which the game takes up at its start. The three brightness rows follow while -// either is on. +// HDR (vk_hdr.c), two kinds: the game's picture tone mapped to the display (1), switched on and off at once, or the +// game's own HDR (native), which the game takes up at its start. The three brightness rows follow while either is on. +// The tone mapped picture's own rows come after them (native HDR is the game's picture as it is): the tone mapper and +// the gamma correction, which every tone mapper's picture gets, then the rows of the tone mapper chosen: none for the +// game's curve, the grading rows of both others between the rows each has alone. All of them apply at once; their +// defaults are one look (vk_hdr.c's head names it). static void row_hdr(void) { static const char *const values[] = {"0", "1", "native"}; const char *names[] = {menu_text(UI_MENU_FRAMEGEN_OFF), menu_text(UI_MENU_HDR_TONEMAPPED), menu_text(UI_MENU_HDR_NATIVE)}; @@ -1121,6 +1124,35 @@ static void row_hdr(void) { opt_slider("NX2NAVI_HDR_PEAK_NITS", UI_MENU_HDR_PEAK, 400, 10000, UI_MENU_HDR_PEAK_DESC, 50, "%.0f nits"); opt_slider("NX2NAVI_HDR_GAME_NITS", UI_MENU_HDR_GAME, 48, 500, UI_MENU_HDR_GAME_DESC, 1, "%.0f nits"); opt_slider("NX2NAVI_HDR_UI_NITS", UI_MENU_HDR_UI, 48, 500, UI_MENU_HDR_UI_DESC, 1, "%.0f nits"); + if (!strcmp(cfg_value("NX2NAVI_HDR"), "native")) return; + static const char *const curves[] = {"vanilla", "psychov35", "renodrt"}; + static const char *const curve_names[] = {"Vanilla", "PsychoV35", "RenoDRT"}; + opt_choice_list("NX2NAVI_HDR_TONEMAP", UI_MENU_HDR_TONEMAP, curves, curve_names, 3, UI_MENU_HDR_TONEMAP_DESC); + static const char *const gammas[] = {"off", "2.2", "2.4"}; + const char *gamma_names[] = {menu_text(UI_MENU_FRAMEGEN_OFF), "2.2", "BT.1886 (2.4)"}; + opt_choice_list("NX2NAVI_HDR_GAMMA", UI_MENU_HDR_GAMMA, gammas, gamma_names, 3, UI_MENU_HDR_GAMMA_DESC); + const char *curve = cfg_value("NX2NAVI_HDR_TONEMAP"); + if (!strcmp(curve, "vanilla")) return; + const bool luminance = !strcmp(curve, "renodrt"); // else the observer-response mapper, as vk_hdr.c reads the setting + if (!luminance) { + static const char *const greys[] = {"render", "vanilla"}; + static const char *const grey_names[] = {"Render", "Vanilla"}; + opt_choice_list("NX2NAVI_HDR_MIDGRAY", UI_MENU_HDR_MIDGRAY, greys, grey_names, 2, UI_MENU_HDR_MIDGRAY_DESC); + opt_toggle("NX2NAVI_HDR_MATCH_SLOPE", UI_MENU_HDR_MATCH_SLOPE, UI_MENU_HDR_MATCH_SLOPE_DESC); + opt_slider("NX2NAVI_HDR_TONE_SLOPE", UI_MENU_HDR_TONE_SLOPE, 0, 100, UI_MENU_HDR_TONE_SLOPE_DESC, 1, "%.0f"); + opt_slider("NX2NAVI_HDR_HUE_CLIP", UI_MENU_HDR_HUE_CLIP, 0, 100, UI_MENU_HDR_HUE_CLIP_DESC, 5, "%.0f %%"); + } + opt_slider("NX2NAVI_HDR_EXPOSURE", UI_MENU_HDR_EXPOSURE, 0, 200, UI_MENU_HDR_EXPOSURE_DESC, 5, "%.0f %%"); + opt_slider("NX2NAVI_HDR_HIGHLIGHTS", UI_MENU_HDR_HIGHLIGHTS, 0, 100, UI_MENU_HDR_HIGHLIGHTS_DESC, 1, "%.0f"); + opt_slider("NX2NAVI_HDR_SHADOWS", UI_MENU_HDR_SHADOWS, 0, 100, UI_MENU_HDR_SHADOWS_DESC, 1, "%.0f"); + opt_slider("NX2NAVI_HDR_CONTRAST", UI_MENU_HDR_CONTRAST, 0, 100, UI_MENU_HDR_CONTRAST_DESC, 1, "%.0f"); + opt_slider("NX2NAVI_HDR_SATURATION", UI_MENU_HDR_SATURATION, 0, 100, UI_MENU_HDR_SATURATION_DESC, 1, "%.0f"); + if (!luminance) return; + opt_slider("NX2NAVI_HDR_HIGHLIGHT_SAT", UI_MENU_HDR_HIGHLIGHT_SAT, 0, 100, UI_MENU_HDR_HIGHLIGHT_SAT_DESC, 1, "%.0f"); + opt_slider("NX2NAVI_HDR_BLOWOUT", UI_MENU_HDR_BLOWOUT, 0, 100, UI_MENU_HDR_BLOWOUT_DESC, 1, "%.0f"); + opt_slider("NX2NAVI_HDR_FLARE", UI_MENU_HDR_FLARE, 0, 100, UI_MENU_HDR_FLARE_DESC, 1, "%.0f"); + opt_slider("NX2NAVI_HDR_HUE", UI_MENU_HDR_HUE, 0, 100, UI_MENU_HDR_HUE_DESC, 5, "%.0f %%"); + opt_slider("NX2NAVI_HDR_GRADE", UI_MENU_HDR_GRADE, 0, 100, UI_MENU_HDR_GRADE_DESC, 5, "%.0f %%"); } // DLSS frame generation's multiplier (NX2NAVI_FRAMEGEN_MULTIPLIER, vk_dlssg.c, at once): 2x, 3x or 4x as far as this diff --git a/runtime/ui_text.c b/runtime/ui_text.c index 475e0e5d..0515150f 100644 --- a/runtime/ui_text.c +++ b/runtime/ui_text.c @@ -126,6 +126,44 @@ static const char *const text_en[UI_TEXT_COUNT] = { [UI_MENU_HDR_GAME_DESC] = "Brightness of white in the game, in nits.", [UI_MENU_HDR_UI] = "HUD and menu brightness", [UI_MENU_HDR_UI_DESC] = "Brightness of the HUD and this menu, in nits.", + [UI_MENU_HDR_HUE] = "Hue correction", + [UI_MENU_HDR_HUE_DESC] = "How much bright colours keep the hue they have before the tone mapper; 0 lets them shift " + "as the curve takes them.", + [UI_MENU_HDR_GAMMA] = "Gamma correction", + [UI_MENU_HDR_GAMMA_DESC] = "Shows the picture as a 2.2 or BT.1886 (2.4) display would, as the game was made to be " + "seen; Off keeps the sRGB curve, with lighter shadows.", + [UI_MENU_HDR_TONEMAP] = "Tone mapper", + [UI_MENU_HDR_TONEMAP_DESC] = "Vanilla: the game's own curve at SDR brightness. PsychoV35: the scene through a model " + "of the eye's response, matched to the game's mid grey and tone, up to your display's " + "peak. RenoDRT: the game's SDR look carried up to the peak by brightness.", + [UI_MENU_HDR_EXPOSURE] = "Exposure", + [UI_MENU_HDR_EXPOSURE_DESC] = "Brightness of the whole picture before the tone mapper; 100 % leaves it unchanged.", + [UI_MENU_HDR_HIGHLIGHTS] = "Highlights", + [UI_MENU_HDR_HIGHLIGHTS_DESC] = "Brightness of the bright parts; 50 leaves them unchanged.", + [UI_MENU_HDR_SHADOWS] = "Shadows", + [UI_MENU_HDR_SHADOWS_DESC] = "Brightness of the dark parts; 50 leaves them unchanged.", + [UI_MENU_HDR_CONTRAST] = "Contrast", + [UI_MENU_HDR_CONTRAST_DESC] = "Contrast around mid grey; 50 leaves it unchanged.", + [UI_MENU_HDR_SATURATION] = "Saturation", + [UI_MENU_HDR_SATURATION_DESC] = "Strength of all colours; 50 leaves it unchanged.", + [UI_MENU_HDR_HIGHLIGHT_SAT] = "Highlight saturation", + [UI_MENU_HDR_HIGHLIGHT_SAT_DESC] = "Strength of the colours of the brightest parts; 50 leaves it unchanged.", + [UI_MENU_HDR_BLOWOUT] = "Blowout", + [UI_MENU_HDR_BLOWOUT_DESC] = "Turns the brightest parts towards white; 0 keeps their colour.", + [UI_MENU_HDR_FLARE] = "Flare", + [UI_MENU_HDR_FLARE_DESC] = "Lifts the darkest parts as a little stray light would; 0 keeps black black.", + [UI_MENU_HDR_GRADE] = "Colour grading", + [UI_MENU_HDR_GRADE_DESC] = "How much of the game's own colour grading the HDR picture keeps; 0 is the scene without it.", + [UI_MENU_HDR_MIDGRAY] = "Mid grey", + [UI_MENU_HDR_MIDGRAY_DESC] = "Where the scene's mid grey lands. Vanilla: where the game's own curve puts it. Render: " + "at the scene's value, a little darker.", + [UI_MENU_HDR_MATCH_SLOPE] = "Match tone slope", + [UI_MENU_HDR_MATCH_SLOPE_DESC] = "Gives the mid tones the contrast of the game's own curve.", + [UI_MENU_HDR_TONE_SLOPE] = "Tone slope", + [UI_MENU_HDR_TONE_SLOPE_DESC] = "Steepness of the tone curve through mid grey; 50 leaves it unchanged.", + [UI_MENU_HDR_HUE_CLIP] = "Hue clip emulation", + [UI_MENU_HDR_HUE_CLIP_DESC] = "How much bright colours turn as they do where the game's SDR picture clips: fire " + "towards yellow and white. 0 keeps their hue.", [UI_MENU_SHADOW_SCALE] = "Shadow resolution", [UI_MENU_SHADOW_SCALE_DESC] = "Sharper shadows. Higher costs more GPU power.", [UI_MENU_PCSS] = "Soft shadows", @@ -760,6 +798,44 @@ static const char *const text_ru[UI_TEXT_COUNT] = { [UI_MENU_HDR_GAME_DESC] = "Яркость белого в игре, в нитах.", [UI_MENU_HDR_UI] = "Яркость интерфейса и меню", [UI_MENU_HDR_UI_DESC] = "Яркость интерфейса и этого меню, в нитах.", + [UI_MENU_HDR_HUE] = "Коррекция оттенка", + [UI_MENU_HDR_HUE_DESC] = "Насколько яркие цвета сохраняют оттенок, который был до тональной кривой; при 0 они " + "смещаются вместе с кривой.", + [UI_MENU_HDR_GAMMA] = "Коррекция гаммы", + [UI_MENU_HDR_GAMMA_DESC] = "Показывает картинку так, как её показал бы экран с гаммой 2.2 или BT.1886 (2.4), для " + "которого игра делалась; «Выкл.» оставляет кривую sRGB с более светлыми тенями.", + [UI_MENU_HDR_TONEMAP] = "Тональная кривая", + [UI_MENU_HDR_TONEMAP_DESC] = "Vanilla: собственная кривая игры с яркостью SDR. PsychoV35: сцена через модель отклика " + "глаза, согласованная со средним серым и тоном игры, до пиковой яркости экрана. RenoDRT: " + "SDR-вид игры, доведённый до пика по яркости.", + [UI_MENU_HDR_EXPOSURE] = "Экспозиция", + [UI_MENU_HDR_EXPOSURE_DESC] = "Яркость всей картинки до тональной кривой; 100 % оставляет её без изменений.", + [UI_MENU_HDR_HIGHLIGHTS] = "Света", + [UI_MENU_HDR_HIGHLIGHTS_DESC] = "Яркость светлых участков; 50 оставляет их без изменений.", + [UI_MENU_HDR_SHADOWS] = "Тени", + [UI_MENU_HDR_SHADOWS_DESC] = "Яркость тёмных участков; 50 оставляет их без изменений.", + [UI_MENU_HDR_CONTRAST] = "Контраст", + [UI_MENU_HDR_CONTRAST_DESC] = "Контраст вокруг среднего серого; 50 оставляет его без изменений.", + [UI_MENU_HDR_SATURATION] = "Насыщенность", + [UI_MENU_HDR_SATURATION_DESC] = "Сила всех цветов; 50 оставляет её без изменений.", + [UI_MENU_HDR_HIGHLIGHT_SAT] = "Насыщенность светов", + [UI_MENU_HDR_HIGHLIGHT_SAT_DESC] = "Сила цвета самых ярких участков; 50 оставляет её без изменений.", + [UI_MENU_HDR_BLOWOUT] = "Выгорание", + [UI_MENU_HDR_BLOWOUT_DESC] = "Уводит самые яркие участки к белому; 0 сохраняет их цвет.", + [UI_MENU_HDR_FLARE] = "Засветка", + [UI_MENU_HDR_FLARE_DESC] = "Приподнимает самые тёмные участки, как немного рассеянного света; 0 оставляет чёрное чёрным.", + [UI_MENU_HDR_GRADE] = "Цветокоррекция игры", + [UI_MENU_HDR_GRADE_DESC] = "Сколько собственной цветокоррекции игры сохраняет HDR-картинка; 0 — сцена без неё.", + [UI_MENU_HDR_MIDGRAY] = "Средний серый", + [UI_MENU_HDR_MIDGRAY_DESC] = "Куда попадает средний серый сцены. Vanilla: туда же, куда его ставит кривая игры. " + "Render: на значение сцены, чуть темнее.", + [UI_MENU_HDR_MATCH_SLOPE] = "Наклон тона как в игре", + [UI_MENU_HDR_MATCH_SLOPE_DESC] = "Даёт средним тонам контраст собственной кривой игры.", + [UI_MENU_HDR_TONE_SLOPE] = "Наклон тона", + [UI_MENU_HDR_TONE_SLOPE_DESC] = "Крутизна тональной кривой у среднего серого; 50 оставляет её без изменений.", + [UI_MENU_HDR_HUE_CLIP] = "Имитация сдвига оттенка", + [UI_MENU_HDR_HUE_CLIP_DESC] = "Насколько яркие цвета сдвигаются так, как при обрезке в SDR-картинке игры: огонь к " + "жёлтому и белому. 0 сохраняет их оттенок.", [UI_MENU_SHADOW_SCALE] = "Разрешение теней", [UI_MENU_SHADOW_SCALE_DESC] = "Более чёткие тени. Выше — больше нагрузка на видеокарту.", [UI_MENU_PCSS] = "Мягкие тени", @@ -1611,6 +1687,44 @@ static const char *const text_pt_br[UI_TEXT_COUNT] = { [UI_MENU_HDR_GAME_DESC] = "Brilho do branco no jogo, em nits.", [UI_MENU_HDR_UI] = "Brilho do HUD e do menu", [UI_MENU_HDR_UI_DESC] = "Brilho da interface e deste menu, em nits.", + [UI_MENU_HDR_HUE] = "Correção de matiz", + [UI_MENU_HDR_HUE_DESC] = "Quanto as cores brilhantes mantêm o matiz que tinham antes da curva de tons; 0 deixa que " + "mudem com a curva.", + [UI_MENU_HDR_GAMMA] = "Correção de gama", + [UI_MENU_HDR_GAMMA_DESC] = "Mostra a imagem como uma tela de gama 2.2 ou BT.1886 (2.4) mostraria, como o jogo foi " + "feito para ser visto; Desligado mantém a curva sRGB, com sombras mais claras.", + [UI_MENU_HDR_TONEMAP] = "Curva de tons", + [UI_MENU_HDR_TONEMAP_DESC] = "Vanilla: a curva do próprio jogo, com brilho SDR. PsychoV35: a cena por um modelo da " + "resposta do olho, ajustada ao cinza médio e ao tom do jogo, até o pico da sua tela. " + "RenoDRT: o visual SDR do jogo levado até o pico pelo brilho.", + [UI_MENU_HDR_EXPOSURE] = "Exposição", + [UI_MENU_HDR_EXPOSURE_DESC] = "Brilho de toda a imagem antes da curva de tons; 100 % a deixa como está.", + [UI_MENU_HDR_HIGHLIGHTS] = "Realces", + [UI_MENU_HDR_HIGHLIGHTS_DESC] = "Brilho das partes claras; 50 as deixa como estão.", + [UI_MENU_HDR_SHADOWS] = "Sombras", + [UI_MENU_HDR_SHADOWS_DESC] = "Brilho das partes escuras; 50 as deixa como estão.", + [UI_MENU_HDR_CONTRAST] = "Contraste", + [UI_MENU_HDR_CONTRAST_DESC] = "Contraste em torno do cinza médio; 50 o deixa como está.", + [UI_MENU_HDR_SATURATION] = "Saturação", + [UI_MENU_HDR_SATURATION_DESC] = "Força de todas as cores; 50 a deixa como está.", + [UI_MENU_HDR_HIGHLIGHT_SAT] = "Saturação dos realces", + [UI_MENU_HDR_HIGHLIGHT_SAT_DESC] = "Força das cores das partes mais claras; 50 a deixa como está.", + [UI_MENU_HDR_BLOWOUT] = "Estouro", + [UI_MENU_HDR_BLOWOUT_DESC] = "Leva as partes mais claras para o branco; 0 mantém a cor delas.", + [UI_MENU_HDR_FLARE] = "Flare", + [UI_MENU_HDR_FLARE_DESC] = "Ergue as partes mais escuras como um pouco de luz difusa faria; 0 mantém o preto preto.", + [UI_MENU_HDR_GRADE] = "Gradação de cor", + [UI_MENU_HDR_GRADE_DESC] = "Quanto da gradação de cor do próprio jogo a imagem HDR mantém; 0 é a cena sem ela.", + [UI_MENU_HDR_MIDGRAY] = "Cinza médio", + [UI_MENU_HDR_MIDGRAY_DESC] = "Onde fica o cinza médio da cena. Vanilla: onde a curva do próprio jogo o coloca. " + "Render: no valor da cena, um pouco mais escuro.", + [UI_MENU_HDR_MATCH_SLOPE] = "Igualar inclinação do tom", + [UI_MENU_HDR_MATCH_SLOPE_DESC] = "Dá aos tons médios o contraste da curva do próprio jogo.", + [UI_MENU_HDR_TONE_SLOPE] = "Inclinação do tom", + [UI_MENU_HDR_TONE_SLOPE_DESC] = "Inclinação da curva de tons no cinza médio; 50 a deixa como está.", + [UI_MENU_HDR_HUE_CLIP] = "Emulação do corte de matiz", + [UI_MENU_HDR_HUE_CLIP_DESC] = "Quanto as cores brilhantes viram como onde a imagem SDR do jogo estoura: o fogo " + "para o amarelo e o branco. 0 mantém o matiz delas.", [UI_MENU_PROMPTS] = "Ícones de botões", [UI_MENU_PROMPTS_DESC] = "Quais botões os ícones do jogo mostram: no texto, no ícone de ação e nas notas da " "ocarina. Nintendo: os do próprio jogo. Xbox e PlayStation: os botões desse controle (LB, " @@ -2295,6 +2409,38 @@ static const char *const text_zh_hans[UI_TEXT_COUNT] = { [UI_MENU_HDR_GAME_DESC] = "游戏中白色的亮度(尼特)。", [UI_MENU_HDR_UI] = "HUD 和菜单亮度", [UI_MENU_HDR_UI_DESC] = "界面和本菜单的亮度(尼特)。", + [UI_MENU_HDR_HUE] = "色调校正", + [UI_MENU_HDR_HUE_DESC] = "明亮色彩保留色调映射前色调的程度;0 让它们随曲线偏移。", + [UI_MENU_HDR_GAMMA] = "伽马校正", + [UI_MENU_HDR_GAMMA_DESC] = "按游戏设计时的观看方式,以 2.2 或 BT.1886(2.4)显示器的效果呈现画面;关闭则保留 sRGB 曲线,暗部更亮。", + [UI_MENU_HDR_TONEMAP] = "色调映射", + [UI_MENU_HDR_TONEMAP_DESC] = "Vanilla:游戏自身的曲线,SDR 亮度。PsychoV35:场景经过人眼响应模型,与游戏的中灰和影调对齐,直到显示器的峰值亮度。RenoDRT:按亮度把游戏的 SDR 观感延伸到峰值。", + [UI_MENU_HDR_EXPOSURE] = "曝光", + [UI_MENU_HDR_EXPOSURE_DESC] = "色调映射前整个画面的亮度;100 % 保持不变。", + [UI_MENU_HDR_HIGHLIGHTS] = "高光", + [UI_MENU_HDR_HIGHLIGHTS_DESC] = "明亮部分的亮度;50 保持不变。", + [UI_MENU_HDR_SHADOWS] = "阴影", + [UI_MENU_HDR_SHADOWS_DESC] = "暗部的亮度;50 保持不变。", + [UI_MENU_HDR_CONTRAST] = "对比度", + [UI_MENU_HDR_CONTRAST_DESC] = "中灰周围的对比度;50 保持不变。", + [UI_MENU_HDR_SATURATION] = "饱和度", + [UI_MENU_HDR_SATURATION_DESC] = "所有色彩的强度;50 保持不变。", + [UI_MENU_HDR_HIGHLIGHT_SAT] = "高光饱和度", + [UI_MENU_HDR_HIGHLIGHT_SAT_DESC] = "最亮部分的色彩强度;50 保持不变。", + [UI_MENU_HDR_BLOWOUT] = "过曝", + [UI_MENU_HDR_BLOWOUT_DESC] = "让最亮的部分趋向白色;0 保留其颜色。", + [UI_MENU_HDR_FLARE] = "眩光", + [UI_MENU_HDR_FLARE_DESC] = "像少量杂散光一样提亮最暗的部分;0 保持纯黑。", + [UI_MENU_HDR_GRADE] = "调色", + [UI_MENU_HDR_GRADE_DESC] = "HDR 画面保留游戏自身调色的程度;0 为不含调色的场景。", + [UI_MENU_HDR_MIDGRAY] = "中灰", + [UI_MENU_HDR_MIDGRAY_DESC] = "场景中灰落在哪里。Vanilla:游戏自身曲线给出的位置。Render:场景的原值,稍暗。", + [UI_MENU_HDR_MATCH_SLOPE] = "匹配影调斜率", + [UI_MENU_HDR_MATCH_SLOPE_DESC] = "让中间调具有游戏自身曲线的对比度。", + [UI_MENU_HDR_TONE_SLOPE] = "影调斜率", + [UI_MENU_HDR_TONE_SLOPE_DESC] = "影调曲线在中灰处的陡峭程度;50 保持不变。", + [UI_MENU_HDR_HUE_CLIP] = "色调裁切模拟", + [UI_MENU_HDR_HUE_CLIP_DESC] = "明亮色彩像游戏 SDR 画面裁切处那样偏转的程度:火焰偏向黄色和白色。0 保持其色调。", [UI_MENU_PROMPTS] = "按键提示", [UI_MENU_PROMPTS_DESC] = "游戏的按键提示显示哪种按键:包括文字、动作提示和陶笛的音符。Nintendo:游戏原有的提示。" "Xbox 和 PlayStation:该手柄的按键(LB、RB、LT、RT、Menu、View;L1、R1、L2、R2、Options、Create)," diff --git a/runtime/ui_text.h b/runtime/ui_text.h index e23a9345..8f5c9f84 100644 --- a/runtime/ui_text.h +++ b/runtime/ui_text.h @@ -58,7 +58,16 @@ typedef enum { UI_MENU_COLOR_SATURATION, UI_MENU_COLOR_SATURATION_DESC, UI_MENU_COLOR_GAMMA, UI_MENU_COLOR_GAMMA_DESC, UI_MENU_HDR_GROUP, // group heading over the HDR rows UI_MENU_HDR, UI_MENU_HDR_DESC, UI_MENU_HDR_PEAK, UI_MENU_HDR_PEAK_DESC, UI_MENU_HDR_GAME, UI_MENU_HDR_GAME_DESC, - UI_MENU_HDR_UI, UI_MENU_HDR_UI_DESC, UI_MENU_SHADOW_SCALE, UI_MENU_SHADOW_SCALE_DESC, + UI_MENU_HDR_UI, UI_MENU_HDR_UI_DESC, UI_MENU_HDR_HUE, UI_MENU_HDR_HUE_DESC, + UI_MENU_HDR_GAMMA, UI_MENU_HDR_GAMMA_DESC, UI_MENU_HDR_TONEMAP, UI_MENU_HDR_TONEMAP_DESC, + UI_MENU_HDR_EXPOSURE, UI_MENU_HDR_EXPOSURE_DESC, UI_MENU_HDR_HIGHLIGHTS, UI_MENU_HDR_HIGHLIGHTS_DESC, + UI_MENU_HDR_SHADOWS, UI_MENU_HDR_SHADOWS_DESC, UI_MENU_HDR_CONTRAST, UI_MENU_HDR_CONTRAST_DESC, + UI_MENU_HDR_SATURATION, UI_MENU_HDR_SATURATION_DESC, UI_MENU_HDR_HIGHLIGHT_SAT, UI_MENU_HDR_HIGHLIGHT_SAT_DESC, + UI_MENU_HDR_BLOWOUT, UI_MENU_HDR_BLOWOUT_DESC, UI_MENU_HDR_FLARE, UI_MENU_HDR_FLARE_DESC, + UI_MENU_HDR_GRADE, UI_MENU_HDR_GRADE_DESC, + UI_MENU_HDR_MIDGRAY, UI_MENU_HDR_MIDGRAY_DESC, UI_MENU_HDR_MATCH_SLOPE, UI_MENU_HDR_MATCH_SLOPE_DESC, + UI_MENU_HDR_TONE_SLOPE, UI_MENU_HDR_TONE_SLOPE_DESC, UI_MENU_HDR_HUE_CLIP, UI_MENU_HDR_HUE_CLIP_DESC, + UI_MENU_SHADOW_SCALE, UI_MENU_SHADOW_SCALE_DESC, UI_MENU_SHADOW_RANGE, UI_MENU_SHADOW_RANGE_DESC, UI_MENU_CONTACT_SHADOWS, UI_MENU_CONTACT_SHADOWS_DESC, UI_MENU_PCSS, UI_MENU_PCSS_DESC, diff --git a/runtime/vk.h b/runtime/vk.h index 38b5ca24..f9c2001c 100644 --- a/runtime/vk.h +++ b/runtime/vk.h @@ -271,9 +271,9 @@ void vk_hdr_to_srgb8(const uint8_t *in, uint8_t *out, size_t px, int a2r); void vk_hdr_luminance_stats(const uint8_t *in, int stride, const int rect[4], int a2r, float out[7]); void vk_hdr_release(void); void vk_hdr_images_release(void); // the swapchain has no HDR: the encode's images go (waits for the device) // before the device goes away -// The support buffer's HDR settings, a vec4 at 1440 (vk_exec_draw.c support_fill; read by the game's tonemap with +// The support buffer's HDR settings, five vec4 at 1440 (vk_exec_draw.c support_fill; read by the game's tonemap with // shaders/hdr_tonemap.frag spliced in, vk_hdr.c tonemap_splice). -void vk_hdr_support(float out[4]); +void vk_hdr_support(float out[20]); // vk_contact.c: screen-space contact shadows on the game's composed scene colour (NX2NAVI_CONTACT_SHADOWS) int vk_contact_strength(void); // percent, 0 = off void vk_contact_release(void); // before the device goes away diff --git a/runtime/vk_exec_draw.c b/runtime/vk_exec_draw.c index ff2e4291..92712a07 100644 --- a/runtime/vk_exec_draw.c +++ b/runtime/vk_exec_draw.c @@ -44,8 +44,8 @@ #define MAX_DESC 1024 #define CHUNK_BYTES (4u << 20) #define ZERO_BYTES 65536u -#define SUPPORT_BYTES 1456u // the translator's SupportBuffer (std140): alpha_test, is_bgra[8], viewport_inverse, viewport_size, ...; - // 1408 pcss and 1424 shadow_spread (translator patches 0010, 0011), 1440..1455 HDR (support_fill) +#define SUPPORT_BYTES 1520u // the translator's SupportBuffer (std140): alpha_test, is_bgra[8], viewport_inverse, viewport_size, ...; + // 1408 pcss and 1424 shadow_spread (translator patches 0010, 0011), 1440..1519 HDR (support_fill) #define NVN_STAGE_VS 0 #define NVN_STAGE_FS 1 #define NVN_STAGE_CS 5 @@ -3323,7 +3323,7 @@ static void support_fill(uint8_t *b, float w, float h, float point_size) { memcpy(b + 1424, shadow_spread_params(), 16); // shadow_spread (translator patch 0011): sun shadow filter width for (int i = 0; i < 73; i++) memcpy(b + 192 + 16 * i, &one, 4); // render scale 1: the host renders at guest size if (vk_hdr_armed()) { // read by the game's tonemap only, once vk_hdr.c tonemap_splice has grown its block past the translator's - float hdr[4]; + float hdr[20]; vk_hdr_support(hdr); memcpy(b + 1440, hdr, sizeof hdr); } diff --git a/runtime/vk_hdr.c b/runtime/vk_hdr.c index f532631f..03608220 100644 --- a/runtime/vk_hdr.c +++ b/runtime/vk_hdr.c @@ -1,8 +1,13 @@ // HDR output (NX2NAVI_HDR=1): an HDR10 swapchain (Rec. 2020 primaries, PQ) and a game picture tone mapped to the -// display's range, one fixed look (shaders/hdr_tonemap.frag says which). Settings: NX2NAVI_HDR, on and off following -// the menu during a run (vk_hdr_wanted), the game's own HDR (native, the menu's other HDR) from the start only; and -// the nits NX2NAVI_HDR_PEAK_NITS (the display's peak), NX2NAVI_HDR_GAME_NITS (the picture's white, "paper white") and -// NX2NAVI_HDR_UI_NITS (the HUD's and this menu's white), read when the settings change (vk_hdr_support). With +// display's range (shaders/hdr_tonemap.frag says how). Settings: NX2NAVI_HDR, on and off following the menu during a +// run (vk_hdr_wanted), the game's own HDR (native, the menu's other HDR) from the start only; the nits +// NX2NAVI_HDR_PEAK_NITS (the display's peak), NX2NAVI_HDR_GAME_NITS (the picture's white, "paper white") and +// NX2NAVI_HDR_UI_NITS (the HUD's and this menu's white); NX2NAVI_HDR_TONEMAP with what each tone mapper takes +// (NX2NAVI_HDR_MIDGRAY, NX2NAVI_HDR_MATCH_SLOPE, NX2NAVI_HDR_TONE_SLOPE and NX2NAVI_HDR_HUE_CLIP, or +// NX2NAVI_HDR_HIGHLIGHT_SAT .. NX2NAVI_HDR_GRADE), the grading sliders NX2NAVI_HDR_EXPOSURE .. NX2NAVI_HDR_SATURATION +// of both and NX2NAVI_HDR_GAMMA, the display the present shows the picture as; all read when the settings change +// (vk_hdr_support). Left at their defaults they give one look: the observer-response mapper anchored to the game's mid +// grey and tone slope, half of the hue bend of the game's clamp, no grading, shown as a 2.2 display would. With // NX2NAVI_HDR=0 the swapchain is SDR, the spliced tonemap gives the game's own SDR picture (vk_hdr_support, p0.w = -1) // and the present is what it was. // @@ -12,10 +17,10 @@ // 1. vk_hdr_shader_swap, on each fragment shader as it loads (in memory only; the cache keeps the translator's): 1.1.1's // tonemap ends in the fitted ACES curve and a clamp to 0..1 per channel. tonemap_splice links the function // nx_tonemap of shaders/hdr_tonemap.frag into that shader and stores its result in place of the three clamped -// channels: the scene mapped onto the display (that file says how), from the values the curve takes and the scene -// before the game's exposure gain, with the settings in a vec4 the support buffer gains at 1440. Recognised by the curve's constants, not by a hash, so -// another translator still finds it. The game's FSR 1 upscale after it would clamp the picture to 1 again; -// fsr_unclamp drops that clamp in its SPIR-V. +// channels: the scene mapped onto the display by the tone mapper chosen (that file says how each does it), from the +// values the curve takes and the scene before the game's exposure gain, with the settings in five vec4 the support +// buffer gains at 1440. Recognised by the curve's constants, not by a hash, so another translator still finds it. +// The game's FSR 1 upscale after it would clamp the picture to 1 again; fsr_unclamp drops that clamp in its SPIR-V. // 2. vk_hdr_draw / vk_hdr_present_source: a draw into a presented window texture that samples one float texture of // its size is that copy; the present then reads the float texture instead of the window texture. Any other last // draw into it leaves the window texture, decoded from sRGB: menus and movies stay SDR at the HUD's white. @@ -104,13 +109,17 @@ void vk_hdr_off(void) { g_native = 0, g_armed = 0; } // that encode decodes, its darkest tones sit up to a hundred times above where such a display puts them (a scene // value of 0.001, 0.08 nits at a white of 350 nits against 0.001), the floor of a dark scene lifted to grey. So the // present shows the picture as a 2.2 display would (hdr_encode.comp, hdr_ui.frag: the power 2.2 of the sRGB code), -// and the tonemap aims its peak below the display's by that correction's inverse (hdr_tonemap.frag nx_tonemap). -#define HDR_GAMMA 1.0f // the present's correction: 0 none, 1: 2.2, 2: 2.4 +// and the tonemap aims its peak below the display's by that correction's inverse (hdr_tonemap.frag +// peak_before_gamma). That holds for every tone mapper and is the default of NX2NAVI_HDR_GAMMA, which can make it 2.4 +// or switch it off (the picture as its sRGB code decodes, the dark tones lighter). The game's own HDR does not take +// the setting: what the present still encodes there (the menu, the 8-bit pictures before the game's switch) is shown +// as a 2.2 display would. +#define HDR_GAMMA_22 1.0f // the present's correction: 0 none, 1: 2.2, 2: 2.4 typedef struct { float peak, game, ui; // nits - float gamma; // HDR_GAMMA (hdr_encode.comp, hdr_ui.frag) - float tm[4]; // the support buffer's vec4 (shaders/hdr_tonemap.frag p0) + float gamma; // the present's correction, as HDR_GAMMA_22 numbers it (hdr_encode.comp, hdr_ui.frag) + float tm[20]; // the support buffer's five vec4 (shaders/hdr_tonemap.frag p0..p4) } Nits; static struct { unsigned seen; int cur; Nits snap[2]; PlatMutex lock; } N = {.seen = ~0u, .lock = PLAT_MUTEX_INIT}; @@ -125,8 +134,34 @@ static Nits nits(void) { n->peak = clampf((float)cfg_double("NX2NAVI_HDR_PEAK_NITS"), 80.0f, 10000.0f); n->game = clampf((float)cfg_double("NX2NAVI_HDR_GAME_NITS"), 48.0f, 500.0f); n->ui = clampf((float)cfg_double("NX2NAVI_HDR_UI_NITS"), 48.0f, 500.0f); - n->gamma = HDR_GAMMA; - n->tm[0] = n->peak, n->tm[1] = n->game, n->tm[2] = n->ui, n->tm[3] = 1.0f; + // the tone mapper as shaders/hdr_tonemap.frag numbers it: the game's curve, the luminance mapper, the + // observer-response mapper (the default, and what any other value gives) + const char *tm = cfg_value("NX2NAVI_HDR_TONEMAP"); + float mapper = !strcmp(tm, "vanilla") ? 0.0f : !strcmp(tm, "renodrt") ? 3.0f : 9.0f; + const char *g = cfg_value("NX2NAVI_HDR_GAMMA"); + n->gamma = vk_hdr_native() ? HDR_GAMMA_22 + : !strcmp(g, "off") || !strcmp(g, "0") ? 0.0f : !strcmp(g, "2.4") ? 2.0f : HDR_GAMMA_22; + // The sliders' 0..100 with 50 unchanged become factors around 1; blowout, hue, grade and hue clip are + // fractions. Divided, not multiplied by a rounded reciprocal: 50 and 100 % come out as exactly 1, which + // the shader compares for to leave a grading step out. + float pct[10]; + static const char *const keys[10] = {"NX2NAVI_HDR_HIGHLIGHTS", "NX2NAVI_HDR_SHADOWS", "NX2NAVI_HDR_CONTRAST", + "NX2NAVI_HDR_SATURATION", "NX2NAVI_HDR_HIGHLIGHT_SAT", "NX2NAVI_HDR_BLOWOUT", + "NX2NAVI_HDR_FLARE", "NX2NAVI_HDR_HUE", "NX2NAVI_HDR_TONE_SLOPE", + "NX2NAVI_HDR_HUE_CLIP"}; + for (int i = 0; i < 10; i++) pct[i] = clampf((float)cfg_double(keys[i]), 0.0f, 100.0f); + float *t = n->tm; + t[0] = n->peak, t[1] = n->game, t[2] = n->ui; + t[3] = mapper; + t[4] = clampf((float)cfg_double("NX2NAVI_HDR_EXPOSURE"), 0.0f, 200.0f) / 100.0f; + t[5] = pct[0] / 50.0f, t[6] = pct[1] / 50.0f, t[7] = pct[2] / 50.0f; + t[8] = pct[3] / 50.0f, t[9] = pct[4] / 50.0f, t[10] = pct[5] / 100.0f, t[11] = pct[6] / 50.0f; + t[12] = pct[7] / 100.0f; + t[13] = clampf((float)cfg_double("NX2NAVI_HDR_GRADE"), 0.0f, 100.0f) / 100.0f; + t[14] = n->gamma, t[15] = pct[8] / 50.0f; + t[16] = pct[9] / 100.0f; + t[17] = !strcmp(cfg_value("NX2NAVI_HDR_MIDGRAY"), "render") ? 0.0f : 1.0f; + t[18] = cfg_bool("NX2NAVI_HDR_MATCH_SLOPE") ? 1.0f : 0.0f, t[19] = 0; __atomic_store_n(&N.cur, !N.cur, __ATOMIC_RELEASE); __atomic_store_n(&N.seen, v, __ATOMIC_RELEASE); } @@ -135,7 +170,7 @@ static Nits nits(void) { return N.snap[__atomic_load_n(&N.cur, __ATOMIC_ACQUIRE)]; } -void vk_hdr_support(float out[4]) { +void vk_hdr_support(float out[20]) { Nits n = nits(); memcpy(out, n.tm, sizeof n.tm); if (!vk_hdr_active()) out[3] = -1.0f; // no HDR10 swapchain (yet): the game's own SDR picture @@ -420,7 +455,7 @@ static int spv_top_level(const uint32_t *w, uint64_t first_label, uint64_t end, // translator's SupportBuffer ends there). The library's types are matched to the game's where the same (SPIR-V allows // one declaration of each), its constants and functions appended with new ids. Gives up (0) on any shape it does not // expect, and the shader stays as it was. -enum { HDR_SETS = 1, HDR_PARAMS = HDR_SETS + 2 }; // nx_tonemap(vec3 x, vec4 scene, vec4 settings[HDR_SETS]) +enum { HDR_SETS = 5, HDR_PARAMS = HDR_SETS + 2 }; // nx_tonemap(vec3 x, vec4 scene, vec4 settings[HDR_SETS]) static int tonemap_splice(uint32_t **spirv, uint64_t *words) { uint32_t *w = *spirv; uint64_t n = *words;