From aa25b0cfe34da6c6bc271e2c9fc3cf46af929a08 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Thu, 16 Jul 2026 23:03:13 +1000 Subject: [PATCH] [idd] hdr: correct the BT.709 to BT.2020 conversion --- idd/LGIdd/effect/CHDR16to10Effect.cpp | 22 ++++++++++-- obs/effect/hdrpq.effect | 52 +++++++++++++++++++++++++++ resources/{icondata.h => icondata.c} | 0 3 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 obs/effect/hdrpq.effect rename resources/{icondata.h => icondata.c} (100%) diff --git a/idd/LGIdd/effect/CHDR16to10Effect.cpp b/idd/LGIdd/effect/CHDR16to10Effect.cpp index b431009f..9acc38cb 100644 --- a/idd/LGIdd/effect/CHDR16to10Effect.cpp +++ b/idd/LGIdd/effect/CHDR16to10Effect.cpp @@ -49,8 +49,17 @@ bool CHDR16to10Effect::Init(const ComPtr& device) "void main(uint3 dt : SV_DispatchThreadID)\n" "{\n" " float3 linearValue = src[dt.xy].rgb * ReferenceWhiteNits;\n" + " // scRGB uses BT.709 primaries whereas HDR10/PQ output uses BT.2020.\n" + " // Rotate the gamut in linear light (BT.2087) BEFORE applying the PQ\n" + " // curve, otherwise the BT.709 values are later reinterpreted as BT.2020\n" + " // and saturated colours (most visibly red) are pushed outside their\n" + " // intended gamut.\n" + " float3 rec2020 = float3(\n" + " dot(linearValue, float3(0.6274039, 0.3292830, 0.0433131)),\n" + " dot(linearValue, float3(0.0690973, 0.9195404, 0.0113623)),\n" + " dot(linearValue, float3(0.0163914, 0.0880133, 0.8955953)));\n" " // scRGB to PQ (ST.2084)\n" - " float3 Y = linearValue / 10000.0;\n" + " float3 Y = rec2020 / 10000.0;\n" " float3 Ym1 = pow(max(Y, 0.0), PQ_m1);\n" " float3 pq = pow((PQ_c1 + PQ_c2 * Ym1) / (1.0 + PQ_c3 * Ym1), PQ_m2);\n" " dst[dt.xy] = float4(pq, src[dt.xy].a);\n" @@ -116,7 +125,16 @@ PostProcessStatus CHDR16to10Effect::SetFormat( // Explicitly propagate HDR static metadata so it survives post-processing. // Do not rely on the caller's copy semantics in CPostProcessor::Configure(). - memcpy(dst.displayPrimary, src.displayPrimary, sizeof(dst.displayPrimary)); + // The shader rotates the gamut from BT.709 to BT.2020, so advertise BT.2020 + // mastering primaries here rather than forwarding the source's BT.709 set + // (in 0.00002 units) - otherwise consumers see PQ/BT.2020 content tagged + // with BT.709 primaries. + dst.displayPrimary[0][0] = 35400; // Rx + dst.displayPrimary[0][1] = 14600; // Ry + dst.displayPrimary[1][0] = 8500; // Gx + dst.displayPrimary[1][1] = 39850; // Gy + dst.displayPrimary[2][0] = 6550; // Bx + dst.displayPrimary[2][1] = 2300; // By memcpy(dst.whitePoint , src.whitePoint , sizeof(dst.whitePoint )); dst.maxDisplayLuminance = src.maxDisplayLuminance; dst.minDisplayLuminance = src.minDisplayLuminance; diff --git a/obs/effect/hdrpq.effect b/obs/effect/hdrpq.effect new file mode 100644 index 00000000..9ec6e104 --- /dev/null +++ b/obs/effect/hdrpq.effect @@ -0,0 +1,52 @@ +uniform float4x4 ViewProj; +uniform texture2d image; +uniform float2 outputSize; +uniform float3 colorMatrix0; +uniform float3 colorMatrix1; +uniform float3 colorMatrix2; +uniform float scale; + +struct VertData +{ + float4 pos : POSITION; + float2 uv : TEXCOORD0; +}; + +VertData VSDefault(VertData vert_in) +{ + VertData vert_out; + vert_out.pos = mul(float4(vert_in.pos.xyz, 1.0), ViewProj); + vert_out.uv = vert_in.uv; + return vert_out; +} + +float4 PSHDRPQ(VertData vert_in) : TARGET +{ + uvec2 outputPos = uvec2(vert_in.uv * outputSize); + vec4 pixel = texelFetch(image, ivec2(outputPos), 0); + + /* SMPTE ST.2084 (PQ) EOTF: recover normalised linear light where 1.0 + * corresponds to 10000 cd/m² */ + vec3 e = pow(max(pixel.rgb, 0.0), vec3(1.0 / 78.84375)); + vec3 num = max(e - 0.8359375, 0.0); + vec3 den = 18.8515625 - 18.6875 * e; + vec3 lin = pow(num / den, vec3(1.0 / 0.1593017578125)); + + /* convert from the source display primaries to BT.709 in linear light */ + vec3 rgb709 = vec3( + dot(colorMatrix0, lin), + dot(colorMatrix1, lin), + dot(colorMatrix2, lin)); + + /* scale to the scRGB reference white convention (1.0 = 80 cd/m²) */ + return float4(rgb709 * scale, pixel.a); +} + +technique Draw +{ + pass + { + vertex_shader = VSDefault(vert_in); + pixel_shader = PSHDRPQ(vert_in); + } +} diff --git a/resources/icondata.h b/resources/icondata.c similarity index 100% rename from resources/icondata.h rename to resources/icondata.c