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