Files
LookingGlass/client/renderers/EGL/shader/hdr.h
Geoffrey McRae 79b031f061 [client] egl: compose native PQ output in linear light
Decode PQ/BT.2020 desktop pixels into an FP16 linear scRGB target before
compositing the color cursor, ImGui, damage overlay, and letterboxing.
Encode the damaged result to PQ once at the surface boundary. Keep logical
AND/XOR cursor masks in the encoded domain.

This preserves premultiplied cursor edges, prevents encoded-domain
blending, uses the Wayland output reference white for local overlays, and
keeps guest cursor white and display calibration independent from frame
SDR white. It also corrects HDR-to-SDR absolute luminance scaling, gamut
matrices, black handling, and buffer-age repair for the extra pass.

Suppress overlays when their conversion framebuffer is unavailable rather
than allowing unconverted sRGB draws into an HDR target.

Use named transfer constants for cursor shader state. Convert clipped
surface rectangles explicitly before using the frame-damage merge helper,
which avoids mixing its unsigned type with the EGL surface rectangle type.

Only advertise native PQ when the FP16 composition target is available.
SDR and native scRGB retain their direct single-pass paths.
2026-07-19 17:49:18 +10:00

122 lines
4.0 KiB
C

/*
_______ _____ __ __ ____ __
/ ____(_)___ ___ ____ ___ ____ _ / ___// /_ ____ _____/ /__ _____ / __ \____ ______/ /__
/ / / / __ \/ _ \/ __ `__ \/ __ `/ \__ \/ __ \/ __ `/ __ / _ \/ ___/ / /_/ / __ `/ ___/ //_/
/ /___/ / / / / __/ / / / / / /_/ / ___/ / / / / /_/ / /_/ / __/ / / ____/ /_/ / /__/ ,<
\____/_/_/ /_/\___/_/ /_/ /_/\__,_/ /____/_/ /_/\__,_/\__,_/\___/_/ /_/ \__,_/\___/_/|_|
http://en.sbence.hu/ Shader: Try to get the SDR part of HDR content
*/
/**
* Translated to GLSL, original source is:
* https://github.com/VoidXH/Cinema-Shader-Pack
*/
// Configuration ---------------------------------------------------------------
const float knee = 0.75; // Compressor knee position
const float ratio = 4.0; // Compressor ratio: 1 = disabled, <1 = expander
// -----------------------------------------------------------------------------
// Precalculated values
const float compressor = 1.0 / ratio;
// PQ constants
const float m1 = 2610.0 / 16384.0;
const float m2 = 2523.0 / 32.0;
const float m1inv = 16384.0 / 2610.0;
const float m2inv = 32.0 / 2523.0;
const float c1 = 3424.0 / 4096.0;
const float c2 = 2413.0 / 128.0;
const float c3 = 2392.0 / 128.0;
const int TRANSFER_SRGB = 0;
const int TRANSFER_SCRGB = 1;
const int TRANSFER_PQ = 2;
vec3 compress(vec3 pixel)
{
pixel = max(pixel, 0.0);
float peak = max(pixel.r, max(pixel.g, pixel.b));
if (peak <= 0.0)
return vec3(0.0);
return pixel * (peak < knee ? peak :
knee + (peak - knee) * compressor) / peak;
}
// Returns luminance in nits
vec3 pq2lin(vec3 pq, float gain)
{
vec3 p = pow(max(pq, 0.0), vec3(m2inv));
vec3 d = max(p - c1, vec3(0.0)) / (c2 - c3 * p);
return pow(d, vec3(m1inv)) * gain;
}
vec3 lin2pq(vec3 linear)
{
// ST.2084 uses absolute luminance normalized to 10000 cd/m².
vec3 p = pow(max(linear, 0.0), vec3(m1));
return pow((c1 + c2 * p) / (1.0 + c3 * p), vec3(m2));
}
vec3 srgb2lin(vec3 c)
{
vec3 v = c / 12.92;
vec3 v2 = pow((c + vec3(0.055)) / 1.055, vec3(2.4));
vec3 threshold = vec3(0.04045);
vec3 result = mix(v, v2, greaterThanEqual(c, threshold));
return result;
}
vec3 lin2srgb(vec3 c)
{
vec3 v = c * 12.92;
vec3 v2 = pow(max(c, 0.0), vec3(1.0/2.4)) * 1.055 - 0.055;
vec3 threshold = vec3(0.0031308);
vec3 result = mix(v, v2, greaterThanEqual(c, threshold));
return result;
}
// in linear space
vec3 bt2020to709(vec3 bt2020)
{
return vec3(
bt2020.r * 1.6604910 + bt2020.g * -0.5876411 + bt2020.b * -0.0728499,
bt2020.r * -0.1245505 + bt2020.g * 1.1328999 + bt2020.b * -0.0083494,
bt2020.r * -0.0181508 + bt2020.g * -0.1005789 + bt2020.b * 1.1187297);
}
// in linear space
vec3 bt709to2020(vec3 bt709)
{
return vec3(
bt709.r * 0.6274039 + bt709.g * 0.3292830 + bt709.b * 0.0433131,
bt709.r * 0.0690973 + bt709.g * 0.9195404 + bt709.b * 0.0113623,
bt709.r * 0.0163914 + bt709.g * 0.0880133 + bt709.b * 0.8955953);
}
vec3 mapToSDR(vec3 color, float gain, float contentPeak, bool pq)
{
if (pq)
{
// HDR10: PQ-encoded BT.2020. Decode the absolute 10000-nit PQ range into
// values relative to the configured SDR display peak. MaxCLL constrains
// content luminance without changing that absolute unit conversion.
color = pq2lin(color.rgb, gain);
float luminance2020 =
dot(color, vec3(0.2627002, 0.6779981, 0.0593017));
if (contentPeak > 0.0 && luminance2020 > contentPeak)
color *= contentPeak / luminance2020;
color = bt2020to709(color);
}
else
{
// scRGB is linear BT.709 with 1.0 fixed at 80 nits. Use the same
// absolute-luminance mapping as PQ before compressing the highlights.
color *= gain;
float luminance709 = dot(color, vec3(0.2126390, 0.7151687, 0.0721923));
if (contentPeak > 0.0 && luminance709 > contentPeak)
color *= contentPeak / luminance709;
}
return lin2srgb(clamp(compress(color), 0.0, 1.0));
}