Loading...
Searching...
No Matches
ColorSpace.hpp
1#pragma once
2#include <Video/VideoInterface.hpp>
3#include <Gfx/Graph/decoders/Tonemap.hpp>
4
5// See softpixel.com/~cwright/programming/colorspace/yuv
6//
7// https://github.com/vlc-qt/vlc-qt/blob/master/src/qml/painter/GlPainter.cpp#L48
8//
9// All mat4 matrices are in column-major (GLSL) order.
10// Input: vec4(Y, Cb, Cr, 1.0) with values in [0, 1] normalized range.
11// The 4th column encodes the offset.
12//
13// Limited range matrices expect:
14// Y in [16/255, 235/255], Cb/Cr in [16/255, 240/255]
15// Y is scaled by 255/219, Cb/Cr by 255/224
16//
17// Full range matrices expect:
18// Y in [0, 1], Cb/Cr in [0, 1] (centered at 0.5)
19//
20// Kr/Kb coefficients used:
21// BT.601: Kr=0.299, Kb=0.114
22// BT.709: Kr=0.2126, Kb=0.0722
23// SMPTE 240M: Kr=0.2122, Kb=0.0865
24// FCC: Kr=0.30, Kb=0.11
25// BT.2020: Kr=0.2627, Kb=0.0593
26
27namespace score::gfx
28{
29
30// ============================================================
31// Identity (RGB passthrough)
32// ============================================================
33
34#define SCORE_GFX_RGB_MATRIX \
35 "mat4(\
36 1., 0., 0., 0.0,\n\
37 0., 1., 0., 0.0,\n\
38 0., 0., 1., 0.0,\n\
39 0., 0., 0., 1.0)\n"
40
41// ============================================================
42// BT.601 (Kr=0.299, Kb=0.114)
43// ============================================================
44
45#define SCORE_GFX_BT601_LIMITED_MATRIX \
46 "mat4(\n\
47 1.164383561643836, 1.164383561643836, 1.164383561643836, 0.0,\n\
48 0.000000000000000, -0.391762290094914, 2.017232142857143, 0.0,\n\
49 1.596026785714286, -0.812967647237771, 0.000000000000000, 0.0,\n\
50 -0.874202217873451, 0.531667823499146, -1.085630789302022, 1.0)\n"
51
52#define SCORE_GFX_BT601_FULL_MATRIX \
53 "mat4(\n\
54 1.000000000000000, 1.000000000000000, 1.000000000000000, 0.0,\n\
55 0.000000000000000, -0.344136286201022, 1.772000000000000, 0.0,\n\
56 1.402000000000000, -0.714136286201022, 0.000000000000000, 0.0,\n\
57 -0.701000000000000, 0.529136286201022, -0.886000000000000, 1.0)\n"
58
59// Backward compat aliases
60#define SCORE_GFX_BT601_MATRIX SCORE_GFX_BT601_LIMITED_MATRIX
61
62// ============================================================
63// BT.709 (Kr=0.2126, Kb=0.0722)
64// ============================================================
65
66#define SCORE_GFX_BT709_LIMITED_MATRIX \
67 "mat4(\n\
68 1.164383561643836, 1.164383561643836, 1.164383561643836, 0.0,\n\
69 0.000000000000000, -0.213248614273730, 2.112401785714286, 0.0,\n\
70 1.792741071428571, -0.532909328559444, 0.000000000000000, 0.0,\n\
71 -0.972945075016308, 0.301482665475862, -1.133402217873451, 1.0)\n"
72
73#define SCORE_GFX_BT709_FULL_MATRIX \
74 "mat4(\n\
75 1.000000000000000, 1.000000000000000, 1.000000000000000, 0.0,\n\
76 0.000000000000000, -0.187324272930649, 1.855600000000000, 0.0,\n\
77 1.574800000000000, -0.468124272930649, 0.000000000000000, 0.0,\n\
78 -0.787400000000000, 0.327724272930649, -0.927800000000000, 1.0)\n"
79
80// Backward compat aliases
81#define SCORE_GFX_BT709_MATRIX SCORE_GFX_BT709_LIMITED_MATRIX
82
83// ============================================================
84// SMPTE 240M (Kr=0.2122, Kb=0.0865)
85// ============================================================
86
87#define SCORE_GFX_SMPTE240M_LIMITED_MATRIX \
88 "mat4(\n\
89 1.164383561643836, 1.164383561643836, 1.164383561643836, 0.0,\n\
90 0.000000000000000, -0.256532845251675, 2.079843750000000, 0.0,\n\
91 1.793651785714286, -0.542724809537390, 0.000000000000000, 0.0,\n\
92 -0.973402217873451, 0.328136638536074, -1.117059360730594, 1.0)\n"
93
94#define SCORE_GFX_SMPTE240M_FULL_MATRIX \
95 "mat4(\n\
96 1.000000000000000, 1.000000000000000, 1.000000000000000, 0.0,\n\
97 0.000000000000000, -0.225346499358335, 1.827000000000000, 0.0,\n\
98 1.575600000000000, -0.476746499358335, 0.000000000000000, 0.0,\n\
99 -0.787800000000000, 0.351046499358335, -0.913500000000000, 1.0)\n"
100
101// ============================================================
102// FCC (Kr=0.30, Kb=0.11)
103// ============================================================
104
105#define SCORE_GFX_FCC_LIMITED_MATRIX \
106 "mat4(\n\
107 1.164383561643836, 1.164383561643836, 1.164383561643836, 0.0,\n\
108 0.000000000000000, -0.377792070217918, 2.026339285714286, 0.0,\n\
109 1.593750000000000, -0.810381355932203, 0.000000000000000, 0.0,\n\
110 -0.873059360730594, 0.523357104160448, -1.090202217873451, 1.0)\n"
111
112#define SCORE_GFX_FCC_FULL_MATRIX \
113 "mat4(\n\
114 1.000000000000000, 1.000000000000000, 1.000000000000000, 0.0,\n\
115 0.000000000000000, -0.331864406779661, 1.780000000000000, 0.0,\n\
116 1.400000000000000, -0.711864406779661, 0.000000000000000, 0.0,\n\
117 -0.700000000000000, 0.521864406779661, -0.890000000000000, 1.0)\n"
118
119// ============================================================
120// YCgCo
121// R = Y - Cg + Co, G = Y + Cg, B = Y - Cg - Co
122// (Cg and Co stored centered at 0.5)
123// ============================================================
124
125#define SCORE_GFX_YCGCO_LIMITED_MATRIX \
126 "mat4(\n\
127 1.164383561643836, 1.164383561643836, 1.164383561643836, 0.0,\n\
128 -1.138392857142857, 1.138392857142857, -1.138392857142857, 0.0,\n\
129 1.138392857142857, 0.000000000000000, -1.138392857142857, 0.0,\n\
130 -0.073059360730594, -0.644487932159165, 1.069797782126549, 1.0)\n"
131
132#define SCORE_GFX_YCGCO_FULL_MATRIX \
133 "mat4(\n\
134 1.0, 1.0, 1.0, 0.0,\n\
135 -1.0, 1.0, -1.0, 0.0,\n\
136 1.0, 0.0, -1.0, 0.0,\n\
137 0.0, -0.5, 0.5, 1.0)\n"
138
139// ============================================================
140// BT.2020 NCL (Kr=0.2627, Kb=0.0593)
141// Used by the BT.2020 HDR pipeline for the initial YUV->RGB step.
142// The full HDR path (EOTF, tonemap, gamut, OETF) is in bt2020shader().
143// ============================================================
144
145#define SCORE_GFX_BT2020_LIMITED_MATRIX \
146 "mat4(\n\
147 1.164383561643836, 1.164383561643836, 1.164383561643836, 0.0,\n\
148 0.000000000000000, -0.187326104219343, 2.141772321428571, 0.0,\n\
149 1.678674107142857, -0.650424318505057, 0.000000000000000, 0.0,\n\
150 -0.915687932159165, 0.347458498519301, -1.148145075016308, 1.0)\n"
151
152#define SCORE_GFX_BT2020_FULL_MATRIX \
153 "mat4(\n\
154 1.000000000000000, 1.000000000000000, 1.000000000000000, 0.0,\n\
155 0.000000000000000, -0.164553126843658, 1.881400000000000, 0.0,\n\
156 1.474600000000000, -0.571353126843658, 0.000000000000000, 0.0,\n\
157 -0.737300000000000, 0.367953126843658, -0.940700000000000, 1.0)\n"
158
159// ============================================================
160// Convenience macros for convert_to_rgb() function generation
161// ============================================================
162
163// --- BT.601 ---
164#define SCORE_GFX_CONVERT_BT601_LIMITED_TO_RGB \
165 "const mat4 conversion_matrix = " SCORE_GFX_BT601_LIMITED_MATRIX ";\n" \
166 "vec4 convert_to_rgb(vec4 tex) { return conversion_matrix * tex; }\n"
167
168#define SCORE_GFX_CONVERT_BT601_FULL_TO_RGB \
169 "const mat4 conversion_matrix = " SCORE_GFX_BT601_FULL_MATRIX ";\n" \
170 "vec4 convert_to_rgb(vec4 tex) { return conversion_matrix * tex; }\n"
171
172// --- BT.709 ---
173#define SCORE_GFX_CONVERT_BT709_LIMITED_TO_RGB \
174 "const mat4 conversion_matrix = " SCORE_GFX_BT709_LIMITED_MATRIX ";\n" \
175 "vec4 convert_to_rgb(vec4 tex) { return conversion_matrix * tex; }\n"
176
177#define SCORE_GFX_CONVERT_BT709_FULL_TO_RGB \
178 "const mat4 conversion_matrix = " SCORE_GFX_BT709_FULL_MATRIX ";\n" \
179 "vec4 convert_to_rgb(vec4 tex) { return conversion_matrix * tex; }\n"
180
181// --- SMPTE 240M ---
182#define SCORE_GFX_CONVERT_SMPTE240M_LIMITED_TO_RGB \
183 "const mat4 conversion_matrix = " SCORE_GFX_SMPTE240M_LIMITED_MATRIX ";\n"\
184 "vec4 convert_to_rgb(vec4 tex) { return conversion_matrix * tex; }\n"
185
186#define SCORE_GFX_CONVERT_SMPTE240M_FULL_TO_RGB \
187 "const mat4 conversion_matrix = " SCORE_GFX_SMPTE240M_FULL_MATRIX ";\n" \
188 "vec4 convert_to_rgb(vec4 tex) { return conversion_matrix * tex; }\n"
189
190// --- FCC ---
191#define SCORE_GFX_CONVERT_FCC_LIMITED_TO_RGB \
192 "const mat4 conversion_matrix = " SCORE_GFX_FCC_LIMITED_MATRIX ";\n" \
193 "vec4 convert_to_rgb(vec4 tex) { return conversion_matrix * tex; }\n"
194
195#define SCORE_GFX_CONVERT_FCC_FULL_TO_RGB \
196 "const mat4 conversion_matrix = " SCORE_GFX_FCC_FULL_MATRIX ";\n" \
197 "vec4 convert_to_rgb(vec4 tex) { return conversion_matrix * tex; }\n"
198
199// --- YCgCo ---
200#define SCORE_GFX_CONVERT_YCGCO_LIMITED_TO_RGB \
201 "const mat4 conversion_matrix = " SCORE_GFX_YCGCO_LIMITED_MATRIX ";\n" \
202 "vec4 convert_to_rgb(vec4 tex) { return conversion_matrix * tex; }\n"
203
204#define SCORE_GFX_CONVERT_YCGCO_FULL_TO_RGB \
205 "const mat4 conversion_matrix = " SCORE_GFX_YCGCO_FULL_MATRIX ";\n" \
206 "vec4 convert_to_rgb(vec4 tex) { return conversion_matrix * tex; }\n"
207
208// Backward compat aliases
209#define SCORE_GFX_CONVERT_BT601_TO_RGB SCORE_GFX_CONVERT_BT601_LIMITED_TO_RGB
210#define SCORE_GFX_CONVERT_BT709_TO_RGB SCORE_GFX_CONVERT_BT709_LIMITED_TO_RGB
211
212// ============================================================
213// BT.2020 gamut conversion matrix (for tone mapping pipeline)
214// Computed from first principles: BT.2020->XYZ->BT.709, D65 white.
215// Verified to 6 decimal places.
216// ============================================================
217
218#define SCORE_GFX_BT2020_TO_709_MATRIX \
219 "mat4(\n\
220 1.660491, -0.587641, -0.072850, 0.000000,\n\
221 -0.124550, 1.132900, -0.008349, 0.000000,\n\
222 -0.018151, -0.100579, 1.118730, 0.000000,\n\
223 0.000000, 0.000000, 0.000000, 1.000000\n\
224 )\n"
225
226#define SCORE_GFX_BT2020_MATRIX SCORE_GFX_BT709_MATRIX
227
228// ============================================================
229// Wide-gamut HDR pipeline building blocks
230//
231// These are reusable GLSL fragments for building the various
232// BT.2020 output modes (SDR, Passthrough, Linear, Normalized).
233// Also supports ICtCp (H.273 MatrixCoefficients=14),
234// SMPTE 2085 Y'D'zD'x (MatrixCoefficients=11),
235// and Display P3 input primaries.
236// ============================================================
237
238// --- YUV to RGB matrices (mat3 + offset for BT.2020 NCL) ---
239
240static constexpr auto BT2020_YUV_MATRIX_LIMITED = R"_(
241const mat3 uYuvToRgbColorTransform = mat3(
242 1.1689, 1.1689, 1.1689,
243 0.0000, -0.1881, 2.1502,
244 1.6853, -0.6530, 0.0000
245);
246const vec3 yuvOffset = vec3(0.0625, 0.5, 0.5);
247)_";
248
249static constexpr auto BT2020_YUV_MATRIX_FULL = R"_(
250const mat3 uYuvToRgbColorTransform = mat3(
251 1.0000, 1.0000, 1.0000,
252 0.0000, -0.1646, 1.8814,
253 1.4746, -0.5714, 0.0000
254);
255const vec3 yuvOffset = vec3(0.0, 0.5, 0.5);
256)_";
257
258// --- EOTF functions ---
259
260static constexpr auto BT2020_PQ_EOTF = R"_(
261vec3 applyEotf(vec3 v) {
262 const float m1 = 2610.0 / 16384.0;
263 const float m2 = (2523.0 / 4096.0) * 128.0;
264 const float c1 = 3424.0 / 4096.0;
265 const float c2 = (2413.0 / 4096.0) * 32.0;
266 const float c3 = (2392.0 / 4096.0) * 32.0;
267 vec3 p = pow(clamp(v, 0.0, 1.0), 1.0 / vec3(m2));
268 return pow(max(p - c1, 0.0) / (c2 - c3 * p), 1.0 / vec3(m1));
269}
270)_";
271
272static constexpr auto BT2020_HLG_EOTF = R"_(
273float hlgEotfSingle(float v) {
274 const float a = 0.17883277;
275 const float b = 0.28466892;
276 const float c = 0.55991073;
277 return v <= 0.5 ? v * v / 3.0
278 : (b + exp((v - c) / a)) / 12.0;
279}
280vec3 applyEotf(vec3 v) {
281 return vec3(hlgEotfSingle(v.r), hlgEotfSingle(v.g), hlgEotfSingle(v.b));
282}
283)_";
284
285static constexpr auto BT2020_LINEAR_EOTF = R"_(
286vec3 applyEotf(vec3 v) { return v; }
287)_";
288
289static constexpr auto BT2020_GAMMA22_EOTF = R"_(
290vec3 applyEotf(vec3 v) { return pow(max(v, 0.0), vec3(2.2)); }
291)_";
292
293// --- HLG OOTF (scene-linear -> display-linear) ---
294//
295// BT.2100 Section 8.3.3: the OOTF maps scene-referred light
296// to display-referred light. For HLG, this is essential because
297// the inverse OETF only gives scene-linear — the OOTF provides
298// the display rendering intent adapted to the target luminance.
299//
300// Fd = Lw * Ys^(gamma-1) * E
301// gamma = 1.2 + 0.42 * log10(Lw / 1000)
302//
303// Output is in nits (absolute luminance).
304// For Lw=1000: gamma=1.2, mild highlight boost
305// For Lw=100: gamma=0.78, dynamic range compression for SDR
306// For Lw=200: gamma=0.91, slight compression
307
308static constexpr auto BT2020_HLG_OOTF = R"_(
309vec3 applyHlgOotf(vec3 scene, float Lw) {
310 const vec3 hlgLuma = vec3(0.2627, 0.6780, 0.0593);
311 float gamma = 1.2 + 0.42 * log(Lw / 1000.0) / log(10.0);
312 float Ys = dot(hlgLuma, scene);
313 // Guard against Ys=0 (black) which would cause pow(0, negative) = inf
314 if (Ys <= 0.0) return vec3(0.0);
315 return Lw * pow(Ys, gamma - 1.0) * scene;
316}
317)_";
318
319// --- Gamut conversion matrices ---
320
321static constexpr auto BT2020_TO_BT709_GAMUT = R"_(
322const mat3 gamutConvert = mat3(
323 1.6605, -0.1246, -0.0182,
324 -0.5876, 1.1329, -0.1006,
325 -0.0728, -0.0083, 1.1187
326);
327)_";
328
329// --- sRGB OETF ---
330
331static constexpr auto SRGB_OETF = R"_(
332vec3 srgbOetf(vec3 c) {
333 vec3 lo = c * 12.92;
334 vec3 hi = 1.055 * pow(max(c, 0.0), vec3(1.0 / 2.4)) - 0.055;
335 return mix(lo, hi, step(vec3(0.0031308), c));
336}
337)_";
338
339// ============================================================
340// ICtCp decoding (H.273 MatrixCoefficients = 14)
341//
342// ICtCp is an alternative colour difference encoding defined
343// in ITU-R BT.2100. Unlike BT.2020 NCL Y'Cb'Cr', the three
344// channels are encoded in a perceptual space:
345// 1. Linear BT.2020 RGB -> linear LMS (crosstalk matrix)
346// 2. Linear LMS -> PQ-encoded LMS (or HLG-encoded)
347// 3. PQ-encoded LMS -> ICtCp (encoding matrix)
348//
349// Decoding reverses this:
350// 1. ICtCp -> PQ-encoded LMS (inverse of encoding matrix)
351// 2. PQ-encoded LMS -> linear LMS (PQ EOTF)
352// 3. Linear LMS -> linear BT.2020 RGB (inverse of crosstalk)
353//
354// Two encoding matrix variants exist (H.273 Table 4, value 14):
355// - PQ: H.273 equations 79–81
356// - HLG: H.273 equations 82–84
357//
358// References:
359// - ITU-R BT.2100-2, Section 8
360// - ITU-T H.273, Section 8.3, MatrixCoefficients = 14
361// ============================================================
362
363// ICtCp -> PQ-encoded LMS (inverse of H.273 eqs 79-81)
364static constexpr auto ICTCP_PQ_TO_LMS = R"_(
365const mat3 ictcpToLms = mat3(
366 1.000000000000000, 1.000000000000000, 1.000000000000000,
367 0.008609037037933, -0.008609037037933, 0.560031335710679,
368 0.111029625003026, -0.111029625003026, -0.320627174987319
369);
370)_";
371
372// ICtCp -> HLG-encoded LMS (inverse of H.273 eqs 82-84)
373static constexpr auto ICTCP_HLG_TO_LMS = R"_(
374const mat3 ictcpToLms = mat3(
375 1.000000000000000, 1.000000000000000, 1.000000000000000,
376 0.015718580108730, -0.015718580108730, 1.021271079842234,
377 0.209581068116406, -0.209581068116406, -0.605274490992431
378);
379)_";
380
381// Linear LMS -> linear BT.2020 RGB (inverse of H.273 eqs 14-16)
382static constexpr auto LMS_TO_BT2020_RGB = R"_(
383const mat3 lmsToBt2020 = mat3(
384 3.436606694333078, -0.791329555598929, -0.025949899690593,
385 -2.506452118656270, 1.983600451792291, -0.098913714711726,
386 0.069845424323191, -0.192270896193362, 1.124863614402319
387);
388)_";
389
390// ============================================================
391// SMPTE 2085 Y'D'zD'x decoding (H.273 MatrixCoefficients = 11)
392//
393// Y'D'zD'x is used in some Dolby Cinema content. The encoding
394// (H.273 equations 76-78) is:
395// E'_Y = E'_G
396// E'_PB = (0.986566 * E'_B − E'_Y) / 2.0 (D'z)
397// E'_PR = (0.991902 * E'_R − E'_Y) / 2.0 (D'x)
398//
399// The inverse (D'zD'x -> R'G'B') is pre-computed below.
400// Note: the OETF-encoded R', G', B' values share the same
401// transfer characteristic as BT.2020, so the same EOTF
402// pipeline applies after decoding.
403//
404// Reference: SMPTE ST 2085, ITU-T H.273 Section 8.3 value 11
405// ============================================================
406
407static constexpr auto SMPTE2085_YUV_MATRIX_LIMITED = R"_(
408// SMPTE 2085: Y'D'zD'x -> R'G'B' inverse matrix
409// Range scaling (255/219 for Y, 255/224 for D'z/D'x) baked in.
410const mat3 uYuvToRgbColorTransform = mat3(
411 1.173889720601265, 1.164383561643836, 1.180238890904243,
412 0.000000000000000, 0.000000000000000, 2.307788545607404,
413 2.295373650104259, 0.000000000000000, 0.000000000000000
414);
415const vec3 yuvOffset = vec3(0.0625, 0.5, 0.5);
416)_";
417
418static constexpr auto SMPTE2085_YUV_MATRIX_FULL = R"_(
419const mat3 uYuvToRgbColorTransform = mat3(
420 1.008164112986969, 1.000000000000000, 1.013616929835409,
421 0.000000000000000, 0.000000000000000, 2.027233859670817,
422 2.016328225973937, 0.000000000000000, 0.000000000000000
423);
424const vec3 yuvOffset = vec3(0.0, 0.5, 0.5);
425)_";
426
427// ============================================================
428// Display P3 gamut conversion matrices
429//
430// Display P3 (H.273 ColourPrimaries = 12, SMPTE EG 432-1)
431// uses the same primaries as DCI-P3 but with D65 white point.
432// These matrices convert between P3 and BT.2020/BT.709.
433//
434// Computed from CIE 1931 chromaticity coordinates via XYZ:
435// P3: R(0.680,0.320) G(0.265,0.690) B(0.150,0.060) W=D65
436// BT.709: R(0.640,0.330) G(0.300,0.600) B(0.150,0.060) W=D65
437// BT.2020:R(0.708,0.292) G(0.170,0.797) B(0.131,0.046) W=D65
438// ============================================================
439
440static constexpr auto DISPLAY_P3_TO_BT2020_GAMUT = R"_(
441const mat3 gamutConvert = mat3(
442 0.753833034361722, 0.045743848965358, -0.001210340354518,
443 0.198597369052617, 0.941777219811693, 0.017601717301090,
444 0.047569596585662, 0.012478931222948, 0.983608623053428
445);
446)_";
447
448static constexpr auto DISPLAY_P3_TO_BT709_GAMUT = R"_(
449const mat3 gamutConvert = mat3(
450 1.224940176280561, -0.042056954709688, -0.019637554590334,
451 -0.224940176280560, 1.042056954709688, -0.078636045550632,
452 0.000000000000000, 0.000000000000000, 1.098273600140966
453);
454)_";
455
456// ============================================================
457// Shader generators for each OutputFormat
458// ============================================================
459
460// Helper: resolve Auto tonemap to the best choice for this content
461static inline ::Video::Tonemap resolvedTonemap(const Video::ImageFormat& d)
462{
463 if(d.tonemap == ::Video::Tonemap::Auto)
464 return resolveAutoTonemap(static_cast<int>(d.color_trc));
465 return d.tonemap;
466}
467
468// Helper: emit YUV matrix based on range
469static inline void bt2020_appendYuvMatrix(QString& shader, const Video::ImageFormat& d)
470{
471 if(d.color_range == AVCOL_RANGE_MPEG)
472 shader += BT2020_YUV_MATRIX_LIMITED;
473 else
474 shader += BT2020_YUV_MATRIX_FULL;
475}
476
477// Helper: emit EOTF based on transfer characteristic
478static inline void bt2020_appendEotf(QString& shader, const Video::ImageFormat& d)
479{
480 if(d.color_trc == AVCOL_TRC_SMPTE2084)
481 shader += BT2020_PQ_EOTF;
482 else if(d.color_trc == AVCOL_TRC_ARIB_STD_B67)
483 shader += BT2020_HLG_EOTF;
484 else if(d.color_trc == AVCOL_TRC_LINEAR)
485 shader += BT2020_LINEAR_EOTF;
486 else
487 shader += BT2020_GAMMA22_EOTF;
488}
489
490// Helper: get content peak luminance in nits based on transfer function
491static inline float bt2020_contentPeakNits(const Video::ImageFormat& d)
492{
493#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(61, 3, 100)
494#if __has_include(<libavutil/mastering_display_metadata.h>)
495 if(d.content_light)
496 {
497 float cll = static_cast<float>(d.content_light->MaxCLL);
498 if(cll >= 100.0f && cll <= 10000.0f)
499 return cll;
500 }
501
502 if(d.mastering_display.has_luminance
503 && d.mastering_display.max_luminance.den > 0)
504 {
505 float peak = static_cast<float>(av_q2d(d.mastering_display.max_luminance));
506 if(peak >= 100.0f && peak <= 10000.0f)
507 return peak;
508 }
509#endif
510#endif
511
512 // Defaults per transfer function
513 if(d.color_trc == AVCOL_TRC_SMPTE2084)
514 return 1000.0f;
515 else if(d.color_trc == AVCOL_TRC_ARIB_STD_B67)
516 return 1000.0f;
517 else
518 return 100.0f;
519}
520
521// Helper: for PQ, the EOTF outputs 1.0 = 10000 nits.
522// This factor converts from EOTF output space to "1.0 = content peak" space.
523//
524// For HLG: after inverse OETF the signal is scene-linear [0, ~1.0].
525// The Linear/Normalized modes output scene-linear directly (OOTF is
526// deferred to the ISF shader pipeline where the user can configure
527// the display peak). Factor = 1.0.
528static inline float bt2020_eotfToNormalizedFactor(const Video::ImageFormat& d)
529{
530 if(d.color_trc == AVCOL_TRC_SMPTE2084)
531 {
532 // PQ EOTF: 1.0 = 10000 nits. Content peak = 1000 nits (default).
533 // So multiply by 10000/1000 = 10.0 to get 1.0 = content peak.
534 return 10000.0f / bt2020_contentPeakNits(d);
535 }
536 else if(d.color_trc == AVCOL_TRC_ARIB_STD_B67)
537 {
538 // HLG inverse OETF: ~1.0 = scene peak.
539 // Scene-linear output for Linear/Normalized modes.
540 return 1.0f;
541 }
542 else
543 {
544 return 1.0f;
545 }
546}
547
548// Helper: target display luminance for HLG OOTF in SDR mode.
549// BT.2408 reference white = 203 nits. For SDR displays, typical
550// peak luminance is 100-400 nits. We use sdrPeakNits as target.
551static inline float bt2020_hlgDisplayPeakNits(const Video::ImageFormat& d)
552{
553 // Use sdr peak as the display target. The OOTF's system gamma
554 // will naturally compress the dynamic range for this luminance.
555 return 203.0f;
556}
557
558// ──────────────────────────────────────────────────────────────
559// OutputFormat::Passthrough
560// YUV->RGB only. No EOTF. PQ values stay PQ-encoded, BT.2020 primaries.
561// For direct HDR10 swapchain output with no processing.
562// ──────────────────────────────────────────────────────────────
563
564static inline QString bt2020shader_passthrough(const Video::ImageFormat& d)
565{
566 QString shader;
567 shader.reserve(1024);
568
569 bt2020_appendYuvMatrix(shader, d);
570
571 shader += R"_(
572vec4 convert_to_rgb(vec4 tex) {
573 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
574 return vec4(rgb, 1.0);
575}
576)_";
577
578 return shader;
579}
580
581// ──────────────────────────────────────────────────────────────
582// OutputFormat::Linear
583// YUV->RGB -> EOTF -> linear BT.2020.
584// PQ: 1.0 = 10000 nits. HLG: 1.0 ≈ reference white.
585// For HDR-aware compositing in the processing graph.
586// ──────────────────────────────────────────────────────────────
587
588static inline QString bt2020shader_linear(const Video::ImageFormat& d)
589{
590 QString shader;
591 shader.reserve(2048);
592
593 bt2020_appendYuvMatrix(shader, d);
594 bt2020_appendEotf(shader, d);
595
596 shader += R"_(
597vec4 convert_to_rgb(vec4 tex) {
598 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
599 return vec4(applyEotf(rgb), 1.0);
600}
601)_";
602
603 return shader;
604}
605
606// ──────────────────────────────────────────────────────────────
607// OutputFormat::Normalized
608// YUV->RGB -> EOTF -> divide by peak -> linear BT.2020, 1.0 = content peak.
609// Friendlier for effects expecting 0–1 range.
610// ──────────────────────────────────────────────────────────────
611
612static inline QString bt2020shader_normalized(const Video::ImageFormat& d)
613{
614 QString shader;
615 shader.reserve(2048);
616
617 bt2020_appendYuvMatrix(shader, d);
618 bt2020_appendEotf(shader, d);
619
620 const float normFactor = bt2020_eotfToNormalizedFactor(d);
621 shader += QString("const float eotfNormFactor = %1;\n").arg(normFactor, 0, 'f', 4);
622
623 shader += R"_(
624vec4 convert_to_rgb(vec4 tex) {
625 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
626 vec3 linear = applyEotf(rgb);
627 // Normalize: 1.0 = content peak luminance
628 return vec4(linear * eotfNormFactor, 1.0);
629}
630)_";
631
632 return shader;
633}
634
635// ──────────────────────────────────────────────────────────────
636// OutputFormat::SDR
637// Full HDR->SDR pipeline, uses the selected Tonemap algorithm.
638//
639// For PQ content:
640// YUV->RGB -> PQ EOTF -> normalize(1.0=peak) -> tonemap -> gamut -> sRGB OETF
641//
642// For HLG content:
643// YUV->RGB -> HLG inv.OETF -> OOTF(Lw) -> normalize(1.0=peak) -> tonemap -> gamut -> sRGB OETF
644// The OOTF converts scene-linear to display-linear, naturally
645// adapting the rendering to the target display luminance.
646// Without it, the tonemapper receives scene-referred values
647// with the wrong peak assumption.
648//
649// The gamut conversion order depends on the tonemapper type:
650//
651// Luminance-based tonemappers (BT.2390, BT.2446, Reinhard):
652// Gamut-agnostic, so we tonemap in BT.2020 then convert:
653// … -> tonemap(BT.2020) -> gamut -> sRGB OETF
654//
655// Per-channel tonemappers (ACES, AgX, Hable, PBR Neutral):
656// Assume BT.709 input, we must convert gamut first:
657// … -> gamut -> tonemap(BT.709) -> sRGB OETF
658// ──────────────────────────────────────────────────────────────
659
660static inline QString bt2020shader_sdr(const Video::ImageFormat& d)
661{
662 QString shader;
663 shader.reserve(8192);
664
665 const bool isHLG = (d.color_trc == AVCOL_TRC_ARIB_STD_B67);
666
667 // 1. YUV to RGB matrix
668 bt2020_appendYuvMatrix(shader, d);
669
670 // 2. EOTF (inverse OETF for HLG, PQ EOTF for PQ)
671 bt2020_appendEotf(shader, d);
672
673 // 3. HLG OOTF (only for HLG content)
674 if(isHLG)
675 shader += BT2020_HLG_OOTF;
676
677 // 4. Gamut conversion matrix (BT.2020 -> BT.709)
678 shader += BT2020_TO_BT709_GAMUT;
679
680 // 5. sRGB OETF
681 shader += SRGB_OETF;
682
683 // 6. Normalization and peak luminance
684 //
685 // For PQ: EOTF output is 1.0 = 10000 nits. Normalize to 1.0 = content peak.
686 // For HLG: After OOTF, output is in nits. Normalize by content peak.
687 // Content peak for HLG = display peak Lw (the OOTF scales to [0, Lw] nits).
688 float contentPeak;
689 float normFactor;
690 if(isHLG)
691 {
692 // After OOTF(Lw), the peak is Lw nits.
693 const float hlgDisplayPeak = bt2020_hlgDisplayPeakNits(d);
694 contentPeak = hlgDisplayPeak;
695 // Normalize: divide by contentPeak to get 1.0 = peak
696 normFactor = 1.0f / contentPeak;
697 shader += QString("const float hlgDisplayLw = %1;\n").arg(hlgDisplayPeak, 0, 'f', 1);
698 }
699 else
700 {
701 contentPeak = bt2020_contentPeakNits(d);
702 normFactor = bt2020_eotfToNormalizedFactor(d);
703 }
704 shader += QString("const float eotfNormFactor = %1;\n").arg(normFactor, 0, 'f', 6);
705
706 // 7. Tone mapping function
707 const float sdrPeak = 203.0f; // BT.2408 reference white
708 const auto effectiveTonemap = resolvedTonemap(d);
709 shader += tonemapShader(effectiveTonemap, contentPeak, sdrPeak);
710
711 // 8. convert_to_rgb: the complete pipeline
712 const bool lumBased = isLuminanceBasedTonemap(effectiveTonemap);
713
714 if(isHLG)
715 {
716 // HLG-specific pipeline with OOTF
717 if(lumBased)
718 {
719 shader += R"_(
720vec4 convert_to_rgb(vec4 tex) {
721 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
722 vec3 sceneLinear = applyEotf(rgb);
723
724 // OOTF: scene-linear -> display-linear (nits)
725 vec3 displayLinear = applyHlgOotf(sceneLinear, hlgDisplayLw);
726
727 // Normalize: 1.0 = content peak
728 displayLinear *= eotfNormFactor;
729
730 vec3 tonemapped = tonemap(displayLinear);
731 vec3 linearBt709 = clamp(gamutConvert * tonemapped, 0.0, 1.0);
732 return vec4(srgbOetf(linearBt709), 1.0);
733}
734)_";
735 }
736 else
737 {
738 shader += R"_(
739vec4 convert_to_rgb(vec4 tex) {
740 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
741 vec3 sceneLinear = applyEotf(rgb);
742 vec3 displayLinear = applyHlgOotf(sceneLinear, hlgDisplayLw);
743 displayLinear *= eotfNormFactor;
744
745 vec3 linearBt709 = gamutConvert * displayLinear;
746 vec3 tonemapped = tonemap(linearBt709);
747 return vec4(srgbOetf(clamp(tonemapped, 0.0, 1.0)), 1.0);
748}
749)_";
750 }
751 }
752 else
753 {
754 // PQ and other transfer functions (original pipeline)
755 if(lumBased)
756 {
757 shader += R"_(
758vec4 convert_to_rgb(vec4 tex) {
759 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
760 vec3 linearBt2020 = applyEotf(rgb);
761 linearBt2020 *= eotfNormFactor;
762
763 vec3 tonemapped = tonemap(linearBt2020);
764 vec3 linearBt709 = clamp(gamutConvert * tonemapped, 0.0, 1.0);
765 return vec4(srgbOetf(linearBt709), 1.0);
766}
767)_";
768 }
769 else
770 {
771 shader += R"_(
772vec4 convert_to_rgb(vec4 tex) {
773 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
774 vec3 linearBt2020 = applyEotf(rgb);
775 linearBt2020 *= eotfNormFactor;
776
777 vec3 linearBt709 = gamutConvert * linearBt2020;
778 vec3 tonemapped = tonemap(linearBt709);
779 return vec4(srgbOetf(clamp(tonemapped, 0.0, 1.0)), 1.0);
780}
781)_";
782 }
783 }
784
785 return shader;
786}
787
788// ============================================================
789// BT.2020 shader dispatch based on OutputFormat
790// ============================================================
791
792static inline QString bt2020shader(const Video::ImageFormat& d)
793{
794 switch(d.output_format)
795 {
796 case Video::OutputFormat::Passthrough:
797 return bt2020shader_passthrough(d);
798 case Video::OutputFormat::Linear:
799 return bt2020shader_linear(d);
800 case Video::OutputFormat::Normalized:
801 return bt2020shader_normalized(d);
802 case Video::OutputFormat::SDR:
803 default:
804 return bt2020shader_sdr(d);
805 }
806}
807
808// ============================================================
809// ICtCp shader generators (H.273 MatrixCoefficients = 14)
810//
811// The decoding path is fundamentally different from BT.2020 NCL:
812// ICtCp -> PQ/HLG-encoded LMS -> linear LMS -> linear BT.2020 RGB
813//
814// After this, the data is in linear BT.2020 primaries and can
815// be fed into the same output pipelines (SDR tonemap, Linear,
816// Normalized, Passthrough).
817// ============================================================
818
819// Helper: emit ICtCp->LMS inverse matrix based on transfer
820static inline void ictcp_appendInverseMatrix(QString& shader, const Video::ImageFormat& d)
821{
822 if(d.color_trc == AVCOL_TRC_ARIB_STD_B67)
823 shader += ICTCP_HLG_TO_LMS;
824 else
825 shader += ICTCP_PQ_TO_LMS; // Default to PQ (most common for ICtCp)
826}
827
828// ICtCp Linear: decode to linear BT.2020 RGB
829static inline QString ictcpshader_linear(const Video::ImageFormat& d)
830{
831 QString shader;
832 shader.reserve(4096);
833
834 ictcp_appendInverseMatrix(shader, d);
835 shader += LMS_TO_BT2020_RGB;
836 bt2020_appendEotf(shader, d);
837
838 // ICtCp channels are stored with the same range encoding as YCbCr:
839 // limited range: I in [16/255,235/255], Ct/Cp in [16/255,240/255]
840 // full range: I in [0,1], Ct/Cp centered at 0.5
841 if(d.color_range == AVCOL_RANGE_MPEG)
842 {
843 shader += R"_(
844const float yScale = 255.0 / 219.0;
845const float yOffset = 16.0 / 255.0;
846const float cScale = 255.0 / 224.0;
847const float cOffset = 128.0 / 255.0;
848vec4 convert_to_rgb(vec4 tex) {
849 // Step 1: Unpack limited range ICtCp
850 float I = (tex.x - yOffset) * yScale;
851 float Ct = (tex.y - cOffset) * cScale;
852 float Cp = (tex.z - cOffset) * cScale;
853
854 // Step 2: ICtCp -> transfer-encoded LMS
855 vec3 lmsPQ = ictcpToLms * vec3(I, Ct, Cp);
856
857 // Step 3: EOTF -> linear LMS
858 vec3 lmsLinear = applyEotf(clamp(lmsPQ, 0.0, 1.0));
859
860 // Step 4: Linear LMS -> linear BT.2020 RGB
861 return vec4(lmsToBt2020 * lmsLinear, 1.0);
862}
863)_";
864 }
865 else
866 {
867 shader += R"_(
868vec4 convert_to_rgb(vec4 tex) {
869 float I = tex.x;
870 float Ct = tex.y - 0.5;
871 float Cp = tex.z - 0.5;
872
873 vec3 lmsPQ = ictcpToLms * vec3(I, Ct, Cp);
874 vec3 lmsLinear = applyEotf(clamp(lmsPQ, 0.0, 1.0));
875 return vec4(lmsToBt2020 * lmsLinear, 1.0);
876}
877)_";
878 }
879
880 return shader;
881}
882
883// ICtCp Normalized: linear BT.2020, 1.0 = content peak
884static inline QString ictcpshader_normalized(const Video::ImageFormat& d)
885{
886 QString shader;
887 shader.reserve(4096);
888
889 ictcp_appendInverseMatrix(shader, d);
890 shader += LMS_TO_BT2020_RGB;
891 bt2020_appendEotf(shader, d);
892
893 const float normFactor = bt2020_eotfToNormalizedFactor(d);
894 shader += QString("const float eotfNormFactor = %1;\n").arg(normFactor, 0, 'f', 4);
895
896 if(d.color_range == AVCOL_RANGE_MPEG)
897 {
898 shader += R"_(
899const float yScale = 255.0 / 219.0;
900const float yOffset = 16.0 / 255.0;
901const float cScale = 255.0 / 224.0;
902const float cOffset = 128.0 / 255.0;
903vec4 convert_to_rgb(vec4 tex) {
904 float I = (tex.x - yOffset) * yScale;
905 float Ct = (tex.y - cOffset) * cScale;
906 float Cp = (tex.z - cOffset) * cScale;
907 vec3 lmsPQ = ictcpToLms * vec3(I, Ct, Cp);
908 vec3 lmsLinear = applyEotf(clamp(lmsPQ, 0.0, 1.0));
909 vec3 bt2020 = lmsToBt2020 * lmsLinear;
910 return vec4(bt2020 * eotfNormFactor, 1.0);
911}
912)_";
913 }
914 else
915 {
916 shader += R"_(
917vec4 convert_to_rgb(vec4 tex) {
918 float I = tex.x;
919 float Ct = tex.y - 0.5;
920 float Cp = tex.z - 0.5;
921 vec3 lmsPQ = ictcpToLms * vec3(I, Ct, Cp);
922 vec3 lmsLinear = applyEotf(clamp(lmsPQ, 0.0, 1.0));
923 vec3 bt2020 = lmsToBt2020 * lmsLinear;
924 return vec4(bt2020 * eotfNormFactor, 1.0);
925}
926)_";
927 }
928
929 return shader;
930}
931
932// ICtCp SDR: full tonemap pipeline, output = sRGB
933static inline QString ictcpshader_sdr(const Video::ImageFormat& d)
934{
935 QString shader;
936 shader.reserve(8192);
937
938 ictcp_appendInverseMatrix(shader, d);
939 shader += LMS_TO_BT2020_RGB;
940 bt2020_appendEotf(shader, d);
941 shader += BT2020_TO_BT709_GAMUT;
942 shader += SRGB_OETF;
943
944 const float normFactor = bt2020_eotfToNormalizedFactor(d);
945 shader += QString("const float eotfNormFactor = %1;\n").arg(normFactor, 0, 'f', 4);
946
947 const float contentPeak = bt2020_contentPeakNits(d);
948 const float sdrPeak = 203.0f;
949 const auto effectiveTonemap = resolvedTonemap(d);
950 shader += tonemapShader(effectiveTonemap, contentPeak, sdrPeak);
951
952 const bool lumBased = isLuminanceBasedTonemap(effectiveTonemap);
953 const char* rangeUnpack;
954 if(d.color_range == AVCOL_RANGE_MPEG)
955 {
956 shader += R"_(
957const float yScale = 255.0 / 219.0;
958const float yOffset = 16.0 / 255.0;
959const float cScale = 255.0 / 224.0;
960const float cOffset = 128.0 / 255.0;
961)_";
962 rangeUnpack = R"_(
963 float I = (tex.x - yOffset) * yScale;
964 float Ct = (tex.y - cOffset) * cScale;
965 float Cp = (tex.z - cOffset) * cScale;
966)_";
967 }
968 else
969 {
970 rangeUnpack = R"_(
971 float I = tex.x;
972 float Ct = tex.y - 0.5;
973 float Cp = tex.z - 0.5;
974)_";
975 }
976
977 if(lumBased)
978 {
979 shader += QString(R"_(
980vec4 convert_to_rgb(vec4 tex) {
981 %1
982 vec3 lmsPQ = ictcpToLms * vec3(I, Ct, Cp);
983 vec3 lmsLinear = applyEotf(clamp(lmsPQ, 0.0, 1.0));
984 vec3 linearBt2020 = lmsToBt2020 * lmsLinear;
985 linearBt2020 *= eotfNormFactor;
986 vec3 tonemapped = tonemap(linearBt2020);
987 vec3 linearBt709 = clamp(gamutConvert * tonemapped, 0.0, 1.0);
988 return vec4(srgbOetf(linearBt709), 1.0);
989}
990)_").arg(rangeUnpack);
991 }
992 else
993 {
994 shader += QString(R"_(
995vec4 convert_to_rgb(vec4 tex) {
996 %1
997 vec3 lmsPQ = ictcpToLms * vec3(I, Ct, Cp);
998 vec3 lmsLinear = applyEotf(clamp(lmsPQ, 0.0, 1.0));
999 vec3 linearBt2020 = lmsToBt2020 * lmsLinear;
1000 linearBt2020 *= eotfNormFactor;
1001 vec3 linearBt709 = gamutConvert * linearBt2020;
1002 vec3 tonemapped = tonemap(linearBt709);
1003 return vec4(srgbOetf(clamp(tonemapped, 0.0, 1.0)), 1.0);
1004}
1005)_").arg(rangeUnpack);
1006 }
1007
1008 return shader;
1009}
1010
1011// ICtCp Passthrough: just decode ICtCp to RGB, no EOTF
1012static inline QString ictcpshader_passthrough(const Video::ImageFormat& d)
1013{
1014 QString shader;
1015 shader.reserve(2048);
1016
1017 ictcp_appendInverseMatrix(shader, d);
1018
1019 if(d.color_range == AVCOL_RANGE_MPEG)
1020 {
1021 shader += R"_(
1022const float yScale = 255.0 / 219.0;
1023const float yOffset = 16.0 / 255.0;
1024const float cScale = 255.0 / 224.0;
1025const float cOffset = 128.0 / 255.0;
1026vec4 convert_to_rgb(vec4 tex) {
1027 float I = (tex.x - yOffset) * yScale;
1028 float Ct = (tex.y - cOffset) * cScale;
1029 float Cp = (tex.z - cOffset) * cScale;
1030 // Decode to PQ-encoded LMS, output as-is (no EOTF, no LMS->RGB)
1031 vec3 lmsPQ = clamp(ictcpToLms * vec3(I, Ct, Cp), 0.0, 1.0);
1032 return vec4(lmsPQ, 1.0);
1033}
1034)_";
1035 }
1036 else
1037 {
1038 shader += R"_(
1039vec4 convert_to_rgb(vec4 tex) {
1040 float I = tex.x;
1041 float Ct = tex.y - 0.5;
1042 float Cp = tex.z - 0.5;
1043 vec3 lmsPQ = clamp(ictcpToLms * vec3(I, Ct, Cp), 0.0, 1.0);
1044 return vec4(lmsPQ, 1.0);
1045}
1046)_";
1047 }
1048
1049 return shader;
1050}
1051
1052static inline QString ictcpshader(const Video::ImageFormat& d)
1053{
1054 switch(d.output_format)
1055 {
1056 case Video::OutputFormat::Passthrough:
1057 return ictcpshader_passthrough(d);
1058 case Video::OutputFormat::Linear:
1059 return ictcpshader_linear(d);
1060 case Video::OutputFormat::Normalized:
1061 return ictcpshader_normalized(d);
1062 case Video::OutputFormat::SDR:
1063 default:
1064 return ictcpshader_sdr(d);
1065 }
1066}
1067
1068// ============================================================
1069// SMPTE 2085 Y'D'zD'x shader (H.273 MatrixCoefficients = 11)
1070//
1071// Uses the SMPTE 2085 inverse matrix instead of BT.2020 NCL,
1072// but the rest of the pipeline (EOTF, gamut, tonemap) is the
1073// same since the result is in BT.2020 primaries.
1074// ============================================================
1075
1076static inline void smpte2085_appendYuvMatrix(QString& shader, const Video::ImageFormat& d)
1077{
1078 if(d.color_range == AVCOL_RANGE_MPEG)
1079 shader += SMPTE2085_YUV_MATRIX_LIMITED;
1080 else
1081 shader += SMPTE2085_YUV_MATRIX_FULL;
1082}
1083
1084// SMPTE 2085 uses the same output pipeline as BT.2020 after YUV decode,
1085// just with a different YUV matrix. Reuse the BT.2020 pipeline structure.
1086static inline QString smpte2085shader(const Video::ImageFormat& d)
1087{
1088 // Build the same pipeline as bt2020shader, but with SMPTE 2085 YUV matrix.
1089 // For simplicity, replicate the structure for each output mode.
1090 switch(d.output_format)
1091 {
1092 case Video::OutputFormat::Passthrough:
1093 {
1094 QString shader;
1095 shader.reserve(1024);
1096 smpte2085_appendYuvMatrix(shader, d);
1097 shader += R"_(
1098vec4 convert_to_rgb(vec4 tex) {
1099 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
1100 return vec4(rgb, 1.0);
1101}
1102)_";
1103 return shader;
1104 }
1105 case Video::OutputFormat::Linear:
1106 {
1107 QString shader;
1108 shader.reserve(2048);
1109 smpte2085_appendYuvMatrix(shader, d);
1110 bt2020_appendEotf(shader, d);
1111 shader += R"_(
1112vec4 convert_to_rgb(vec4 tex) {
1113 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
1114 return vec4(applyEotf(rgb), 1.0);
1115}
1116)_";
1117 return shader;
1118 }
1119 case Video::OutputFormat::Normalized:
1120 {
1121 QString shader;
1122 shader.reserve(2048);
1123 smpte2085_appendYuvMatrix(shader, d);
1124 bt2020_appendEotf(shader, d);
1125 const float normFactor = bt2020_eotfToNormalizedFactor(d);
1126 shader += QString("const float eotfNormFactor = %1;\n").arg(normFactor, 0, 'f', 4);
1127 shader += R"_(
1128vec4 convert_to_rgb(vec4 tex) {
1129 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
1130 vec3 linear = applyEotf(rgb);
1131 return vec4(linear * eotfNormFactor, 1.0);
1132}
1133)_";
1134 return shader;
1135 }
1136 case Video::OutputFormat::SDR:
1137 default:
1138 {
1139 // Reuse the full SDR pipeline — only the YUV matrix differs.
1140 // Temporarily swap YUV matrix, then delegate to bt2020shader_sdr structure.
1141 QString shader;
1142 shader.reserve(8192);
1143 smpte2085_appendYuvMatrix(shader, d);
1144 bt2020_appendEotf(shader, d);
1145 shader += BT2020_TO_BT709_GAMUT;
1146 shader += SRGB_OETF;
1147
1148 const float normFactor = bt2020_eotfToNormalizedFactor(d);
1149 shader += QString("const float eotfNormFactor = %1;\n").arg(normFactor, 0, 'f', 4);
1150 const float contentPeak = bt2020_contentPeakNits(d);
1151 const auto effectiveTonemap = resolvedTonemap(d);
1152 shader += tonemapShader(effectiveTonemap, contentPeak, 203.0f);
1153
1154 const bool lumBased = isLuminanceBasedTonemap(effectiveTonemap);
1155 if(lumBased)
1156 {
1157 shader += R"_(
1158vec4 convert_to_rgb(vec4 tex) {
1159 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
1160 vec3 linearBt2020 = applyEotf(rgb) * eotfNormFactor;
1161 vec3 tonemapped = tonemap(linearBt2020);
1162 vec3 linearBt709 = clamp(gamutConvert * tonemapped, 0.0, 1.0);
1163 return vec4(srgbOetf(linearBt709), 1.0);
1164}
1165)_";
1166 }
1167 else
1168 {
1169 shader += R"_(
1170vec4 convert_to_rgb(vec4 tex) {
1171 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
1172 vec3 linearBt2020 = applyEotf(rgb) * eotfNormFactor;
1173 vec3 linearBt709 = gamutConvert * linearBt2020;
1174 vec3 tonemapped = tonemap(linearBt709);
1175 return vec4(srgbOetf(clamp(tonemapped, 0.0, 1.0)), 1.0);
1176}
1177)_";
1178 }
1179 return shader;
1180 }
1181 }
1182}
1183
1184// ============================================================
1185// Display P3 input shader (H.273 ColourPrimaries = 11 or 12)
1186//
1187// For content encoded with BT.709 matrix coefficients but
1188// Display P3 primaries. Converts P3 -> BT.2020 for the wide-
1189// gamut pipeline, or P3 -> BT.709 for SDR output.
1190// The YUV->RGB step uses the BT.709 matrix (since that's what
1191// the content's MatrixCoefficients says), then a gamut
1192// conversion handles the primaries mismatch.
1193// ============================================================
1194
1195static inline QString displayP3shader(const Video::ImageFormat& d)
1196{
1197 const bool full_range = (d.color_range == AVCOL_RANGE_JPEG);
1198
1199 switch(d.output_format)
1200 {
1201 case Video::OutputFormat::Linear:
1202 case Video::OutputFormat::Normalized:
1203 {
1204 // Output in BT.2020 primaries (wide-gamut preserving) for downstream shaders
1205 QString shader;
1206 shader.reserve(2048);
1207
1208 // Use BT.709 YUV matrix (content is P3 but encoded with 709 coefficients)
1209 shader += full_range ? "const mat4 conversion_matrix = " SCORE_GFX_BT709_FULL_MATRIX ";\n"
1210 : "const mat4 conversion_matrix = " SCORE_GFX_BT709_LIMITED_MATRIX ";\n";
1211
1212 shader += DISPLAY_P3_TO_BT2020_GAMUT;
1213
1214 shader += R"_(
1215vec4 convert_to_rgb(vec4 tex) {
1216 vec3 p3 = (conversion_matrix * tex).rgb;
1217 // Convert P3 linear -> BT.2020 linear (preserves full gamut)
1218 return vec4(gamutConvert * p3, 1.0);
1219}
1220)_";
1221 return shader;
1222 }
1223 case Video::OutputFormat::SDR:
1224 default:
1225 {
1226 // Convert P3 -> BT.709 for SDR display
1227 QString shader;
1228 shader.reserve(2048);
1229
1230 shader += full_range ? "const mat4 conversion_matrix = " SCORE_GFX_BT709_FULL_MATRIX ";\n"
1231 : "const mat4 conversion_matrix = " SCORE_GFX_BT709_LIMITED_MATRIX ";\n";
1232
1233 shader += DISPLAY_P3_TO_BT709_GAMUT;
1234
1235 shader += R"_(
1236vec4 convert_to_rgb(vec4 tex) {
1237 vec3 p3 = (conversion_matrix * tex).rgb;
1238 // Convert P3 -> BT.709 (may clip out-of-gamut colors)
1239 return vec4(clamp(gamutConvert * p3, 0.0, 1.0), 1.0);
1240}
1241)_";
1242 return shader;
1243 }
1244 case Video::OutputFormat::Passthrough:
1245 {
1246 // Just decode YUV, keep P3 primaries
1247 return full_range ? SCORE_GFX_CONVERT_BT709_FULL_TO_RGB
1248 : SCORE_GFX_CONVERT_BT709_LIMITED_TO_RGB;
1249 }
1250 }
1251}
1252
1253// ============================================================
1254// Chroma-derived NCL matrix coefficients lookup
1255// (H.273 MatrixCoefficients = 12)
1256//
1257// Per H.273 equations 39-44, Kr and Kb are derived from the
1258// ColourPrimaries chromaticity coordinates. We look up the
1259// actual primaries and route to the correct YUV matrix.
1260// For BT.2020 primaries this equals BT.2020 NCL (Kr=0.2627).
1261// For BT.709 primaries this equals BT.709 (Kr=0.2126).
1262// ============================================================
1263
1264static inline QString chromaDerivedNclMatrix(const Video::ImageFormat& d)
1265{
1266 const bool full_range = (d.color_range == AVCOL_RANGE_JPEG);
1267
1268 // Route based on actual color primaries
1269 switch(d.color_primaries)
1270 {
1271 case AVCOL_PRI_BT2020:
1272 // Kr=0.2627, Kb=0.0593 -> BT.2020 NCL
1273 return bt2020shader(d);
1274
1275 case AVCOL_PRI_SMPTE432: // Display P3 (D65)
1276 case AVCOL_PRI_SMPTE431: // DCI-P3
1277 // P3 primaries -> Kr≈0.2290, Kb≈0.0792
1278 // Close enough to BT.709 coefficients for YUV decoding,
1279 // but gamut is wider. Route through P3 pipeline.
1280 return displayP3shader(d);
1281
1282 case AVCOL_PRI_BT709:
1283 case AVCOL_PRI_UNSPECIFIED:
1284 default:
1285 // Kr=0.2126, Kb=0.0722 -> BT.709
1286 return full_range ? SCORE_GFX_CONVERT_BT709_FULL_TO_RGB
1287 : SCORE_GFX_CONVERT_BT709_LIMITED_TO_RGB;
1288
1289 case AVCOL_PRI_BT470BG:
1290 case AVCOL_PRI_SMPTE170M:
1291 // Kr=0.299, Kb=0.114 -> BT.601
1292 return full_range ? SCORE_GFX_CONVERT_BT601_FULL_TO_RGB
1293 : SCORE_GFX_CONVERT_BT601_LIMITED_TO_RGB;
1294
1295 case AVCOL_PRI_SMPTE240M:
1296 return full_range ? SCORE_GFX_CONVERT_SMPTE240M_FULL_TO_RGB
1297 : SCORE_GFX_CONVERT_SMPTE240M_LIMITED_TO_RGB;
1298 }
1299}
1300
1301// ============================================================
1302// Main color matrix selection
1303// ============================================================
1304
1305static inline QString colorMatrix(const Video::ImageFormat& d)
1306{
1307 const bool full_range = (d.color_range == AVCOL_RANGE_JPEG);
1308
1309 switch(d.color_space)
1310 {
1311 case AVCOL_SPC_RGB:
1312 return "vec4 convert_to_rgb(vec4 tex) { return tex; }";
1313
1314 case AVCOL_SPC_BT709:
1315 // Check if primaries indicate wider gamut than BT.709
1316 if(d.color_primaries == AVCOL_PRI_SMPTE432
1317 || d.color_primaries == AVCOL_PRI_SMPTE431)
1318 return displayP3shader(d);
1319 return full_range ? SCORE_GFX_CONVERT_BT709_FULL_TO_RGB
1320 : SCORE_GFX_CONVERT_BT709_LIMITED_TO_RGB;
1321
1322 case AVCOL_SPC_FCC:
1323 return full_range ? SCORE_GFX_CONVERT_FCC_FULL_TO_RGB
1324 : SCORE_GFX_CONVERT_FCC_LIMITED_TO_RGB;
1325
1326 case AVCOL_SPC_BT470BG:
1327 case AVCOL_SPC_SMPTE170M:
1328 return full_range ? SCORE_GFX_CONVERT_BT601_FULL_TO_RGB
1329 : SCORE_GFX_CONVERT_BT601_LIMITED_TO_RGB;
1330
1331 case AVCOL_SPC_SMPTE240M:
1332 return full_range ? SCORE_GFX_CONVERT_SMPTE240M_FULL_TO_RGB
1333 : SCORE_GFX_CONVERT_SMPTE240M_LIMITED_TO_RGB;
1334
1335 case AVCOL_SPC_YCGCO:
1336 return full_range ? SCORE_GFX_CONVERT_YCGCO_FULL_TO_RGB
1337 : SCORE_GFX_CONVERT_YCGCO_LIMITED_TO_RGB;
1338
1339 case AVCOL_SPC_BT2020_NCL:
1340 case AVCOL_SPC_BT2020_CL:
1341 // NOTE: BT.2020 constant luminance (CL) requires a different
1342 // decoding path. In practice CL content is extremely rare;
1343 // treating as NCL is a reasonable approximation.
1344 return bt2020shader(d);
1345
1346 case AVCOL_SPC_SMPTE2085:
1347 // SMPTE 2085 Y'D'zD'x — different encoding matrix,
1348 // same BT.2020 primaries and EOTF pipeline.
1349 return smpte2085shader(d);
1350
1351 case AVCOL_SPC_ICTCP:
1352 // ICtCp (BT.2100) — completely different encoding via LMS.
1353 // Separate decoding path through PQ/HLG-encoded LMS domain.
1354 return ictcpshader(d);
1355
1356 case AVCOL_SPC_CHROMA_DERIVED_NCL:
1357 // H.273 MatrixCoefficients=12: derive Kr/Kb from ColourPrimaries.
1358 return chromaDerivedNclMatrix(d);
1359
1360 case AVCOL_SPC_CHROMA_DERIVED_CL:
1361 // H.273 MatrixCoefficients=13: chroma-derived constant luminance.
1362 // Extremely rare. Fall back to NCL derivation as approximation.
1363 return chromaDerivedNclMatrix(d);
1364
1365 default:
1366 case AVCOL_SPC_NB:
1367 case AVCOL_SPC_UNSPECIFIED:
1368 case AVCOL_SPC_RESERVED:
1369 break;
1370 }
1371
1372 // Fallback: check if color_primaries indicate wide gamut
1373 if(d.color_primaries == AVCOL_PRI_BT2020)
1374 return bt2020shader(d);
1375 if(d.color_primaries == AVCOL_PRI_SMPTE432
1376 || d.color_primaries == AVCOL_PRI_SMPTE431)
1377 return displayP3shader(d);
1378
1379 // Fallback based on resolution; assume limited range if unspecified
1380 if(d.width >= 1280)
1381 return full_range ? SCORE_GFX_CONVERT_BT709_FULL_TO_RGB
1382 : SCORE_GFX_CONVERT_BT709_LIMITED_TO_RGB;
1383 else
1384 return full_range ? SCORE_GFX_CONVERT_BT601_FULL_TO_RGB
1385 : SCORE_GFX_CONVERT_BT601_LIMITED_TO_RGB;
1386}
1387}
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:12
Definition VideoInterface.hpp:26