From f3d1cda67206d3049a2fd518d597df394b2489be Mon Sep 17 00:00:00 2001 From: Sam Belliveau Date: Sun, 10 Mar 2024 03:25:20 -0400 Subject: [PATCH 1/3] Add PerceptualHDR --- Data/Sys/Shaders/PerceptualHDR.glsl | 92 +++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 Data/Sys/Shaders/PerceptualHDR.glsl diff --git a/Data/Sys/Shaders/PerceptualHDR.glsl b/Data/Sys/Shaders/PerceptualHDR.glsl new file mode 100644 index 0000000000..d9e5db2a1c --- /dev/null +++ b/Data/Sys/Shaders/PerceptualHDR.glsl @@ -0,0 +1,92 @@ +/* +[configuration] + +[OptionRangeFloat] +GUIName = Amplificiation +OptionName = AMPLIFICATION +MinValue = 1.0 +MaxValue = 6.0 +StepAmount = 0.25 +DefaultValue = 2.5 + +[OptionRangeFloat] +GUIName = Desaturation +OptionName = DESATURATION +MinValue = 0.0 +MaxValue = 1.0 +StepAmount = 0.1 +DefaultValue = 0.0 + +[/configuration] +*/ + +/***** Linear <--> Oklab *****/ + +const mat4 RGBtoLMS = mat4( + 0.4122214708, 0.2119034982, 0.0883024619, 0.0000000000, + 0.5363325363, 0.6806995451, 0.2817188376, 0.0000000000, + 0.0514459929, 0.1073969566, 0.6299787005, 0.0000000000, + 0.0000000000, 0.0000000000, 0.0000000000, 1.0000000000); + +const mat4 LMStoOklab = mat4( + 0.2104542553, 1.9779984951, 0.0259040371, 0.0000000000, + 0.7936177850, -2.4285922050, 0.7827717662, 0.0000000000, + -0.0040720468, 0.4505937099, -0.8086757660, 0.0000000000, + 0.0000000000, 0.0000000000, 0.0000000000, 1.0000000000); + +float4 LinearRGBToOklab(float4 c) +{ + return LMStoOklab * pow(RGBtoLMS * c, float4(1.0 / 3.0)); +} + +const mat4 OklabtoLMS = mat4( + 1.0000000000, 1.0000000000, 1.0000000000, 0.0000000000, + 0.3963377774, -0.1055613458, -0.0894841775, 0.0000000000, + 0.2158037573, -0.0638541728, -1.2914855480, 0.0000000000, + 0.0000000000, 0.0000000000, 0.0000000000, 1.0000000000); + +const mat4 LMStoRGB = mat4( + 4.0767416621, -1.2684380046, -0.0041960863, 0.0000000000, + -3.3077115913, 2.6097574011, -0.7034186147, 0.0000000000, + 0.2309699292, -0.3413193965, 1.7076147010, 0.0000000000, + 0.0000000000, 0.0000000000, 0.0000000000, 1.0000000000); + +float4 OklabToLinearRGB(float4 c) +{ + return max(LMStoRGB * pow(OklabtoLMS * c, float4(3.0)), 0.0); +} + +void main() +{ + float4 color = Sample(); + + // Nothing to do here, we are in SDR + if (!OptionEnabled(hdr_output) || !OptionEnabled(linear_space_output)) { + SetOutput(color); + return; + } + + // Renormalize Color to be in SDR Space + const float hdr_paper_white = hdr_paper_white_nits / hdr_sdr_white_nits; + color.rgb /= hdr_paper_white; + + // Convert Color to Oklab (previous conditions garuntee color is linear) + float4 oklab_color = LinearRGBToOklab(color); + + // Amount to raise hdr_paper_white to the power of. + // We divide by 3 because Oklab is a cubic space, this accounts for that. + float lum_pow = pow(oklab_color.x, 1.0) / 3.0; + float sat_pow = pow(oklab_color.x, DESATURATION) / 3.0; + + // The reason we raise hdr_paper_white to a power is so that at low + // luminosities, very little about the colors / brightnesses change. + // However at luminosities of 1.0, the colors and brightnesses are + // able to reach the full range of hdr_paper_white. + + // This is the key to PerceptualHDR working. + oklab_color.x *= pow(AMPLIFICATION, lum_pow); + oklab_color.z *= pow(AMPLIFICATION, sat_pow); + oklab_color.y *= pow(AMPLIFICATION, sat_pow); + + SetOutput(hdr_paper_white * OklabToLinearRGB(oklab_color)); +} From 153d0201a80cb5eec84239396a1d66efc04833fd Mon Sep 17 00:00:00 2001 From: Sam Belliveau Date: Sun, 10 Mar 2024 03:25:33 -0400 Subject: [PATCH 2/3] Add HDR to Metal --- Source/Core/VideoBackends/Metal/MTLMain.mm | 15 +++++++++++++++ Source/Core/VideoBackends/Metal/MTLUtil.h | 2 ++ Source/Core/VideoBackends/Metal/MTLUtil.mm | 2 ++ 3 files changed, 19 insertions(+) diff --git a/Source/Core/VideoBackends/Metal/MTLMain.mm b/Source/Core/VideoBackends/Metal/MTLMain.mm index cddb8f1c7c..56c9b3c68f 100644 --- a/Source/Core/VideoBackends/Metal/MTLMain.mm +++ b/Source/Core/VideoBackends/Metal/MTLMain.mm @@ -164,8 +164,23 @@ void Metal::VideoBackend::PrepareWindow(WindowSystemInfo& wsi) return; NSView* view = static_cast(wsi.render_surface); CAMetalLayer* layer = [CAMetalLayer layer]; + + Util::PopulateBackendInfo(&g_Config); + + if (g_Config.backend_info.bSupportsHDROutput && g_Config.bHDR) + { + [layer setWantsExtendedDynamicRangeContent:YES]; + [layer setPixelFormat:MTLPixelFormatRGBA16Float]; + + const CFStringRef name = kCGColorSpaceExtendedLinearSRGB; + CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(name); + [layer setColorspace:colorspace]; + CGColorSpaceRelease(colorspace); + } + [view setWantsLayer:YES]; [view setLayer:layer]; + wsi.render_surface = layer; #endif } diff --git a/Source/Core/VideoBackends/Metal/MTLUtil.h b/Source/Core/VideoBackends/Metal/MTLUtil.h index 28fb57cb0b..a5663b5e35 100644 --- a/Source/Core/VideoBackends/Metal/MTLUtil.h +++ b/Source/Core/VideoBackends/Metal/MTLUtil.h @@ -3,7 +3,9 @@ #pragma once +#include #include + #include #include "VideoCommon/AbstractShader.h" diff --git a/Source/Core/VideoBackends/Metal/MTLUtil.mm b/Source/Core/VideoBackends/Metal/MTLUtil.mm index c7a5c82e34..6ebe745ee6 100644 --- a/Source/Core/VideoBackends/Metal/MTLUtil.mm +++ b/Source/Core/VideoBackends/Metal/MTLUtil.mm @@ -77,6 +77,8 @@ void Metal::Util::PopulateBackendInfo(VideoConfig* config) config->backend_info.bSupportsPartialMultisampleResolve = false; config->backend_info.bSupportsDynamicVertexLoader = true; config->backend_info.bSupportsVSLinePointExpand = true; + config->backend_info.bSupportsHDROutput = + 1.0 < [[NSScreen deepestScreen] maximumPotentialExtendedDynamicRangeColorComponentValue]; } void Metal::Util::PopulateBackendInfoAdapters(VideoConfig* config, From fba333dde52320540bd5ba898e5a9ed22a6362e7 Mon Sep 17 00:00:00 2001 From: Sam Belliveau Date: Sun, 10 Mar 2024 12:52:54 -0400 Subject: [PATCH 3/3] Update PerceptualHDR with better color space --- Data/Sys/Shaders/PerceptualHDR.glsl | 121 ++++++++++++++++------------ 1 file changed, 69 insertions(+), 52 deletions(-) diff --git a/Data/Sys/Shaders/PerceptualHDR.glsl b/Data/Sys/Shaders/PerceptualHDR.glsl index d9e5db2a1c..6545992fc4 100644 --- a/Data/Sys/Shaders/PerceptualHDR.glsl +++ b/Data/Sys/Shaders/PerceptualHDR.glsl @@ -9,51 +9,63 @@ MaxValue = 6.0 StepAmount = 0.25 DefaultValue = 2.5 -[OptionRangeFloat] -GUIName = Desaturation -OptionName = DESATURATION -MinValue = 0.0 -MaxValue = 1.0 -StepAmount = 0.1 -DefaultValue = 0.0 - [/configuration] */ -/***** Linear <--> Oklab *****/ +// ICtCP Colorspace as defined by Dolby here: +// https://professional.dolby.com/siteassets/pdfs/ictcp_dolbywhitepaper_v071.pdf -const mat4 RGBtoLMS = mat4( - 0.4122214708, 0.2119034982, 0.0883024619, 0.0000000000, - 0.5363325363, 0.6806995451, 0.2817188376, 0.0000000000, - 0.0514459929, 0.1073969566, 0.6299787005, 0.0000000000, - 0.0000000000, 0.0000000000, 0.0000000000, 1.0000000000); +/***** Transfer Function *****/ -const mat4 LMStoOklab = mat4( - 0.2104542553, 1.9779984951, 0.0259040371, 0.0000000000, - 0.7936177850, -2.4285922050, 0.7827717662, 0.0000000000, - -0.0040720468, 0.4505937099, -0.8086757660, 0.0000000000, - 0.0000000000, 0.0000000000, 0.0000000000, 1.0000000000); +const float4 m_1 = float4(2610.0 / 16384.0); +const float4 m_2 = float4(128.0 * 2523.0 / 4096.0); +const float4 m_1_inv = float4(16384.0 / 2610.0); +const float4 m_2_inv = float4(4096.0 / (128.0 * 2523.0)); -float4 LinearRGBToOklab(float4 c) -{ - return LMStoOklab * pow(RGBtoLMS * c, float4(1.0 / 3.0)); +const float4 c_1 = float4(3424.0 / 4096.0); +const float4 c_2 = float4(2413.0 / 4096.0 * 32.0); +const float4 c_3 = float4(2392.0 / 4096.0 * 32.0); + +float4 EOTF_inv(float4 lms) { + float4 y = pow(lms, m_1); + return pow((c_1 + c_2 * y) / (1.0 + c_3 * y), m_2); } -const mat4 OklabtoLMS = mat4( - 1.0000000000, 1.0000000000, 1.0000000000, 0.0000000000, - 0.3963377774, -0.1055613458, -0.0894841775, 0.0000000000, - 0.2158037573, -0.0638541728, -1.2914855480, 0.0000000000, - 0.0000000000, 0.0000000000, 0.0000000000, 1.0000000000); +float4 EOTF(float4 lms) { + float4 x = pow(lms, m_2_inv); + return pow(-(x - c_1) / (c_3 * x - c_2), m_1_inv); +} -const mat4 LMStoRGB = mat4( - 4.0767416621, -1.2684380046, -0.0041960863, 0.0000000000, - -3.3077115913, 2.6097574011, -0.7034186147, 0.0000000000, - 0.2309699292, -0.3413193965, 1.7076147010, 0.0000000000, - 0.0000000000, 0.0000000000, 0.0000000000, 1.0000000000); +// This is required as scaling in EOTF space is not linear. +float EOTF_AMPLIFICATION = EOTF_inv(float4(AMPLIFICATION)).x; -float4 OklabToLinearRGB(float4 c) +/***** Linear <--> ICtCp *****/ + +const mat4 RGBtoLMS = mat4( + 1688.0, 683.0, 99.0, 0.0, + 2146.0, 2951.0, 309.0, 0.0, + 262.0, 462.0, 3688.0, 0.0, + 0.0, 0.0, 0.0, 4096.0) / 4096.0; + +const mat4 LMStoICtCp = mat4( + +2048.0, +6610.0, +17933.0, 0.0, + +2048.0, -13613.0, -17390.0, 0.0, + +0.0, +7003.0, -543.0, 0.0, + +0.0, +0.0, +0.0, 4096.0) / 4096.0; + +float4 LinearRGBToICtCP(float4 c) { - return max(LMStoRGB * pow(OklabtoLMS * c, float4(3.0)), 0.0); + return LMStoICtCp * EOTF_inv(RGBtoLMS * c); +} + +/***** ICtCp <--> Linear *****/ + +mat4 ICtCptoLMS = inverse(LMStoICtCp); +mat4 LMStoRGB = inverse(RGBtoLMS); + +float4 ICtCpToLinearRGB(float4 c) +{ + return LMStoRGB * EOTF(ICtCptoLMS * c); } void main() @@ -66,27 +78,32 @@ void main() return; } - // Renormalize Color to be in SDR Space + // Renormalize Color to be in [0.0 - 1.0] SDR Space. We will revert this later. const float hdr_paper_white = hdr_paper_white_nits / hdr_sdr_white_nits; color.rgb /= hdr_paper_white; - // Convert Color to Oklab (previous conditions garuntee color is linear) - float4 oklab_color = LinearRGBToOklab(color); + // Convert Color to Perceptual Color Space. This will allow us to do perceptual + // scaling while also being able to use the luminance channel. + float4 ictcp_color = LinearRGBToICtCP(color); - // Amount to raise hdr_paper_white to the power of. - // We divide by 3 because Oklab is a cubic space, this accounts for that. - float lum_pow = pow(oklab_color.x, 1.0) / 3.0; - float sat_pow = pow(oklab_color.x, DESATURATION) / 3.0; + // Scale the color in perceptual space depending on the percieved luminance. + // + // At low luminances, ~0.0, pow(EOTF_AMPLIFICATION, ~0.0) ~= 1.0, so the + // color will appear to be unchanged. This is important as we don't want to + // over expose dark colors which would not have otherwise been seen. + // + // At high luminances, ~1.0, pow(EOTF_AMPLIFICATION, ~1.0) ~= EOTF_AMPLIFICATION, + // which is equivilant to scaling the color by EOTF_AMPLIFICATION. This is + // important as we want to get the most out of the display, and we want to + // get bright colors to hit their target brightness. + // + // For more information, see this desmos demonstrating this scaling process: + // https://www.desmos.com/calculator/syjyrjsj5c + const float luminance = ictcp_color.x; + ictcp_color *= pow(EOTF_AMPLIFICATION, luminance); - // The reason we raise hdr_paper_white to a power is so that at low - // luminosities, very little about the colors / brightnesses change. - // However at luminosities of 1.0, the colors and brightnesses are - // able to reach the full range of hdr_paper_white. - - // This is the key to PerceptualHDR working. - oklab_color.x *= pow(AMPLIFICATION, lum_pow); - oklab_color.z *= pow(AMPLIFICATION, sat_pow); - oklab_color.y *= pow(AMPLIFICATION, sat_pow); - - SetOutput(hdr_paper_white * OklabToLinearRGB(oklab_color)); + // Convert back to Linear RGB and output the color to the display. + // We use hdr_paper_white to renormalize the color to the comfortable + // SDR viewing range. + SetOutput(hdr_paper_white * ICtCpToLinearRGB(ictcp_color)); }