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// BT.2020 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// ============================================================
234
235// --- YUV to RGB matrices (mat3 + offset for BT.2020 NCL) ---
236
237static constexpr auto BT2020_YUV_MATRIX_LIMITED = R"_(
238const mat3 uYuvToRgbColorTransform = mat3(
239 1.1689, 1.1689, 1.1689,
240 0.0000, -0.1881, 2.1502,
241 1.6853, -0.6530, 0.0000
242);
243const vec3 yuvOffset = vec3(0.0625, 0.5, 0.5);
244)_";
245
246static constexpr auto BT2020_YUV_MATRIX_FULL = R"_(
247const mat3 uYuvToRgbColorTransform = mat3(
248 1.0000, 1.0000, 1.0000,
249 0.0000, -0.1646, 1.8814,
250 1.4746, -0.5714, 0.0000
251);
252const vec3 yuvOffset = vec3(0.0, 0.5, 0.5);
253)_";
254
255// --- EOTF functions ---
256
257static constexpr auto BT2020_PQ_EOTF = R"_(
258vec3 applyEotf(vec3 v) {
259 const float m1 = 2610.0 / 16384.0;
260 const float m2 = (2523.0 / 4096.0) * 128.0;
261 const float c1 = 3424.0 / 4096.0;
262 const float c2 = (2413.0 / 4096.0) * 32.0;
263 const float c3 = (2392.0 / 4096.0) * 32.0;
264 vec3 p = pow(clamp(v, 0.0, 1.0), 1.0 / vec3(m2));
265 return pow(max(p - c1, 0.0) / (c2 - c3 * p), 1.0 / vec3(m1));
266}
267)_";
268
269static constexpr auto BT2020_HLG_EOTF = R"_(
270float hlgEotfSingle(float v) {
271 const float a = 0.17883277;
272 const float b = 0.28466892;
273 const float c = 0.55991073;
274 return v <= 0.5 ? v * v / 3.0
275 : (b + exp((v - c) / a)) / 12.0;
276}
277vec3 applyEotf(vec3 v) {
278 return vec3(hlgEotfSingle(v.r), hlgEotfSingle(v.g), hlgEotfSingle(v.b));
279}
280)_";
281
282static constexpr auto BT2020_LINEAR_EOTF = R"_(
283vec3 applyEotf(vec3 v) { return v; }
284)_";
285
286static constexpr auto BT2020_GAMMA22_EOTF = R"_(
287vec3 applyEotf(vec3 v) { return pow(max(v, 0.0), vec3(2.2)); }
288)_";
289
290// --- Gamut conversion matrices ---
291
292static constexpr auto BT2020_TO_BT709_GAMUT = R"_(
293const mat3 gamutConvert = mat3(
294 1.6605, -0.1246, -0.0182,
295 -0.5876, 1.1329, -0.1006,
296 -0.0728, -0.0083, 1.1187
297);
298)_";
299
300// --- sRGB OETF ---
301
302static constexpr auto SRGB_OETF = R"_(
303vec3 srgbOetf(vec3 c) {
304 vec3 lo = c * 12.92;
305 vec3 hi = 1.055 * pow(max(c, 0.0), vec3(1.0 / 2.4)) - 0.055;
306 return mix(lo, hi, step(vec3(0.0031308), c));
307}
308)_";
309
310// ============================================================
311// BT.2020 shader generators for each OutputFormat
312// ============================================================
313
314// Helper: emit YUV matrix based on range
315static inline void bt2020_appendYuvMatrix(QString& shader, const Video::ImageFormat& d)
316{
317 if(d.color_range == AVCOL_RANGE_MPEG)
318 shader += BT2020_YUV_MATRIX_LIMITED;
319 else
320 shader += BT2020_YUV_MATRIX_FULL;
321}
322
323// Helper: emit EOTF based on transfer characteristic
324static inline void bt2020_appendEotf(QString& shader, const Video::ImageFormat& d)
325{
326 if(d.color_trc == AVCOL_TRC_SMPTE2084)
327 shader += BT2020_PQ_EOTF;
328 else if(d.color_trc == AVCOL_TRC_ARIB_STD_B67)
329 shader += BT2020_HLG_EOTF;
330 else if(d.color_trc == AVCOL_TRC_LINEAR)
331 shader += BT2020_LINEAR_EOTF;
332 else
333 shader += BT2020_GAMMA22_EOTF;
334}
335
336// Helper: get content peak luminance in nits based on transfer function
337static inline float bt2020_contentPeakNits(const Video::ImageFormat& d)
338{
339#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(61, 3, 100)
340#if __has_include(<libavutil/mastering_display_metadata.h>)
341 if(d.content_light)
342 {
343 float cll = static_cast<float>(d.content_light->MaxCLL);
344 if(cll >= 100.0f && cll <= 10000.0f)
345 return cll;
346 }
347
348 if(d.mastering_display.has_luminance
349 && d.mastering_display.max_luminance.den > 0)
350 {
351 float peak = static_cast<float>(av_q2d(d.mastering_display.max_luminance));
352 if(peak >= 100.0f && peak <= 10000.0f)
353 return peak;
354 }
355#endif
356#endif
357
358 // Defaults per transfer function
359 if(d.color_trc == AVCOL_TRC_SMPTE2084)
360 return 1000.0f;
361 else if(d.color_trc == AVCOL_TRC_ARIB_STD_B67)
362 return 1000.0f;
363 else
364 return 100.0f;
365}
366
367// Helper: for PQ, the EOTF outputs 1.0 = 10000 nits.
368// This factor converts from EOTF output space to "1.0 = content peak" space.
369static inline float bt2020_eotfToNormalizedFactor(const Video::ImageFormat& d)
370{
371 if(d.color_trc == AVCOL_TRC_SMPTE2084)
372 {
373 // PQ EOTF: 1.0 = 10000 nits. Content peak = 1000 nits (default).
374 // So multiply by 10000/1000 = 10.0 to get 1.0 = content peak.
375 return 10000.0f / bt2020_contentPeakNits(d);
376 }
377 else if(d.color_trc == AVCOL_TRC_ARIB_STD_B67)
378 {
379 // HLG EOTF: ~1.0 = reference white. Already roughly normalized.
380 return 1.0f;
381 }
382 else
383 {
384 return 1.0f;
385 }
386}
387
388// ──────────────────────────────────────────────────────────────
389// OutputFormat::Passthrough
390// YUV→RGB only. No EOTF. PQ values stay PQ-encoded, BT.2020 primaries.
391// For direct HDR10 swapchain output with no processing.
392// ──────────────────────────────────────────────────────────────
393
394static inline QString bt2020shader_passthrough(const Video::ImageFormat& d)
395{
396 QString shader;
397 shader.reserve(1024);
398
399 bt2020_appendYuvMatrix(shader, d);
400
401 shader += R"_(
402vec4 convert_to_rgb(vec4 tex) {
403 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
404 return vec4(rgb, 1.0);
405}
406)_";
407
408 return shader;
409}
410
411// ──────────────────────────────────────────────────────────────
412// OutputFormat::Linear
413// YUV→RGB → EOTF → linear BT.2020.
414// PQ: 1.0 = 10000 nits. HLG: 1.0 ≈ reference white.
415// For HDR-aware compositing in the processing graph.
416// ──────────────────────────────────────────────────────────────
417
418static inline QString bt2020shader_linear(const Video::ImageFormat& d)
419{
420 QString shader;
421 shader.reserve(2048);
422
423 bt2020_appendYuvMatrix(shader, d);
424 bt2020_appendEotf(shader, d);
425
426 shader += R"_(
427vec4 convert_to_rgb(vec4 tex) {
428 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
429 return vec4(applyEotf(rgb), 1.0);
430}
431)_";
432
433 return shader;
434}
435
436// ──────────────────────────────────────────────────────────────
437// OutputFormat::Normalized
438// YUV→RGB → EOTF → divide by peak → linear BT.2020, 1.0 = content peak.
439// Friendlier for effects expecting 0–1 range.
440// ──────────────────────────────────────────────────────────────
441
442static inline QString bt2020shader_normalized(const Video::ImageFormat& d)
443{
444 QString shader;
445 shader.reserve(2048);
446
447 bt2020_appendYuvMatrix(shader, d);
448 bt2020_appendEotf(shader, d);
449
450 const float normFactor = bt2020_eotfToNormalizedFactor(d);
451 shader += QString("const float eotfNormFactor = %1;\n").arg(normFactor, 0, 'f', 4);
452
453 shader += R"_(
454vec4 convert_to_rgb(vec4 tex) {
455 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
456 vec3 linear = applyEotf(rgb);
457 // Normalize: 1.0 = content peak luminance
458 return vec4(linear * eotfNormFactor, 1.0);
459}
460)_";
461
462 return shader;
463}
464
465// ──────────────────────────────────────────────────────────────
466// OutputFormat::SDR
467// Full HDR→SDR pipeline, uses the selected Tonemap algorithm.
468//
469// The gamut conversion order depends on the tonemapper type:
470//
471// Luminance-based tonemappers (BT.2390, BT.2446, Reinhard):
472// Operate on a luminance channel and scale RGB proportionally.
473// Gamut-agnostic, so we tonemap in BT.2020 then convert:
474// YUV→RGB → EOTF → normalize → tonemap(BT.2020) → gamut → sRGB OETF
475//
476// Per-channel tonemappers (ACES, AgX, Hable, PBR Neutral):
477// Apply curves per-channel or contain internal color-space
478// matrices that assume BT.709/sRGB input (ACES: sRGB→AP1,
479// AgX: sRGB→AgX log space). We must convert gamut first:
480// YUV→RGB → EOTF → normalize → gamut → tonemap(BT.709) → sRGB OETF
481// ──────────────────────────────────────────────────────────────
482
483static inline QString bt2020shader_sdr(const Video::ImageFormat& d)
484{
485 QString shader;
486 shader.reserve(8192);
487
488 // 1. YUV to RGB matrix
489 bt2020_appendYuvMatrix(shader, d);
490
491 // 2. EOTF
492 bt2020_appendEotf(shader, d);
493
494 // 3. Gamut conversion matrix (BT.2020 → BT.709)
495 shader += BT2020_TO_BT709_GAMUT;
496
497 // 4. sRGB OETF
498 shader += SRGB_OETF;
499
500 // 5. Normalization factor: EOTF output → "1.0 = content peak" space
501 const float normFactor = bt2020_eotfToNormalizedFactor(d);
502 shader += QString("const float eotfNormFactor = %1;\n").arg(normFactor, 0, 'f', 4);
503
504 // 6. Tone mapping function: defines vec3 tonemap(vec3)
505 // with contentPeakNits and sdrPeakNits constants.
506 const float contentPeak = bt2020_contentPeakNits(d);
507 const float sdrPeak = 203.0f; // BT.2408 reference white
508 shader += tonemapShader(d.tonemap, contentPeak, sdrPeak);
509
510 // 7. convert_to_rgb: the complete pipeline
511 // Gamut conversion order depends on tonemapper type.
512 const bool lumBased = isLuminanceBasedTonemap(d.tonemap);
513
514 if(lumBased)
515 {
516 // Luminance-based: tonemap in BT.2020, then convert gamut
517 shader += R"_(
518vec4 convert_to_rgb(vec4 tex) {
519 // Step 1: YUV decode → BT.2020 RGB
520 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
521
522 // Step 2: EOTF → linear light (BT.2020 primaries)
523 vec3 linearBt2020 = applyEotf(rgb);
524
525 // Step 3: Normalize so 1.0 = content peak
526 linearBt2020 *= eotfNormFactor;
527
528 // Step 4: Tone map in BT.2020 (luminance-based, gamut-agnostic)
529 vec3 tonemapped = tonemap(linearBt2020);
530
531 // Step 5: Gamut conversion BT.2020 → BT.709
532 vec3 linearBt709 = gamutConvert * tonemapped;
533 linearBt709 = clamp(linearBt709, 0.0, 1.0);
534
535 // Step 6: sRGB OETF for display
536 return vec4(srgbOetf(linearBt709), 1.0);
537}
538)_";
539 }
540 else
541 {
542 // Per-channel: convert gamut first, then tonemap in BT.709
543 shader += R"_(
544vec4 convert_to_rgb(vec4 tex) {
545 // Step 1: YUV decode → BT.2020 RGB
546 vec3 rgb = clamp(uYuvToRgbColorTransform * (tex.xyz - yuvOffset), 0.0, 1.0);
547
548 // Step 2: EOTF → linear light (BT.2020 primaries)
549 vec3 linearBt2020 = applyEotf(rgb);
550
551 // Step 3: Normalize so 1.0 = content peak
552 linearBt2020 *= eotfNormFactor;
553
554 // Step 4: Gamut conversion BT.2020 → BT.709 (BEFORE tonemapping)
555 vec3 linearBt709 = gamutConvert * linearBt2020;
556 // Note: do NOT clamp here — values outside [0,1] represent
557 // BT.2020-exclusive colors. The tonemapper will handle them.
558
559 // Step 5: Tone map in BT.709 (per-channel / assumes BT.709 input)
560 vec3 tonemapped = tonemap(linearBt709);
561
562 // Step 6: Clamp and sRGB OETF for display
563 return vec4(srgbOetf(clamp(tonemapped, 0.0, 1.0)), 1.0);
564}
565)_";
566 }
567
568 return shader;
569}
570
571// ============================================================
572// BT.2020 shader dispatch based on OutputFormat
573// ============================================================
574
575static inline QString bt2020shader(const Video::ImageFormat& d)
576{
577 switch(d.output_format)
578 {
579 case Video::OutputFormat::Passthrough:
580 return bt2020shader_passthrough(d);
581 case Video::OutputFormat::Linear:
582 return bt2020shader_linear(d);
583 case Video::OutputFormat::Normalized:
584 return bt2020shader_normalized(d);
585 case Video::OutputFormat::SDR:
586 default:
587 return bt2020shader_sdr(d);
588 }
589}
590
591// ============================================================
592// Helper to select limited vs full range conversion macro
593// ============================================================
594
595static inline QString colorMatrix(const Video::ImageFormat& d)
596{
597 const bool full_range = (d.color_range == AVCOL_RANGE_JPEG);
598
599 switch(d.color_space)
600 {
601 case AVCOL_SPC_RGB:
602 return "vec4 convert_to_rgb(vec4 tex) { return tex; }";
603
604 case AVCOL_SPC_BT709:
605 return full_range ? SCORE_GFX_CONVERT_BT709_FULL_TO_RGB
606 : SCORE_GFX_CONVERT_BT709_LIMITED_TO_RGB;
607
608 case AVCOL_SPC_FCC:
609 return full_range ? SCORE_GFX_CONVERT_FCC_FULL_TO_RGB
610 : SCORE_GFX_CONVERT_FCC_LIMITED_TO_RGB;
611
612 case AVCOL_SPC_BT470BG:
613 case AVCOL_SPC_SMPTE170M:
614 return full_range ? SCORE_GFX_CONVERT_BT601_FULL_TO_RGB
615 : SCORE_GFX_CONVERT_BT601_LIMITED_TO_RGB;
616
617 case AVCOL_SPC_SMPTE240M:
618 return full_range ? SCORE_GFX_CONVERT_SMPTE240M_FULL_TO_RGB
619 : SCORE_GFX_CONVERT_SMPTE240M_LIMITED_TO_RGB;
620
621 case AVCOL_SPC_YCGCO:
622 return full_range ? SCORE_GFX_CONVERT_YCGCO_FULL_TO_RGB
623 : SCORE_GFX_CONVERT_YCGCO_LIMITED_TO_RGB;
624
625 case AVCOL_SPC_BT2020_NCL:
626 case AVCOL_SPC_BT2020_CL:
627 // NOTE: BT.2020 constant luminance requires a different decoding path.
628 // In practice CL content is extremely rare; treating as NCL is a
629 // reasonable approximation.
630 case AVCOL_SPC_SMPTE2085:
631 case AVCOL_SPC_CHROMA_DERIVED_NCL:
632 case AVCOL_SPC_CHROMA_DERIVED_CL:
633 case AVCOL_SPC_ICTCP:
634 return bt2020shader(d);
635
636 default:
637 case AVCOL_SPC_NB:
638 case AVCOL_SPC_UNSPECIFIED:
639 case AVCOL_SPC_RESERVED:
640 break;
641 }
642
643 // Fallback based on resolution; assume limited range if unspecified
644 if(d.width >= 1280)
645 return full_range ? SCORE_GFX_CONVERT_BT709_FULL_TO_RGB
646 : SCORE_GFX_CONVERT_BT709_LIMITED_TO_RGB;
647 else
648 return full_range ? SCORE_GFX_CONVERT_BT601_FULL_TO_RGB
649 : SCORE_GFX_CONVERT_BT601_LIMITED_TO_RGB;
650}
651}
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:12
Definition VideoInterface.hpp:26