From 46cbd8113196ac4586de3a2ec07aa6a534232dd9 Mon Sep 17 00:00:00 2001 From: majorpainthecactus Date: Wed, 22 Dec 2021 00:20:56 +0000 Subject: [PATCH] Added HDR shader chain to be used with associated pull request 'Added support for HDR shaders #13390' in the RetroArch repository --- crt/crt-sony-pvm-4k-hdr.slangp | 78 ++++++++ crt/shaders/crt-sony-pvm-4k-hdr.slang | 253 ++++++++++++++++++++++++++ misc/hdr10.slang | 88 +++++++++ misc/inverse_tonemap.slang | 87 +++++++++ 4 files changed, 506 insertions(+) create mode 100644 crt/crt-sony-pvm-4k-hdr.slangp create mode 100644 crt/shaders/crt-sony-pvm-4k-hdr.slang create mode 100644 misc/hdr10.slang create mode 100644 misc/inverse_tonemap.slang diff --git a/crt/crt-sony-pvm-4k-hdr.slangp b/crt/crt-sony-pvm-4k-hdr.slangp new file mode 100644 index 0000000..da24770 --- /dev/null +++ b/crt/crt-sony-pvm-4k-hdr.slangp @@ -0,0 +1,78 @@ +/* +A group of shaders that tries to emulate a sony PVM type aperture grille screen but with full brightness. + +The novel thing about this group of shaders is that it transforms the image output by the 'console/arcade/computer' into HDR space first i.e brightens it first and then applies +an aperture grille afterwards which is kind of what a CRT would actually do - its kind of a kin to the electron beam (but nothing like it lol). + +My HDR 700 monitors does seem to get reasonably close to the brightness of my PVM's - its not quite there but its close. + +To use: +Please Enable HDR in RetroArch, + +You'll need a version of RetroArch post Christmas 2021 that disables the HDR chain when it detects a HDR render target in the render chain + +NOTE: when the hdr10 and inverse_tonemap shaders are envoked the Peak Luminance and Paper White Luminance in the menu *do nothing* instead set those values through the 'Shader Parameters' instead + +Set Peak Luminance to the 'Peak Luminance' of your monitor and set Paper White Luminance to roughly half dependent on the game and the monitor. + +Also try to use a integer scaling - its just better - overscaling is fine/great. + +This shader doesn't do any geometry warping or bouncing of light around inside the screen - I think these effects just add unwanted noise, I know people disagree. Please feel free to make you own and add them + +Works only with the D3D11/D3D12 drivers currently + +If taking fullscreen at 4K it currently emulates lower than a 600TVL screen - ie 3840(res) / 6(aperture grille pattern) = 640 TVL. +But 600TVL on a 4:3 TV actually means 800 vertical lines as the TVL figure relates to the screen height across the width. + +We need 8K to really start to get round the right ballpark. We need 9600 resolution to have the full 12 pixel apperture grille (800TVL * 12 (aperture grille pattern)) + +TODO: Make the horizontal scanlines more blurry in the vertical direction - we're working in HDR space at this point so its trickier than normal. +*/ + +shaders = "4" +feedback_pass = "0" + +shader0 = "../../slang-shaders/stock.slang" +filter_linear0 = false +wrap_mode0 = "clamp_to_border" +mipmap_input0 = "false" +alias0 = "StockPass" +float_framebuffer0 = "false" +srgb_framebuffer0 = "false" +scale_type_x0 = "source" +scale_x0 = "1.000000" +scale_type_y0 = "source" +scale_y0 = "1.000000" + +shader1 = "../../slang-shaders/misc/inverse_tonemap.slang" +filter_linear1 = "false" +wrap_mode1 = "clamp_to_border" +mipmap_input1 = "false" +alias1 = "" +float_framebuffer1 = "false" +srgb_framebuffer1 = "false" +scale_type_x1 = "source" +scale_x1 = "1.000000" +scale_type_y1 = "source" +scale_y1 = "1.000000" + +shader2 = "../../slang-shaders/misc/hdr10.slang" +filter_linear2 = "false" +wrap_mode2 = "clamp_to_border" +mipmap_input2 = "false" +alias2 = "" +float_framebuffer2 = "false" +srgb_framebuffer2 = "false" +scale_type_x2 = "source" +scale_x2 = "1.000000" +scale_type_y2 = "source" +scale_y2 = "1.000000" + +shader3 = "../../slang-shaders/crt/shaders/crt-sony-pvm-4k-hdr.slang" +filter_linear3 = "false" +wrap_mode3 = "clamp_to_border" +mipmap_input3 = "false" +alias3 = "" +float_framebuffer3 = "false" +srgb_framebuffer3 = "false" +ScanlineWidth = "1.000000" diff --git a/crt/shaders/crt-sony-pvm-4k-hdr.slang b/crt/shaders/crt-sony-pvm-4k-hdr.slang new file mode 100644 index 0000000..8d936e3 --- /dev/null +++ b/crt/shaders/crt-sony-pvm-4k-hdr.slang @@ -0,0 +1,253 @@ +#version 450 + +/* +A shader that tries to emulate a sony PVM type aperture grille screen but with full brightness. + +The novel thing about this shader is that it relies on the HDR shaders to brighten up the image so that when +we apply this shader which emulates the apperture grille the resulting screen isn't left too dark. + +I think you'd need a HDR 1000 monitor to get close to CRT levels of brightness but my HDR 700 does an alright job of it. + +Please Enable HDR in RetroArch NOTE: when the hdr10 and inverse_tonemap shaders are envoked the Peak Luminance and Paper White Luminance in the menu do nothing instead set those values through the shader parameters instead + +Set Peak Luminance to the peak luminance of your monitor and set Paper White Luminance to roughly half dependent on the game and the monitor. + +Also try to use a integer scaling - its just better - overscaling is fine. + +This shader doesn't do any geometry warping or bouncing of light around inside the screen etc - I think these effects just add unwanted noise, I know people disagree. Please feel free to make you own and add them + +Dont use this shader directly - use the crt\crt-sony-pvm-4k-hdr.slangp to have the proper chain of effects. +*/ + +#pragma format A2B10G10R10_UNORM_PACK32 + +layout(push_constant) uniform Push +{ + vec4 SourceSize; + vec4 OriginalSize; + vec4 OutputSize; + uint FrameCount; + float ScanlineWidth; + float ScreenWidth; + float ScreenHeight; +} params; + +/* #pragma parameter ScanlineWidth "Scanline Width" 4.0 0.0 20.0 1.0 */ +#pragma parameter ScanlineWidth "Scanline Width" 0.5 0.0 1.0 0.01 +#pragma parameter ScreenWidth "Screen Width" 3840.0 0.0 7680.0 1.0 +#pragma parameter ScreenHeight "Screen Height" 2160.0 0.0 4320.0 1.0 + +layout(std140, set = 0, binding = 0) uniform UBO +{ + mat4 MVP; +} global; + +#pragma stage vertex +layout(location = 0) in vec4 Position; +layout(location = 1) in vec2 TexCoord; +layout(location = 0) out vec2 vTexCoord; +layout(location = 1) out float Scale; + +void main() +{ + gl_Position = global.MVP * Position; + vTexCoord = TexCoord; + + vec2 ScreenSize = max(params.OutputSize.xy, vec2(params.ScreenWidth, params.ScreenHeight)); + + if((params.SourceSize.x > ScreenSize.x) || (params.SourceSize.y > ScreenSize.y)) + { + Scale = 1.0; + } + else + { + float ScaleFactor = 2.0; + + while(((params.SourceSize.x * ScaleFactor) <= ScreenSize.x) && ((params.SourceSize.y * ScaleFactor) <= ScreenSize.y)) + { + ScaleFactor += 1.0; + } + + Scale = ScaleFactor - 1.0; + } +} + +#pragma stage fragment +layout(location = 0) in vec2 vTexCoord; +layout(location = 1) in float Scale; +layout(location = 0) out vec4 FragColor; +layout(set = 0, binding = 2) uniform sampler2D Source; +layout(set = 0, binding = 3) uniform sampler2D StockPass; + +float ModInteger(float a, float b) +{ + float m = a - floor((a + 0.5) / b) * b; + return floor(m + 0.5); +} + +#define kPi 3.1415926536 +#define kEuler 2.718281828459 +#define kMax 1.0 + +#define kGuassianMin 1.1 +#define kGuassianMax 3.0 +#define Sharpness 2.5 + +float Ramp(const float gaussian, float colour) +{ + return clamp(gaussian * colour, 0.0, 1.0); +} + +void main() +{ + float ScanlineSize = params.OutputSize.y / params.SourceSize.y; + + /* vec2 InPixels = (vTexCoord * params.SourceSize.xy) * vec2(Scale); */ + const vec2 InPixels = (vTexCoord * params.OutputSize.xy); + + const float ScanlinePosition = (floor(vTexCoord.y * params.SourceSize.y) * ScanlineSize) + (ScanlineSize * 0.5); + + float ScanlineDistance = ScanlinePosition - (floor(InPixels.y) + 0.5); + + /* + if(ModInteger(floor(InPixels.y), Scale) < params.ScanlineWidth) + { + FragColor = vec4(texture(Source, vTexCoord).xyz, 1.0); + } + else + { + FragColor = vec4(0.0,0.0,0.0,1.0); + } + */ + + ScanlineDistance /= ScanlineSize * params.ScanlineWidth; + ScanlineDistance = clamp(abs(ScanlineDistance * 2.0), 0.0, 1.0); + + /* Gaussian distribution */ + const float gaussian = pow(kEuler, -0.5 * pow(ScanlineDistance/0.3, 2.0)); + + //const float guassianf = gaussian * clamp(ScanlineSize / 2, 1.2, 5.0); + //const float guassian_clamp = clamp(guassianf, 0.0, 1.0); + + float Ratio = (vTexCoord.x * params.SourceSize.x) - (floor(vTexCoord.x * params.SourceSize.x)); + Ratio = clamp(((Ratio - 0.5) * Sharpness) + 0.5, 0.0f, 1.0); + + const vec2 SourceTexCoord0 = vec2(vTexCoord.x, ScanlinePosition / params.OutputSize.y); + + vec3 hdr_colour0 = texture(Source, SourceTexCoord0).xyz; + vec3 sdr_colour0 = texture(StockPass, SourceTexCoord0).xyz; + + const vec2 SourceTexCoord1 = vec2(vTexCoord.x + (1.0 / params.SourceSize.x), ScanlinePosition / params.OutputSize.y); + + vec3 hdr_colour1 = texture(Source, SourceTexCoord1).xyz; + vec3 sdr_colour1 = texture(StockPass, SourceTexCoord1).xyz; + + vec3 hdr_colour = mix(hdr_colour0, hdr_colour1, vec3(Ratio)); + vec3 sdr_colour = mix(sdr_colour0, sdr_colour1, vec3(Ratio)); + + /* TODO: Below needs optimising - just needs a mask array rather than doing if's! Ran out of time */ + +#if 0 /* 12 pattern - 8K screens */ + float x = ModInteger(floor(InPixels.x), 12.0); + + if(x < 3.0) + { + float red = Ramp(gaussian, (sdr_colour.x * kGuassianMax) + kGuassianMin); + FragColor = vec4(red * hdr_colour.x, 0.0, 0.0, 1.0); + } + else if(x < 4.0) + { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); /* black */ + } + else if(x < 7.0) + { + float green = Ramp(gaussian, (sdr_colour.y * kGuassianMax) + kGuassianMin); + FragColor = vec4(0.0, green * hdr_colour.y, 0.0, 1.0); + } + else if(x < 8.0) + { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); /* black */ + } + else if(x < 11.0) + { + float blue = Ramp(gaussian, (sdr_colour.z * kGuassianMax) + kGuassianMin); + FragColor = vec4(0.0, 0.0, blue * hdr_colour.z, 1.0); + } + else /* if(x < 12.0) */ + { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); /* black */ + } +#endif /* 0 */ + +#if 0 /* 9 pattern - 8K screens */ + float x = ModInteger(floor(InPixels.x), 9.0); + + if(x < 2.0) + { + float red = Ramp(gaussian, (sdr_colour.x * kGuassianMax) + kGuassianMin); + FragColor = vec4(red * hdr_colour.x, 0.0, 0.0, 1.0); + } + else if(x < 3.0) + { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); /* black */ + } + else if(x < 5.0) + { + float green = Ramp(gaussian, (sdr_colour.y * kGuassianMax) + kGuassianMin); + FragColor = vec4(0.0, green * hdr_colour.y, 0.0, 1.0); + } + else if(x < 6.0) + { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); /* black */ + } + else if(x < 8.0) + { + float blue = Ramp(gaussian, (sdr_colour.z * kGuassianMax) + kGuassianMin); + FragColor = vec4(0.0, 0.0, blue * hdr_colour.z, 1.0); + } + else /* if(x < 9.0) */ + { + FragColor = vec4(0.0, 0.0, 0.0, 1.0); /* black */ + } +#endif /* 0 */ + +#if 1 /* 6 pattern - 4K screens */ + float x = ModInteger(floor(InPixels.x), 6.0); + + if(x < 2.0) + { + float red = Ramp(gaussian, (sdr_colour.x * kGuassianMax) + kGuassianMin); + FragColor = vec4(red * hdr_colour.x, 0.0, 0.0, 1.0); + } + else if(x < 4.0) + { + float green = Ramp(gaussian, (sdr_colour.y * kGuassianMax) + kGuassianMin); + FragColor = vec4(0.0, green * hdr_colour.y, 0.0, 1.0); + } + else /* if(x < 6.0) */ + { + float blue = Ramp(gaussian, (sdr_colour.z * kGuassianMax) + kGuassianMin); + FragColor = vec4(0.0, 0.0, blue * hdr_colour.z, 1.0); + } +#endif /* 1 */ + +#if 0 /* 3 pattern - 1080-1440p screens */ + float x = ModInteger(floor(InPixels.x), 3.0); + + if(x < 1.0) + { + float red = Ramp(gaussian, (sdr_colour.x * kGuassianMax) + kGuassianMin); + FragColor = vec4(red * hdr_colour.x, 0.0, 0.0, 1.0); + } + else if(x < 2.0) + { + float green = Ramp(gaussian, (sdr_colour.y * kGuassianMax) + kGuassianMin); + FragColor = vec4(0.0, green * hdr_colour.y, 0.0, 1.0); + } + else /* if(x < 3.0) */ + { + float blue = Ramp(gaussian, (sdr_colour.z * kGuassianMax) + kGuassianMin); + FragColor = vec4(0.0, 0.0, blue * hdr_colour.z, 1.0); + } +#endif /* 0 */ +} diff --git a/misc/hdr10.slang b/misc/hdr10.slang new file mode 100644 index 0000000..e616728 --- /dev/null +++ b/misc/hdr10.slang @@ -0,0 +1,88 @@ +#version 450 + +/* +Part of the crt-sony-pvm-4k-hdr shader group. Does the exact same thing as RetroArch does internally to map into HDR10 space. + +This is used to do this mapping BEFORE screen effects are applied. + +Originally part of the crt\crt-sony-pvm-4k-hdr.slangp but can be used for any shader +*/ + +#pragma format A2B10G10R10_UNORM_PACK32 + +layout(push_constant) uniform Push +{ + vec4 SourceSize; + vec4 OriginalSize; + vec4 OutputSize; + uint FrameCount; + float PaperWhiteNits; + float ExpandGamut; +} params; + +#pragma parameter PaperWhiteNits "Paper White Luminance" 400.0 0.0 10000.0 10.0 +#pragma parameter ExpandGamut "ExpandGamut" 1.0 0.0 1.0 1.0 + +layout(std140, set = 0, binding = 0) uniform UBO +{ + mat4 MVP; +} global; + +#pragma stage vertex +layout(location = 0) in vec4 Position; +layout(location = 1) in vec2 TexCoord; +layout(location = 0) out vec2 vTexCoord; + +void main() +{ + gl_Position = global.MVP * Position; + vTexCoord = TexCoord; +} + +#pragma stage fragment +layout(location = 0) in vec2 vTexCoord; +layout(location = 0) out vec4 FragColor; +layout(set = 0, binding = 2) uniform sampler2D Source; + +#define kMaxNitsFor2084 10000.0f + +const mat3 k709to2020 = mat3 ( + 0.6274040f, 0.3292820f, 0.0433136f, + 0.0690970f, 0.9195400f, 0.0113612f, + 0.0163916f, 0.0880132f, 0.8955950f); + +/* START Converted from (Copyright (c) Microsoft Corporation - Licensed under the MIT License.) https://github.com/microsoft/Xbox-ATG-Samples/tree/master/Kits/ATGTK/HDR */ +const mat3 kExpanded709to2020 = mat3 ( + 0.6274040f, 0.3292820f, 0.0433136f, + 0.0457456, 0.941777, 0.0124772, + -0.00121055, 0.0176041, 0.983607); + +vec3 LinearToST2084(vec3 normalizedLinearValue) +{ + vec3 ST2084 = pow((0.8359375f + 18.8515625f * pow(abs(normalizedLinearValue), vec3(0.1593017578f))) / (1.0f + 18.6875f * pow(abs(normalizedLinearValue), vec3(0.1593017578f))), vec3(78.84375f)); + return ST2084; /* Don't clamp between [0..1], so we can still perform operations on scene values higher than 10,000 nits */ +} +/* END Converted from (Copyright (c) Microsoft Corporation - Licensed under the MIT License.) https://github.com/microsoft/Xbox-ATG-Samples/tree/master/Kits/ATGTK/HDR */ + +vec3 Hdr10(vec3 hdr) +{ + /* Now convert into HDR10 */ + vec3 rec2020 = hdr * k709to2020; + + if(params.ExpandGamut > 0.0f) + { + rec2020 = hdr * kExpanded709to2020; + } + + vec3 linearColour = rec2020 * (params.PaperWhiteNits / kMaxNitsFor2084); + vec3 hdr10 = LinearToST2084(linearColour); + + return hdr10; +} + +void main() +{ + vec4 hdr = texture(Source, vTexCoord); + FragColor = vec4(Hdr10(hdr.rgb), hdr.a); +} + diff --git a/misc/inverse_tonemap.slang b/misc/inverse_tonemap.slang new file mode 100644 index 0000000..86b6852 --- /dev/null +++ b/misc/inverse_tonemap.slang @@ -0,0 +1,87 @@ +#version 450 + +/* +Part of the crt-sony-pvm-4k-hdr shader group. Does the exact same thing as RetroArch does internally to inverse tonemap from a SDR image to HDR. + +This is used to do this mapping BEFORE screen effects are applied. + +Originally part of the crt\crt-sony-pvm-4k-hdr.slangp but can be used for any shader +*/ + +#pragma format R16G16B16A16_SFLOAT + +layout(push_constant) uniform Push +{ + vec4 SourceSize; + vec4 OriginalSize; + vec4 OutputSize; + uint FrameCount; + float Contrast; + float PaperWhiteNits; + float MaxNits; +} params; + +#pragma parameter Contrast "Contrast" 5.0 0.0 10.0 0.01 +#pragma parameter PaperWhiteNits "Paper White Luminance" 400.0 0.0 10000.0 10.0 +#pragma parameter MaxNits "Peak Luminance" 700.0 0.0 10000.0 10.0 + +layout(std140, set = 0, binding = 0) uniform UBO +{ + mat4 MVP; +} global; + +#pragma stage vertex +layout(location = 0) in vec4 Position; +layout(location = 1) in vec2 TexCoord; +layout(location = 0) out vec2 vTexCoord; + +void main() +{ + gl_Position = global.MVP * Position; + vTexCoord = TexCoord; +} + +#pragma stage fragment +layout(location = 0) in vec2 vTexCoord; +layout(location = 0) out vec4 FragColor; +layout(set = 0, binding = 2) uniform sampler2D Source; + +#define kMaxNitsFor2084 10000.0f +#define kEpsilon 0.0001f +#define kLumaChannelRatio 0.25f + +vec3 InverseTonemap(vec3 sdr) +{ + sdr = pow(abs(sdr), vec3(params.Contrast / 2.2f)); /* Display Gamma - needs to be determined by calibration screen */ + + float luma = dot(sdr, vec3(0.2126, 0.7152, 0.0722)); /* Rec BT.709 luma coefficients - https://en.wikipedia.org/wiki/Luma_(video) */ + + /* Inverse reinhard tonemap */ + float maxValue = (params.MaxNits / params.PaperWhiteNits) + kEpsilon; + float elbow = maxValue / (maxValue - 1.0f); + float offset = 1.0f - ((0.5f * elbow) / (elbow - 0.5f)); + + float hdrLumaInvTonemap = offset + ((luma * elbow) / (elbow - luma)); + float sdrLumaInvTonemap = luma / ((1.0f + kEpsilon) - luma); /* Convert the srd < 0.5 to 0.0 -> 1.0 range */ + + float lumaInvTonemap = (luma > 0.5f) ? hdrLumaInvTonemap : sdrLumaInvTonemap; + vec3 perLuma = sdr / (luma + kEpsilon) * lumaInvTonemap; + + vec3 hdrInvTonemap = offset + ((sdr * elbow) / (elbow - sdr)); + vec3 sdrInvTonemap = sdr / ((1.0f + kEpsilon) - sdr); /* Convert the srd < 0.5 to 0.0 -> 1.0 range */ + + vec3 perChannel = vec3(sdr.x > 0.5f ? hdrInvTonemap.x : sdrInvTonemap.x, + sdr.y > 0.5f ? hdrInvTonemap.y : sdrInvTonemap.y, + sdr.z > 0.5f ? hdrInvTonemap.z : sdrInvTonemap.z); + + vec3 hdr = mix(perLuma, perChannel, vec3(kLumaChannelRatio)); + + return hdr; +} + +void main() +{ + vec4 sdr = texture(Source, vTexCoord); + FragColor = vec4(InverseTonemap(sdr.rgb), sdr.a); +} +