Files
NX2navi/runtime/shaders/hdr_encode.comp
tangerineturtle1 9e574f84ec Replace the enhanced HDR with a port of RenoDX's tone mapper.
The game's ACES tonemap shader gets a GLSL function linked into its SPIR-V where it clamps each channel: the game's SDR result upgraded to the HDR range by luminance and mapped to the display with the RenoDRT curve, exposure, highlights, shadows, contrast, saturation, highlight saturation, blowout, flare and hue correction. The settings reach it as four vec4 the support buffer gains at 1440. The present encode is a port of the swap chain pass: gamma correction, gamut compression, the peak clamp and PQ. Without an HDR10 swapchain the shader gives the game's own SDR picture; native HDR is unchanged.

The menu offers Off, RenoDX and Native, with gamma correction, the tone mapper and its grading rows. The shader build compiles sources marked "keep functions" without optimisation so their functions survive for the splice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 06:06:33 -05:00

85 lines
4.4 KiB
Plaintext

#version 450
//
// Copyright (c) 2025 Carlos Lopez Jr.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
// Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// HDR10 encode on the present (runtime/vk_hdr.c vk_hdr_encode): the game's picture, crop rectangle at offset, linear
// Rec. 709 with the HUD's white at 1.0 (an sRGB view decodes the 8-bit window texture), into an RGBA16F image of PQ
// code values in Rec. 2020 that FSR 1 or the blit then scale into the HDR10 swapchain. In order: the gamma correction
// (the game's sRGB-encoded look shown as a 2.2 or 2.4 display would: decode of the sRGB code with that power), the
// conversion to Rec. 2020, a gamut compression that desaturates towards the grey of the same luminance until no
// channel is negative (in a gamma-encoded space whose 50% is the scene's mid grey), the HUD's white in nits, the
// largest channel scaled down to the peak, and PQ (SMPTE ST 2084).
// p.x: nits of 1.0, p.y: the display's peak in nits, p.z: 1 when the source holds sRGB-encoded values in a UNORM
// format, p.w: gamma correction (0 none, 1: 2.2, 2: 2.4).
layout(local_size_x = 8, local_size_y = 8) in;
layout(binding = 0) uniform sampler2D src;
layout(binding = 1, rgba16f) uniform writeonly image2D dst;
layout(push_constant) uniform P {
ivec2 offset;
ivec2 size;
vec4 p;
} pc;
vec3 copy_sign(vec3 mag, vec3 s) {
vec3 a = abs(mag);
return mix(a, -a, lessThan(s, vec3(0.0)));
}
vec3 sign_pow(vec3 x, float e) { return copy_sign(pow(abs(x), vec3(e)), x); }
vec3 srgb_decode(vec3 c) {
return mix(pow((c + 0.055) / 1.055, vec3(2.4)), c / 12.92, lessThanEqual(c, vec3(0.04045)));
}
vec3 srgb_encode(vec3 c) {
return mix(1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, c * 12.92, lessThanEqual(c, vec3(0.0031308)));
}
// The colour an sRGB code shows on a display of power g, sign kept.
vec3 gamma_correct(vec3 c, float g) { return copy_sign(pow(srgb_encode(abs(c)), vec3(g)), c); }
vec3 pq(vec3 nits) {
const float m1 = 0.1593017578125, m2 = 78.84375, c1 = 0.8359375, c2 = 18.8515625, c3 = 18.6875;
vec3 y = pow(clamp(nits / 10000.0, 0.0, 1.0), vec3(m1));
return pow((c1 + c2 * y) / (1.0 + c3 * y), vec3(m2));
}
void main() {
ivec2 at = ivec2(gl_GlobalInvocationID.xy);
if (any(greaterThanEqual(at, pc.size))) return;
vec3 c = texelFetch(src, at + pc.offset, 0).rgb;
if (pc.p.z > 0.5) c = srgb_decode(clamp(c, 0.0, 1.0));
c = mix(c, vec3(0.0), isnan(c));
if (pc.p.w == 1.0) c = gamma_correct(c, 2.2);
else if (pc.p.w == 2.0) c = gamma_correct(c, 2.4);
// Rec. 709 to Rec. 2020 (rows)
c = c * mat3(0.6274038960, 0.3292830384, 0.0433130657,
0.0690972894, 0.9195403951, 0.0113623156,
0.0163914388, 0.0880133078, 0.8955952532);
float gray = dot(c, vec3(0.2627002120, 0.6779980715, 0.0593017165));
const float MID_GRAY_GAMMA = log(1.0 / pow(10.0, 0.75)) / log(0.5); // about 2.49: 18% grey encodes to 50%
vec3 encoded = sign_pow(c, 1.0 / MID_GRAY_GAMMA);
float encoded_gray = pow(max(gray, 0.0), 1.0 / MID_GRAY_GAMMA);
float lowest = min(0.0, min(encoded.r, min(encoded.g, encoded.b)));
float distance = encoded_gray - lowest;
float ratio = distance == 0.0 ? 0.0 : -lowest / distance;
c = sign_pow(mix(vec3(encoded_gray), encoded, 1.0 - ratio), MID_GRAY_GAMMA);
c *= pc.p.x;
float max_channel = max(max(max(c.r, c.g), c.b), pc.p.y);
c *= pc.p.y / max_channel;
imageStore(dst, at, vec4(pq(max(c, vec3(0.0))), 1.0));
}