summaryrefslogtreecommitdiffstats
path: root/src/colorspace.c
blob: 5cef2b5de61bfae0da99f3d3a3fd1de26a8587f1 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
/*
 * This file is part of libplacebo.
 *
 * libplacebo is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * libplacebo is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with libplacebo. If not, see <http://www.gnu.org/licenses/>.
 */

#include <math.h>

#include "common.h"
#include "hash.h"

#include <libplacebo/colorspace.h>
#include <libplacebo/tone_mapping.h>

bool pl_color_system_is_ycbcr_like(enum pl_color_system sys)
{
    switch (sys) {
    case PL_COLOR_SYSTEM_UNKNOWN:
    case PL_COLOR_SYSTEM_RGB:
    case PL_COLOR_SYSTEM_XYZ:
        return false;
    case PL_COLOR_SYSTEM_BT_601:
    case PL_COLOR_SYSTEM_BT_709:
    case PL_COLOR_SYSTEM_SMPTE_240M:
    case PL_COLOR_SYSTEM_BT_2020_NC:
    case PL_COLOR_SYSTEM_BT_2020_C:
    case PL_COLOR_SYSTEM_BT_2100_PQ:
    case PL_COLOR_SYSTEM_BT_2100_HLG:
    case PL_COLOR_SYSTEM_DOLBYVISION:
    case PL_COLOR_SYSTEM_YCGCO:
        return true;
    case PL_COLOR_SYSTEM_COUNT: break;
    };

    pl_unreachable();
}

bool pl_color_system_is_linear(enum pl_color_system sys)
{
    switch (sys) {
    case PL_COLOR_SYSTEM_UNKNOWN:
    case PL_COLOR_SYSTEM_RGB:
    case PL_COLOR_SYSTEM_BT_601:
    case PL_COLOR_SYSTEM_BT_709:
    case PL_COLOR_SYSTEM_SMPTE_240M:
    case PL_COLOR_SYSTEM_BT_2020_NC:
    case PL_COLOR_SYSTEM_YCGCO:
        return true;
    case PL_COLOR_SYSTEM_BT_2020_C:
    case PL_COLOR_SYSTEM_BT_2100_PQ:
    case PL_COLOR_SYSTEM_BT_2100_HLG:
    case PL_COLOR_SYSTEM_DOLBYVISION:
    case PL_COLOR_SYSTEM_XYZ:
        return false;
    case PL_COLOR_SYSTEM_COUNT: break;
    };

    pl_unreachable();
}

enum pl_color_system pl_color_system_guess_ycbcr(int width, int height)
{
    if (width >= 1280 || height > 576) {
        // Typical HD content
        return PL_COLOR_SYSTEM_BT_709;
    } else {
        // Typical SD content
        return PL_COLOR_SYSTEM_BT_601;
    }
}

bool pl_bit_encoding_equal(const struct pl_bit_encoding *b1,
                           const struct pl_bit_encoding *b2)
{
    return b1->sample_depth == b2->sample_depth &&
           b1->color_depth  == b2->color_depth &&
           b1->bit_shift    == b2->bit_shift;
}

const struct pl_color_repr pl_color_repr_unknown = {0};

const struct pl_color_repr pl_color_repr_rgb = {
    .sys    = PL_COLOR_SYSTEM_RGB,
    .levels = PL_COLOR_LEVELS_FULL,
};

const struct pl_color_repr pl_color_repr_sdtv = {
    .sys    = PL_COLOR_SYSTEM_BT_601,
    .levels = PL_COLOR_LEVELS_LIMITED,
};

const struct pl_color_repr pl_color_repr_hdtv = {
    .sys    = PL_COLOR_SYSTEM_BT_709,
    .levels = PL_COLOR_LEVELS_LIMITED,
};

const struct pl_color_repr pl_color_repr_uhdtv = {
    .sys    = PL_COLOR_SYSTEM_BT_2020_NC,
    .levels = PL_COLOR_LEVELS_LIMITED,
};

const struct pl_color_repr pl_color_repr_jpeg = {
    .sys    = PL_COLOR_SYSTEM_BT_601,
    .levels = PL_COLOR_LEVELS_FULL,
};

bool pl_color_repr_equal(const struct pl_color_repr *c1,
                         const struct pl_color_repr *c2)
{
    return c1->sys    == c2->sys &&
           c1->levels == c2->levels &&
           c1->alpha  == c2->alpha &&
           c1->dovi   == c2->dovi &&
           pl_bit_encoding_equal(&c1->bits, &c2->bits);
}

static struct pl_bit_encoding pl_bit_encoding_merge(const struct pl_bit_encoding *orig,
                                                    const struct pl_bit_encoding *new)
{
    return (struct pl_bit_encoding) {
        .sample_depth = PL_DEF(orig->sample_depth, new->sample_depth),
        .color_depth  = PL_DEF(orig->color_depth,  new->color_depth),
        .bit_shift    = PL_DEF(orig->bit_shift,    new->bit_shift),
    };
}

void pl_color_repr_merge(struct pl_color_repr *orig, const struct pl_color_repr *new)
{
    *orig = (struct pl_color_repr) {
        .sys    = PL_DEF(orig->sys,    new->sys),
        .levels = PL_DEF(orig->levels, new->levels),
        .alpha  = PL_DEF(orig->alpha,  new->alpha),
        .dovi   = PL_DEF(orig->dovi,   new->dovi),
        .bits   = pl_bit_encoding_merge(&orig->bits, &new->bits),
    };
}

enum pl_color_levels pl_color_levels_guess(const struct pl_color_repr *repr)
{
    if (repr->sys == PL_COLOR_SYSTEM_DOLBYVISION)
        return PL_COLOR_LEVELS_FULL;

    if (repr->levels)
        return repr->levels;

    return pl_color_system_is_ycbcr_like(repr->sys)
                ? PL_COLOR_LEVELS_LIMITED
                : PL_COLOR_LEVELS_FULL;
}

float pl_color_repr_normalize(struct pl_color_repr *repr)
{
    float scale = 1.0;
    struct pl_bit_encoding *bits = &repr->bits;

    if (bits->bit_shift) {
        scale /= (1LL << bits->bit_shift);
        bits->bit_shift = 0;
    }

    // If one of these is set but not the other, use the set one
    int tex_bits = PL_DEF(bits->sample_depth, 8);
    int col_bits = PL_DEF(bits->color_depth, tex_bits);
    tex_bits = PL_DEF(tex_bits, col_bits);

    if (pl_color_levels_guess(repr) == PL_COLOR_LEVELS_LIMITED) {
        // Limit range is always shifted directly
        scale *= (float) (1LL << tex_bits) / (1LL << col_bits);
    } else {
        // Full range always uses the full range available
        scale *= ((1LL << tex_bits) - 1.) / ((1LL << col_bits) - 1.);
    }

    bits->color_depth = bits->sample_depth;
    return scale;
}

bool pl_color_primaries_is_wide_gamut(enum pl_color_primaries prim)
{
    switch (prim) {
    case PL_COLOR_PRIM_UNKNOWN:
    case PL_COLOR_PRIM_BT_601_525:
    case PL_COLOR_PRIM_BT_601_625:
    case PL_COLOR_PRIM_BT_709:
    case PL_COLOR_PRIM_BT_470M:
    case PL_COLOR_PRIM_EBU_3213:
        return false;
    case PL_COLOR_PRIM_BT_2020:
    case PL_COLOR_PRIM_APPLE:
    case PL_COLOR_PRIM_ADOBE:
    case PL_COLOR_PRIM_PRO_PHOTO:
    case PL_COLOR_PRIM_CIE_1931:
    case PL_COLOR_PRIM_DCI_P3:
    case PL_COLOR_PRIM_DISPLAY_P3:
    case PL_COLOR_PRIM_V_GAMUT:
    case PL_COLOR_PRIM_S_GAMUT:
    case PL_COLOR_PRIM_FILM_C:
    case PL_COLOR_PRIM_ACES_AP0:
    case PL_COLOR_PRIM_ACES_AP1:
        return true;
    case PL_COLOR_PRIM_COUNT: break;
    }

    pl_unreachable();
}

enum pl_color_primaries pl_color_primaries_guess(int width, int height)
{
    // HD content
    if (width >= 1280 || height > 576)
        return PL_COLOR_PRIM_BT_709;

    switch (height) {
    case 576: // Typical PAL content, including anamorphic/squared
        return PL_COLOR_PRIM_BT_601_625;

    case 480: // Typical NTSC content, including squared
    case 486: // NTSC Pro or anamorphic NTSC
        return PL_COLOR_PRIM_BT_601_525;

    default: // No good metric, just pick BT.709 to minimize damage
        return PL_COLOR_PRIM_BT_709;
    }
}

// HLG 75% value (scene-referred)
#define HLG_75 3.17955

float pl_color_transfer_nominal_peak(enum pl_color_transfer trc)
{
    switch (trc) {
    case PL_COLOR_TRC_UNKNOWN:
    case PL_COLOR_TRC_BT_1886:
    case PL_COLOR_TRC_SRGB:
    case PL_COLOR_TRC_LINEAR:
    case PL_COLOR_TRC_GAMMA18:
    case PL_COLOR_TRC_GAMMA20:
    case PL_COLOR_TRC_GAMMA22:
    case PL_COLOR_TRC_GAMMA24:
    case PL_COLOR_TRC_GAMMA26:
    case PL_COLOR_TRC_GAMMA28:
    case PL_COLOR_TRC_PRO_PHOTO:
    case PL_COLOR_TRC_ST428:
        return 1.0;
    case PL_COLOR_TRC_PQ:       return 10000.0 / PL_COLOR_SDR_WHITE;
    case PL_COLOR_TRC_HLG:      return 12.0 / HLG_75;
    case PL_COLOR_TRC_V_LOG:    return 46.0855;
    case PL_COLOR_TRC_S_LOG1:   return 6.52;
    case PL_COLOR_TRC_S_LOG2:   return 9.212;
    case PL_COLOR_TRC_COUNT: break;
    }

    pl_unreachable();
}

const struct pl_hdr_metadata pl_hdr_metadata_empty = {0};
const struct pl_hdr_metadata pl_hdr_metadata_hdr10 ={
    .prim = {
        .red   = {0.708,    0.292},
        .green = {0.170,    0.797},
        .blue  = {0.131,    0.046},
        .white = {0.31271,  0.32902},
    },
    .min_luma = 0,
    .max_luma = 10000,
    .max_cll  = 10000,
    .max_fall = 0, // unknown
};

static const float PQ_M1 = 2610./4096 * 1./4,
                   PQ_M2 = 2523./4096 * 128,
                   PQ_C1 = 3424./4096,
                   PQ_C2 = 2413./4096 * 32,
                   PQ_C3 = 2392./4096 * 32;

float pl_hdr_rescale(enum pl_hdr_scaling from, enum pl_hdr_scaling to, float x)
{
    if (from == to)
        return x;
    if (!x) // micro-optimization for common value
        return x;

    x = fmaxf(x, 0.0f);

    // Convert input to PL_SCALE_RELATIVE
    switch (from) {
    case PL_HDR_PQ:
        x = powf(x, 1.0f / PQ_M2);
        x = fmaxf(x - PQ_C1, 0.0f) / (PQ_C2 - PQ_C3 * x);
        x = powf(x, 1.0f / PQ_M1);
        x *= 10000.0f;
        // fall through
    case PL_HDR_NITS:
        x /= PL_COLOR_SDR_WHITE;
        // fall through
    case PL_HDR_NORM:
        goto output;
    case PL_HDR_SQRT:
        x *= x;
        goto output;
    case PL_HDR_SCALING_COUNT:
        break;
    }

    pl_unreachable();

output:
    // Convert PL_SCALE_RELATIVE to output
    switch (to) {
    case PL_HDR_NORM:
        return x;
    case PL_HDR_SQRT:
        return sqrtf(x);
    case PL_HDR_NITS:
        return x * PL_COLOR_SDR_WHITE;
    case PL_HDR_PQ:
        x *= PL_COLOR_SDR_WHITE / 10000.0f;
        x = powf(x, PQ_M1);
        x = (PQ_C1 + PQ_C2 * x) / (1.0f + PQ_C3 * x);
        x = powf(x, PQ_M2);
        return x;
    case PL_HDR_SCALING_COUNT:
        break;
    }

    pl_unreachable();
}

static inline bool pl_hdr_bezier_equal(const struct pl_hdr_bezier *a,
                                       const struct pl_hdr_bezier *b)
{
    return a->target_luma == b->target_luma &&
           a->knee_x      == b->knee_x &&
           a->knee_y      == b->knee_y &&
           a->num_anchors == b->num_anchors &&
           !memcmp(a->anchors, b->anchors, sizeof(a->anchors[0]) * a->num_anchors);
}

bool pl_hdr_metadata_equal(const struct pl_hdr_metadata *a,
                           const struct pl_hdr_metadata *b)
{
    return pl_raw_primaries_equal(&a->prim, &b->prim) &&
           a->min_luma == b->min_luma &&
           a->max_luma == b->max_luma &&
           a->max_cll  == b->max_cll  &&
           a->max_fall == b->max_fall &&
           a->scene_max[0] == b->scene_max[0] &&
           a->scene_max[1] == b->scene_max[1] &&
           a->scene_max[2] == b->scene_max[2] &&
           a->scene_avg == b->scene_avg &&
           pl_hdr_bezier_equal(&a->ootf, &b->ootf) &&
           a->max_pq_y == b->max_pq_y &&
           a->avg_pq_y == b->avg_pq_y;
}

void pl_hdr_metadata_merge(struct pl_hdr_metadata *orig,
                           const struct pl_hdr_metadata *update)
{
    pl_raw_primaries_merge(&orig->prim, &update->prim);
    if (!orig->min_luma)
        orig->min_luma = update->min_luma;
    if (!orig->max_luma)
        orig->max_luma = update->max_luma;
    if (!orig->max_cll)
        orig->max_cll = update->max_cll;
    if (!orig->max_fall)
        orig->max_fall = update->max_fall;
    if (!orig->scene_max[1])
        memcpy(orig->scene_max, update->scene_max, sizeof(orig->scene_max));
    if (!orig->scene_avg)
        orig->scene_avg = update->scene_avg;
    if (!orig->ootf.target_luma)
        orig->ootf = update->ootf;
    if (!orig->max_pq_y)
        orig->max_pq_y = update->max_pq_y;
    if (!orig->avg_pq_y)
        orig->avg_pq_y = update->avg_pq_y;
}

bool pl_hdr_metadata_contains(const struct pl_hdr_metadata *data,
                              enum pl_hdr_metadata_type type)
{
    bool has_hdr10 = data->max_luma;
    bool has_hdr10plus = data->scene_avg && (data->scene_max[0] ||
                                             data->scene_max[1] ||
                                             data->scene_max[2]);
    bool has_cie_y = data->max_pq_y && data->avg_pq_y;

    switch (type) {
    case PL_HDR_METADATA_NONE:          return true;
    case PL_HDR_METADATA_ANY:           return has_hdr10 || has_hdr10plus || has_cie_y;
    case PL_HDR_METADATA_HDR10:         return has_hdr10;
    case PL_HDR_METADATA_HDR10PLUS:     return has_hdr10plus;
    case PL_HDR_METADATA_CIE_Y:         return has_cie_y;
    case PL_HDR_METADATA_TYPE_COUNT:    break;
    }

    pl_unreachable();
}

const struct pl_color_space pl_color_space_unknown = {0};

const struct pl_color_space pl_color_space_srgb = {
    .primaries = PL_COLOR_PRIM_BT_709,
    .transfer  = PL_COLOR_TRC_SRGB,
};

const struct pl_color_space pl_color_space_bt709 = {
    .primaries = PL_COLOR_PRIM_BT_709,
    .transfer  = PL_COLOR_TRC_BT_1886,
};

const struct pl_color_space pl_color_space_hdr10 = {
    .primaries = PL_COLOR_PRIM_BT_2020,
    .transfer  = PL_COLOR_TRC_PQ,
};

const struct pl_color_space pl_color_space_bt2020_hlg = {
    .primaries = PL_COLOR_PRIM_BT_2020,
    .transfer  = PL_COLOR_TRC_HLG,
};

const struct pl_color_space pl_color_space_monitor = {
    .primaries = PL_COLOR_PRIM_BT_709, // sRGB primaries
    .transfer  = PL_COLOR_TRC_UNKNOWN, // unknown SDR response
};

bool pl_color_space_is_hdr(const struct pl_color_space *csp)
{
    return csp->hdr.max_luma > PL_COLOR_SDR_WHITE ||
           pl_color_transfer_is_hdr(csp->transfer);
}

bool pl_color_space_is_black_scaled(const struct pl_color_space *csp)
{
    switch (csp->transfer) {
    case PL_COLOR_TRC_UNKNOWN:
    case PL_COLOR_TRC_SRGB:
    case PL_COLOR_TRC_LINEAR:
    case PL_COLOR_TRC_GAMMA18:
    case PL_COLOR_TRC_GAMMA20:
    case PL_COLOR_TRC_GAMMA22:
    case PL_COLOR_TRC_GAMMA24:
    case PL_COLOR_TRC_GAMMA26:
    case PL_COLOR_TRC_GAMMA28:
    case PL_COLOR_TRC_PRO_PHOTO:
    case PL_COLOR_TRC_ST428:
    case PL_COLOR_TRC_HLG:
        return true;

    case PL_COLOR_TRC_BT_1886:
    case PL_COLOR_TRC_PQ:
    case PL_COLOR_TRC_V_LOG:
    case PL_COLOR_TRC_S_LOG1:
    case PL_COLOR_TRC_S_LOG2:
        return false;

    case PL_COLOR_TRC_COUNT: break;
    }

    pl_unreachable();
}

void pl_color_space_merge(struct pl_color_space *orig,
                          const struct pl_color_space *new)
{
    if (!orig->primaries)
        orig->primaries = new->primaries;
    if (!orig->transfer)
        orig->transfer = new->transfer;
    pl_hdr_metadata_merge(&orig->hdr, &new->hdr);
}

bool pl_color_space_equal(const struct pl_color_space *c1,
                          const struct pl_color_space *c2)
{
    return c1->primaries == c2->primaries &&
           c1->transfer  == c2->transfer &&
           pl_hdr_metadata_equal(&c1->hdr, &c2->hdr);
}

// Estimates luminance from maxRGB by looking at how monochromatic MaxSCL is
static void luma_from_maxrgb(const struct pl_color_space *csp,
                             enum pl_hdr_scaling scaling,
                             float *out_max, float *out_avg)
{
    const float maxscl = PL_MAX3(csp->hdr.scene_max[0],
                                 csp->hdr.scene_max[1],
                                 csp->hdr.scene_max[2]);
    if (!maxscl)
        return;

    struct pl_raw_primaries prim = csp->hdr.prim;
    pl_raw_primaries_merge(&prim, pl_raw_primaries_get(csp->primaries));
    const pl_matrix3x3 rgb2xyz = pl_get_rgb2xyz_matrix(&prim);

    const float max_luma = rgb2xyz.m[1][0] * csp->hdr.scene_max[0] +
                           rgb2xyz.m[1][1] * csp->hdr.scene_max[1] +
                           rgb2xyz.m[1][2] * csp->hdr.scene_max[2];

    const float coef = max_luma / maxscl;
    *out_max = pl_hdr_rescale(PL_HDR_NITS, scaling, max_luma);
    *out_avg = pl_hdr_rescale(PL_HDR_NITS, scaling, coef * csp->hdr.scene_avg);
}

static inline bool metadata_compat(enum pl_hdr_metadata_type metadata,
                                   enum pl_hdr_metadata_type compat)
{
    return metadata == PL_HDR_METADATA_ANY || metadata == compat;
}

void pl_color_space_nominal_luma_ex(const struct pl_nominal_luma_params *params)
{
    if (!params || (!params->out_min && !params->out_max && !params->out_avg))
        return;

    const struct pl_color_space *csp = params->color;
    const enum pl_hdr_scaling scaling = params->scaling;

    float min_luma = 0, max_luma = 0, avg_luma = 0;
    if (params->metadata != PL_HDR_METADATA_NONE) {
        // Initialize from static HDR10 metadata, in all cases
        min_luma = pl_hdr_rescale(PL_HDR_NITS, scaling, csp->hdr.min_luma);
        max_luma = pl_hdr_rescale(PL_HDR_NITS, scaling, csp->hdr.max_luma);
    }

    if (metadata_compat(params->metadata, PL_HDR_METADATA_HDR10PLUS) &&
        pl_hdr_metadata_contains(&csp->hdr, PL_HDR_METADATA_HDR10PLUS))
    {
        luma_from_maxrgb(csp, scaling, &max_luma, &avg_luma);
    }

    if (metadata_compat(params->metadata, PL_HDR_METADATA_CIE_Y) &&
        pl_hdr_metadata_contains(&csp->hdr, PL_HDR_METADATA_CIE_Y))
    {
        max_luma = pl_hdr_rescale(PL_HDR_PQ, scaling, csp->hdr.max_pq_y);
        avg_luma = pl_hdr_rescale(PL_HDR_PQ, scaling, csp->hdr.avg_pq_y);
    }

    // Clamp to sane value range
    const float hdr_min = pl_hdr_rescale(PL_HDR_NITS, scaling, PL_COLOR_HDR_BLACK);
    const float hdr_max = pl_hdr_rescale(PL_HDR_PQ,   scaling, 1.0f);
    max_luma = max_luma ? PL_CLAMP(max_luma, hdr_min, hdr_max) : 0;
    min_luma = min_luma ? PL_CLAMP(min_luma, hdr_min, hdr_max) : 0;
    if ((max_luma && min_luma >= max_luma) || min_luma >= hdr_max)
        min_luma = max_luma = 0; // sanity

    // PQ is always scaled down to absolute black, ignoring HDR metadata
    if (csp->transfer == PL_COLOR_TRC_PQ)
        min_luma = hdr_min;

    // Baseline/fallback metadata, inferred entirely from the colorspace
    // description and built-in default assumptions
    if (!max_luma) {
        if (csp->transfer == PL_COLOR_TRC_HLG) {
            max_luma = pl_hdr_rescale(PL_HDR_NITS, scaling, PL_COLOR_HLG_PEAK);
        } else {
            const float peak = pl_color_transfer_nominal_peak(csp->transfer);
            max_luma = pl_hdr_rescale(PL_HDR_NORM, scaling, peak);
        }
    }

    if (!min_luma) {
        if (pl_color_transfer_is_hdr(csp->transfer)) {
            min_luma = hdr_min;
        } else {
            const float peak = pl_hdr_rescale(scaling, PL_HDR_NITS, max_luma);
            min_luma = pl_hdr_rescale(PL_HDR_NITS, scaling,
                                      peak / PL_COLOR_SDR_CONTRAST);
        }
    }

    if (avg_luma)
        avg_luma = PL_CLAMP(avg_luma, min_luma, max_luma); // sanity

    if (params->out_min)
        *params->out_min = min_luma;
    if (params->out_max)
        *params->out_max = max_luma;
    if (params->out_avg)
        *params->out_avg = avg_luma;
}

void pl_color_space_nominal_luma(const struct pl_color_space *csp,
                                 float *out_min, float *out_max)
{
    pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
        .color      = csp,
        .metadata   = PL_HDR_METADATA_ANY,
        .scaling    = PL_HDR_NORM,
        .out_min    = out_min,
        .out_max    = out_max,
    ));
}

void pl_color_space_infer(struct pl_color_space *space)
{
    if (!space->primaries)
        space->primaries = PL_COLOR_PRIM_BT_709;
    if (!space->transfer)
        space->transfer = PL_COLOR_TRC_BT_1886;

    // Sanitize the static HDR metadata
    pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
        .color      = space,
        .metadata   = PL_HDR_METADATA_HDR10,
        .scaling    = PL_HDR_NITS,
        .out_max    = &space->hdr.max_luma,
        // Preserve tagged minimum
        .out_min    = space->hdr.min_luma ? NULL : &space->hdr.min_luma,
    ));

    // Default the signal color space based on the nominal raw primaries
    if (!pl_primaries_valid(&space->hdr.prim))
        space->hdr.prim = *pl_raw_primaries_get(space->primaries);
}

static void infer_both_ref(struct pl_color_space *space,
                           struct pl_color_space *ref)
{
    pl_color_space_infer(ref);

    if (!space->primaries) {
        if (pl_color_primaries_is_wide_gamut(ref->primaries)) {
            space->primaries = PL_COLOR_PRIM_BT_709;
        } else {
            space->primaries = ref->primaries;
        }
    }

    if (!space->transfer) {
        switch (ref->transfer) {
        case PL_COLOR_TRC_UNKNOWN:
        case PL_COLOR_TRC_COUNT:
            pl_unreachable();
        case PL_COLOR_TRC_BT_1886:
        case PL_COLOR_TRC_SRGB:
        case PL_COLOR_TRC_GAMMA22:
            // Re-use input transfer curve to avoid small adaptations
            space->transfer = ref->transfer;
            break;
        case PL_COLOR_TRC_PQ:
        case PL_COLOR_TRC_HLG:
        case PL_COLOR_TRC_V_LOG:
        case PL_COLOR_TRC_S_LOG1:
        case PL_COLOR_TRC_S_LOG2:
            // Pick BT.1886 model because it models SDR contrast accurately,
            // and we need contrast information for tone mapping
            space->transfer = PL_COLOR_TRC_BT_1886;
            break;
        case PL_COLOR_TRC_PRO_PHOTO:
            // ProPhotoRGB and sRGB are both piecewise with linear slope
            space->transfer = PL_COLOR_TRC_SRGB;
            break;
        case PL_COLOR_TRC_LINEAR:
        case PL_COLOR_TRC_GAMMA18:
        case PL_COLOR_TRC_GAMMA20:
        case PL_COLOR_TRC_GAMMA24:
        case PL_COLOR_TRC_GAMMA26:
        case PL_COLOR_TRC_GAMMA28:
        case PL_COLOR_TRC_ST428:
            // Pick pure power output curve to avoid introducing black crush
            space->transfer = PL_COLOR_TRC_GAMMA22;
            break;
        }
    }

    // Infer the remaining fields after making the above choices
    pl_color_space_infer(space);
}

void pl_color_space_infer_ref(struct pl_color_space *space,
                              const struct pl_color_space *refp)
{
    // Make a copy of `refp` to infer missing values first
    struct pl_color_space ref = *refp;
    infer_both_ref(space, &ref);
}

void pl_color_space_infer_map(struct pl_color_space *src,
                              struct pl_color_space *dst)
{
    bool unknown_src_contrast = !src->hdr.min_luma;
    bool unknown_dst_contrast = !dst->hdr.min_luma;

    infer_both_ref(dst, src);

    // If the src has an unspecified gamma curve with dynamic black scaling,
    // default it to match the dst colorspace contrast. This does not matter in
    // most cases, but ensures that BT.1886 is tuned to the appropriate black
    // point by default.
    bool dynamic_src_contrast = pl_color_space_is_black_scaled(src) ||
                                src->transfer == PL_COLOR_TRC_BT_1886;
    if (unknown_src_contrast && dynamic_src_contrast)
        src->hdr.min_luma = dst->hdr.min_luma;

    // Do the same in reverse if both src and dst are SDR curves
    bool src_is_sdr = !pl_color_space_is_hdr(src);
    bool dst_is_sdr = !pl_color_space_is_hdr(dst);
    if (unknown_dst_contrast && src_is_sdr && dst_is_sdr)
        dst->hdr.min_luma = src->hdr.min_luma;

    // If the src is HLG and the output is HDR, tune the HLG peak to the output
    if (src->transfer == PL_COLOR_TRC_HLG && pl_color_space_is_hdr(dst))
        src->hdr.max_luma = dst->hdr.max_luma;
}

const struct pl_color_adjustment pl_color_adjustment_neutral = {
    PL_COLOR_ADJUSTMENT_NEUTRAL
};

void pl_chroma_location_offset(enum pl_chroma_location loc, float *x, float *y)
{
    *x = *y = 0;

    // This is the majority of subsampled chroma content out there
    loc = PL_DEF(loc, PL_CHROMA_LEFT);

    switch (loc) {
    case PL_CHROMA_LEFT:
    case PL_CHROMA_TOP_LEFT:
    case PL_CHROMA_BOTTOM_LEFT:
        *x = -0.5;
        break;
    default: break;
    }

    switch (loc) {
    case PL_CHROMA_TOP_LEFT:
    case PL_CHROMA_TOP_CENTER:
        *y = -0.5;
        break;
    default: break;
    }

    switch (loc) {
    case PL_CHROMA_BOTTOM_LEFT:
    case PL_CHROMA_BOTTOM_CENTER:
        *y = 0.5;
        break;
    default: break;
    }
}

struct pl_cie_xy pl_white_from_temp(float temp)
{
    temp = PL_CLAMP(temp, 2500, 25000);

    double ti = 1000.0 / temp, ti2 = ti * ti, ti3 = ti2 * ti, x;
    if (temp <= 7000) {
        x = -4.6070 * ti3 + 2.9678 * ti2 + 0.09911 * ti + 0.244063;
    } else {
        x = -2.0064 * ti3 + 1.9018 * ti2 + 0.24748 * ti + 0.237040;
    }

    return (struct pl_cie_xy) {
        .x = x,
        .y = -3 * (x*x) + 2.87 * x - 0.275,
    };
}

bool pl_raw_primaries_equal(const struct pl_raw_primaries *a,
                            const struct pl_raw_primaries *b)
{
    return pl_cie_xy_equal(&a->red,   &b->red)   &&
           pl_cie_xy_equal(&a->green, &b->green) &&
           pl_cie_xy_equal(&a->blue,  &b->blue)  &&
           pl_cie_xy_equal(&a->white, &b->white);
}

bool pl_raw_primaries_similar(const struct pl_raw_primaries *a,
                              const struct pl_raw_primaries *b)
{
    float delta = fabsf(a->red.x   - b->red.x)   +
                  fabsf(a->red.y   - b->red.y)   +
                  fabsf(a->green.x - b->green.x) +
                  fabsf(a->green.y - b->green.y) +
                  fabsf(a->blue.x  - b->blue.x)  +
                  fabsf(a->blue.y  - b->blue.y)  +
                  fabsf(a->white.x - b->white.x) +
                  fabsf(a->white.y - b->white.y);

    return delta < 0.001;
}

void pl_raw_primaries_merge(struct pl_raw_primaries *orig,
                            const struct pl_raw_primaries *update)
{
    union {
        struct pl_raw_primaries prim;
        float raw[8];
    } *pa = (void *) orig,
      *pb = (void *) update;

    pl_static_assert(sizeof(*pa) == sizeof(*orig));
    for (int i = 0; i < PL_ARRAY_SIZE(pa->raw); i++)
        pa->raw[i] = PL_DEF(pa->raw[i], pb->raw[i]);
}

const struct pl_raw_primaries *pl_raw_primaries_get(enum pl_color_primaries prim)
{
    /*
    Values from: ITU-R Recommendations BT.470-6, BT.601-7, BT.709-5, BT.2020-0

    https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.470-6-199811-S!!PDF-E.pdf
    https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.601-7-201103-I!!PDF-E.pdf
    https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.709-5-200204-I!!PDF-E.pdf
    https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.2020-0-201208-I!!PDF-E.pdf

    Other colorspaces from https://en.wikipedia.org/wiki/RGB_color_space#Specifications
    */

    // CIE standard illuminant series
#define CIE_D50 {0.3457, 0.3585}
#define CIE_D65 {0.3127, 0.3290}
#define CIE_C   {0.3100, 0.3160}
#define CIE_E   {1.0/3.0, 1.0/3.0}
#define DCI     {0.3140, 0.3510}

    static const struct pl_raw_primaries primaries[] = {
        [PL_COLOR_PRIM_BT_470M] = {
            .red   = {0.670, 0.330},
            .green = {0.210, 0.710},
            .blue  = {0.140, 0.080},
            .white = CIE_C,
        },

        [PL_COLOR_PRIM_BT_601_525] = {
            .red   = {0.630, 0.340},
            .green = {0.310, 0.595},
            .blue  = {0.155, 0.070},
            .white = CIE_D65,
        },
        [PL_COLOR_PRIM_BT_601_625] = {
            .red   = {0.640, 0.330},
            .green = {0.290, 0.600},
            .blue  = {0.150, 0.060},
            .white = CIE_D65,
        },
        [PL_COLOR_PRIM_BT_709] = {
            .red   = {0.640, 0.330},
            .green = {0.300, 0.600},
            .blue  = {0.150, 0.060},
            .white = CIE_D65,
        },
        [PL_COLOR_PRIM_BT_2020] = {
            .red   = {0.708, 0.292},
            .green = {0.170, 0.797},
            .blue  = {0.131, 0.046},
            .white = CIE_D65,
        },
        [PL_COLOR_PRIM_APPLE] = {
            .red   = {0.625, 0.340},
            .green = {0.280, 0.595},
            .blue  = {0.115, 0.070},
            .white = CIE_D65,
        },
        [PL_COLOR_PRIM_ADOBE] = {
            .red   = {0.640, 0.330},
            .green = {0.210, 0.710},
            .blue  = {0.150, 0.060},
            .white = CIE_D65,
        },
        [PL_COLOR_PRIM_PRO_PHOTO] = {
            .red   = {0.7347, 0.2653},
            .green = {0.1596, 0.8404},
            .blue  = {0.0366, 0.0001},
            .white = CIE_D50,
        },
        [PL_COLOR_PRIM_CIE_1931] = {
            .red   = {0.7347, 0.2653},
            .green = {0.2738, 0.7174},
            .blue  = {0.1666, 0.0089},
            .white = CIE_E,
        },
    // From SMPTE RP 431-2
        [PL_COLOR_PRIM_DCI_P3] = {
            .red   = {0.680, 0.320},
            .green = {0.265, 0.690},
            .blue  = {0.150, 0.060},
            .white = DCI,
        },
        [PL_COLOR_PRIM_DISPLAY_P3] = {
            .red   = {0.680, 0.320},
            .green = {0.265, 0.690},
            .blue  = {0.150, 0.060},
            .white = CIE_D65,
        },
    // From Panasonic VARICAM reference manual
        [PL_COLOR_PRIM_V_GAMUT] = {
            .red   = {0.730, 0.280},
            .green = {0.165, 0.840},
            .blue  = {0.100, -0.03},
            .white = CIE_D65,
        },
    // From Sony S-Log reference manual
        [PL_COLOR_PRIM_S_GAMUT] = {
            .red   = {0.730, 0.280},
            .green = {0.140, 0.855},
            .blue  = {0.100, -0.05},
            .white = CIE_D65,
        },
    // From FFmpeg source code
        [PL_COLOR_PRIM_FILM_C] = {
            .red   = {0.681, 0.319},
            .green = {0.243, 0.692},
            .blue  = {0.145, 0.049},
            .white = CIE_C,
        },
        [PL_COLOR_PRIM_EBU_3213] = {
            .red   = {0.630, 0.340},
            .green = {0.295, 0.605},
            .blue  = {0.155, 0.077},
            .white = CIE_D65,
        },
    // From Wikipedia
        [PL_COLOR_PRIM_ACES_AP0] = {
            .red   = {0.7347, 0.2653},
            .green = {0.0000, 1.0000},
            .blue  = {0.0001, -0.0770},
            .white = {0.32168, 0.33767},
        },
        [PL_COLOR_PRIM_ACES_AP1] = {
            .red   = {0.713, 0.293},
            .green = {0.165, 0.830},
            .blue  = {0.128, 0.044},
            .white = {0.32168, 0.33767},
        },
    };

    // This is the default assumption if no colorspace information could
    // be determined, eg. for files which have no video channel.
    if (!prim)
        prim = PL_COLOR_PRIM_BT_709;

    pl_assert(prim < PL_ARRAY_SIZE(primaries));
    return &primaries[prim];
}

// Compute the RGB/XYZ matrix as described here:
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
pl_matrix3x3 pl_get_rgb2xyz_matrix(const struct pl_raw_primaries *prim)
{
    pl_matrix3x3 out = {{{0}}};
    float S[3], X[4], Z[4];

    X[0] = pl_cie_X(prim->red);
    X[1] = pl_cie_X(prim->green);
    X[2] = pl_cie_X(prim->blue);
    X[3] = pl_cie_X(prim->white);

    Z[0] = pl_cie_Z(prim->red);
    Z[1] = pl_cie_Z(prim->green);
    Z[2] = pl_cie_Z(prim->blue);
    Z[3] = pl_cie_Z(prim->white);

    // S = XYZ^-1 * W
    for (int i = 0; i < 3; i++) {
        out.m[0][i] = X[i];
        out.m[1][i] = 1;
        out.m[2][i] = Z[i];
    }

    pl_matrix3x3_invert(&out);

    for (int i = 0; i < 3; i++)
        S[i] = out.m[i][0] * X[3] + out.m[i][1] * 1 + out.m[i][2] * Z[3];

    // M = [Sc * XYZc]
    for (int i = 0; i < 3; i++) {
        out.m[0][i] = S[i] * X[i];
        out.m[1][i] = S[i] * 1;
        out.m[2][i] = S[i] * Z[i];
    }

    return out;
}

pl_matrix3x3 pl_get_xyz2rgb_matrix(const struct pl_raw_primaries *prim)
{
    // For simplicity, just invert the rgb2xyz matrix
    pl_matrix3x3 out = pl_get_rgb2xyz_matrix(prim);
    pl_matrix3x3_invert(&out);
    return out;
}

// LMS<-XYZ revised matrix from CIECAM97, based on a linear transform and
// normalized for equal energy on monochrome inputs
static const pl_matrix3x3 m_cat97 = {{
    {  0.8562,  0.3372, -0.1934 },
    { -0.8360,  1.8327,  0.0033 },
    {  0.0357, -0.0469,  1.0112 },
}};

// M := M * XYZd<-XYZs
static void apply_chromatic_adaptation(struct pl_cie_xy src,
                                       struct pl_cie_xy dest,
                                       pl_matrix3x3 *mat)
{
    // If the white points are nearly identical, this is a wasteful identity
    // operation.
    if (fabs(src.x - dest.x) < 1e-6 && fabs(src.y - dest.y) < 1e-6)
        return;

    // XYZd<-XYZs = Ma^-1 * (I*[Cd/Cs]) * Ma
    // http://www.brucelindbloom.com/index.html?Eqn_ChromAdapt.html
    // For Ma, we use the CIECAM97 revised (linear) matrix
    float C[3][2];

    for (int i = 0; i < 3; i++) {
        // source cone
        C[i][0] = m_cat97.m[i][0] * pl_cie_X(src)
                + m_cat97.m[i][1] * 1
                + m_cat97.m[i][2] * pl_cie_Z(src);

        // dest cone
        C[i][1] = m_cat97.m[i][0] * pl_cie_X(dest)
                + m_cat97.m[i][1] * 1
                + m_cat97.m[i][2] * pl_cie_Z(dest);
    }

    // tmp := I * [Cd/Cs] * Ma
    pl_matrix3x3 tmp = {0};
    for (int i = 0; i < 3; i++)
        tmp.m[i][i] = C[i][1] / C[i][0];

    pl_matrix3x3_mul(&tmp, &m_cat97);

    // M := M * Ma^-1 * tmp
    pl_matrix3x3 ma_inv = m_cat97;
    pl_matrix3x3_invert(&ma_inv);
    pl_matrix3x3_mul(mat, &ma_inv);
    pl_matrix3x3_mul(mat, &tmp);
}

pl_matrix3x3 pl_get_adaptation_matrix(struct pl_cie_xy src, struct pl_cie_xy dst)
{
    // Use BT.709 primaries (with chosen white point) as an XYZ reference
    struct pl_raw_primaries csp = *pl_raw_primaries_get(PL_COLOR_PRIM_BT_709);
    csp.white = src;

    pl_matrix3x3 rgb2xyz = pl_get_rgb2xyz_matrix(&csp);
    pl_matrix3x3 xyz2rgb = rgb2xyz;
    pl_matrix3x3_invert(&xyz2rgb);

    apply_chromatic_adaptation(src, dst, &xyz2rgb);
    pl_matrix3x3_mul(&xyz2rgb, &rgb2xyz);
    return xyz2rgb;
}

pl_matrix3x3 pl_ipt_rgb2lms(const struct pl_raw_primaries *prim)
{
    static const pl_matrix3x3 hpe = {{ // HPE XYZ->LMS (D65) method
        {  0.40024f, 0.70760f, -0.08081f },
        { -0.22630f, 1.16532f,  0.04570f },
        {  0.00000f, 0.00000f,  0.91822f },
    }};

    const float c = 0.04; // 4% crosstalk
    pl_matrix3x3 m = {{
        { 1 - 2*c,       c,       c },
        {       c, 1 - 2*c,       c },
        {       c,       c, 1 - 2*c },
    }};

    pl_matrix3x3_mul(&m, &hpe);

    // Apply chromatic adaptation to D65 if the input white point differs
    static const struct pl_cie_xy d65 = CIE_D65;
    apply_chromatic_adaptation(prim->white, d65, &m);

    const pl_matrix3x3 rgb2xyz = pl_get_rgb2xyz_matrix(prim);
    pl_matrix3x3_mul(&m, &rgb2xyz);
    return m;
}

pl_matrix3x3 pl_ipt_lms2rgb(const struct pl_raw_primaries *prim)
{
    pl_matrix3x3 m = pl_ipt_rgb2lms(prim);
    pl_matrix3x3_invert(&m);
    return m;
}

// As standardized in Ebner & Fairchild IPT (1998)
const pl_matrix3x3 pl_ipt_lms2ipt = {{
    { 0.4000,  0.4000,  0.2000 },
    { 4.4550, -4.8510,  0.3960 },
    { 0.8056,  0.3572, -1.1628 },
}};

// Numerically inverted from the matrix above
const pl_matrix3x3 pl_ipt_ipt2lms = {{
    { 1.0,  0.0975689,  0.205226 },
    { 1.0, -0.1138760,  0.133217 },
    { 1.0,  0.0326151, -0.676887 },
}};

const struct pl_cone_params pl_vision_normal        = {PL_CONE_NONE, 1.0};
const struct pl_cone_params pl_vision_protanomaly   = {PL_CONE_L,    0.5};
const struct pl_cone_params pl_vision_protanopia    = {PL_CONE_L,    0.0};
const struct pl_cone_params pl_vision_deuteranomaly = {PL_CONE_M,    0.5};
const struct pl_cone_params pl_vision_deuteranopia  = {PL_CONE_M,    0.0};
const struct pl_cone_params pl_vision_tritanomaly   = {PL_CONE_S,    0.5};
const struct pl_cone_params pl_vision_tritanopia    = {PL_CONE_S,    0.0};
const struct pl_cone_params pl_vision_monochromacy  = {PL_CONE_LM,   0.0};
const struct pl_cone_params pl_vision_achromatopsia = {PL_CONE_LMS,  0.0};

pl_matrix3x3 pl_get_cone_matrix(const struct pl_cone_params *params,
                                const struct pl_raw_primaries *prim)
{
    // LMS<-RGB := LMS<-XYZ * XYZ<-RGB
    pl_matrix3x3 rgb2lms = m_cat97;
    pl_matrix3x3 rgb2xyz = pl_get_rgb2xyz_matrix(prim);
    pl_matrix3x3_mul(&rgb2lms, &rgb2xyz);

    // LMS versions of the two opposing primaries, plus neutral
    float lms_r[3] = {1.0, 0.0, 0.0},
          lms_b[3] = {0.0, 0.0, 1.0},
          lms_w[3] = {1.0, 1.0, 1.0};

    pl_matrix3x3_apply(&rgb2lms, lms_r);
    pl_matrix3x3_apply(&rgb2lms, lms_b);
    pl_matrix3x3_apply(&rgb2lms, lms_w);

    float a, b, c = params->strength;
    pl_matrix3x3 distort;

    switch (params->cones) {
    case PL_CONE_NONE:
        return pl_matrix3x3_identity;

    case PL_CONE_L:
        // Solve to preserve neutral and blue
        a = (lms_b[0] - lms_b[2] * lms_w[0] / lms_w[2]) /
            (lms_b[1] - lms_b[2] * lms_w[1] / lms_w[2]);
        b = (lms_b[0] - lms_b[1] * lms_w[0] / lms_w[1]) /
            (lms_b[2] - lms_b[1] * lms_w[2] / lms_w[1]);
        assert(fabs(a * lms_w[1] + b * lms_w[2] - lms_w[0]) < 1e-6);

        distort = (pl_matrix3x3) {{
            {            c, (1.0 - c) * a, (1.0 - c) * b},
            {          0.0,           1.0,           0.0},
            {          0.0,           0.0,           1.0},
        }};
        break;

    case PL_CONE_M:
        // Solve to preserve neutral and blue
        a = (lms_b[1] - lms_b[2] * lms_w[1] / lms_w[2]) /
            (lms_b[0] - lms_b[2] * lms_w[0] / lms_w[2]);
        b = (lms_b[1] - lms_b[0] * lms_w[1] / lms_w[0]) /
            (lms_b[2] - lms_b[0] * lms_w[2] / lms_w[0]);
        assert(fabs(a * lms_w[0] + b * lms_w[2] - lms_w[1]) < 1e-6);

        distort = (pl_matrix3x3) {{
            {          1.0,           0.0,           0.0},
            {(1.0 - c) * a,             c, (1.0 - c) * b},
            {          0.0,           0.0,           1.0},
        }};
        break;

    case PL_CONE_S:
        // Solve to preserve neutral and red
        a = (lms_r[2] - lms_r[1] * lms_w[2] / lms_w[1]) /
            (lms_r[0] - lms_r[1] * lms_w[0] / lms_w[1]);
        b = (lms_r[2] - lms_r[0] * lms_w[2] / lms_w[0]) /
            (lms_r[1] - lms_r[0] * lms_w[1] / lms_w[0]);
        assert(fabs(a * lms_w[0] + b * lms_w[1] - lms_w[2]) < 1e-6);

        distort = (pl_matrix3x3) {{
            {          1.0,           0.0,           0.0},
            {          0.0,           1.0,           0.0},
            {(1.0 - c) * a, (1.0 - c) * b,             c},
        }};
        break;

    case PL_CONE_LM:
        // Solve to preserve neutral
        a = lms_w[0] / lms_w[2];
        b = lms_w[1] / lms_w[2];

        distort = (pl_matrix3x3) {{
            {            c,           0.0, (1.0 - c) * a},
            {          0.0,             c, (1.0 - c) * b},
            {          0.0,           0.0,           1.0},
        }};
        break;

    case PL_CONE_MS:
        // Solve to preserve neutral
        a = lms_w[1] / lms_w[0];
        b = lms_w[2] / lms_w[0];

        distort = (pl_matrix3x3) {{
            {          1.0,           0.0,           0.0},
            {(1.0 - c) * a,             c,           0.0},
            {(1.0 - c) * b,           0.0,             c},
        }};
        break;

    case PL_CONE_LS:
        // Solve to preserve neutral
        a = lms_w[0] / lms_w[1];
        b = lms_w[2] / lms_w[1];

        distort = (pl_matrix3x3) {{
            {            c, (1.0 - c) * a,           0.0},
            {          0.0,           1.0,           0.0},
            {          0.0, (1.0 - c) * b,             c},
        }};
        break;

    case PL_CONE_LMS: {
        // Rod cells only, which can be modelled somewhat as a combination of
        // L and M cones. Either way, this is pushing the limits of the our
        // color model, so this is only a rough approximation.
        const float w[3] = {0.3605, 0.6415, -0.002};
        assert(fabs(w[0] + w[1] + w[2] - 1.0) < 1e-6);

        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                distort.m[i][j] = (1.0 - c) * w[j] * lms_w[i] / lms_w[j];
                if (i == j)
                    distort.m[i][j] += c;
            }
        }
        break;
    }

    default:
        pl_unreachable();
    }

    // out := RGB<-LMS * distort * LMS<-RGB
    pl_matrix3x3 out = rgb2lms;
    pl_matrix3x3_invert(&out);
    pl_matrix3x3_mul(&out, &distort);
    pl_matrix3x3_mul(&out, &rgb2lms);

    return out;
}

pl_matrix3x3 pl_get_color_mapping_matrix(const struct pl_raw_primaries *src,
                                         const struct pl_raw_primaries *dst,
                                         enum pl_rendering_intent intent)
{
    // In saturation mapping, we don't care about accuracy and just want
    // primaries to map to primaries, making this an identity transformation.
    if (intent == PL_INTENT_SATURATION)
        return pl_matrix3x3_identity;

    // RGBd<-RGBs = RGBd<-XYZd * XYZd<-XYZs * XYZs<-RGBs
    // Equations from: http://www.brucelindbloom.com/index.html?Math.html
    // Note: Perceptual is treated like relative colorimetric. There's no
    // definition for perceptual other than "make it look good".

    // RGBd<-XYZd matrix
    pl_matrix3x3 xyz2rgb_d = pl_get_xyz2rgb_matrix(dst);

    // Chromatic adaptation, except in absolute colorimetric intent
    if (intent != PL_INTENT_ABSOLUTE_COLORIMETRIC)
        apply_chromatic_adaptation(src->white, dst->white, &xyz2rgb_d);

    // XYZs<-RGBs
    pl_matrix3x3 rgb2xyz_s = pl_get_rgb2xyz_matrix(src);
    pl_matrix3x3_mul(&xyz2rgb_d, &rgb2xyz_s);
    return xyz2rgb_d;
}

// Test the sign of 'p' relative to the line 'ab' (barycentric coordinates)
static float test_point_line(const struct pl_cie_xy p,
                             const struct pl_cie_xy a,
                             const struct pl_cie_xy b)
{
    return (p.x - b.x) * (a.y - b.y) - (a.x - b.x) * (p.y - b.y);
}

// Test if a point is entirely inside a gamut
static float test_point_gamut(struct pl_cie_xy point,
                              const struct pl_raw_primaries *prim)
{
    float d1 = test_point_line(point, prim->red, prim->green),
          d2 = test_point_line(point, prim->green, prim->blue),
          d3 = test_point_line(point, prim->blue, prim->red);

    bool has_neg = d1 < -1e-6f || d2 < -1e-6f || d3 < -1e-6f,
         has_pos = d1 >  1e-6f || d2 >  1e-6f || d3 >  1e-6f;

    return !(has_neg && has_pos);
}

bool pl_primaries_superset(const struct pl_raw_primaries *a,
                           const struct pl_raw_primaries *b)
{
    return test_point_gamut(b->red, a) &&
           test_point_gamut(b->green, a) &&
           test_point_gamut(b->blue, a);
}

bool pl_primaries_valid(const struct pl_raw_primaries *prim)
{
    // Test to see if the primaries form a valid triangle (nonzero area)
    float area = (prim->blue.x - prim->green.x) * (prim->red.y  - prim->green.y)
               - (prim->red.x  - prim->green.x) * (prim->blue.y - prim->green.y);

    return fabs(area) > 1e-6 && test_point_gamut(prim->white, prim);
}

static inline float xy_dist2(struct pl_cie_xy a, struct pl_cie_xy b)
{
    const float dx = a.x - b.x, dy = a.y - b.y;
    return dx * dx + dy * dy;
}

bool pl_primaries_compatible(const struct pl_raw_primaries *a,
                             const struct pl_raw_primaries *b)
{
    float RR = xy_dist2(a->red, b->red),    RG = xy_dist2(a->red, b->green),
          RB = xy_dist2(a->red, b->blue),   GG = xy_dist2(a->green, b->green),
          GB = xy_dist2(a->green, b->blue), BB = xy_dist2(a->blue, b->blue);
    return RR < RG && RR < RB && GG < RG && GG < GB && BB < RB && BB < GB;
}

// returns the intersection of the two lines defined by ab and cd
static struct pl_cie_xy intersection(struct pl_cie_xy a, struct pl_cie_xy b,
                                     struct pl_cie_xy c, struct pl_cie_xy d)
{
    float det = (a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x);
    float t = ((a.x - c.x) * (c.y - d.y) - (a.y - c.y) * (c.x - d.x)) / det;
    return (struct pl_cie_xy) {
        .x = t ? a.x + t * (b.x - a.x) : 0.0f,
        .y = t ? a.y + t * (b.y - a.y) : 0.0f,
    };
}

// x, y, z specified in clockwise order, with a, b, c being the enclosing gamut
static struct pl_cie_xy
clip_point(struct pl_cie_xy x, struct pl_cie_xy y, struct pl_cie_xy z,
           struct pl_cie_xy a, struct pl_cie_xy b, struct pl_cie_xy c)
{
    const float d1 = test_point_line(y, a, b);
    const float d2 = test_point_line(y, b, c);
    if (d1 <= 0.0f && d2 <= 0.0f) {
        return y; // already inside triangle
    } else if (d1 > 0.0f && d2 > 0.0f) {
        return b; // target vertex fully enclosed
    } else if (d1 > 0.0f) {
        return intersection(a, b, y, z);
    } else {
        return intersection(x, y, b, c);
    }
}

struct pl_raw_primaries pl_primaries_clip(const struct pl_raw_primaries *src,
                                          const struct pl_raw_primaries *dst)
{
    return (struct pl_raw_primaries) {
        .red   = clip_point(src->green, src->red, src->blue,
                            dst->green, dst->red, dst->blue),
        .green = clip_point(src->blue, src->green, src->red,
                            dst->blue, dst->green, dst->red),
        .blue  = clip_point(src->red, src->blue, src->green,
                            dst->red, dst->blue, dst->green),
        .white = src->white,
    };
}

/* Fill in the Y, U, V vectors of a yuv-to-rgb conversion matrix
 * based on the given luma weights of the R, G and B components (lr, lg, lb).
 * lr+lg+lb is assumed to equal 1.
 * This function is meant for colorspaces satisfying the following
 * conditions (which are true for common YUV colorspaces):
 * - The mapping from input [Y, U, V] to output [R, G, B] is linear.
 * - Y is the vector [1, 1, 1].  (meaning input Y component maps to 1R+1G+1B)
 * - U maps to a value with zero R and positive B ([0, x, y], y > 0;
 *   i.e. blue and green only).
 * - V maps to a value with zero B and positive R ([x, y, 0], x > 0;
 *   i.e. red and green only).
 * - U and V are orthogonal to the luma vector [lr, lg, lb].
 * - The magnitudes of the vectors U and V are the minimal ones for which
 *   the image of the set Y=[0...1],U=[-0.5...0.5],V=[-0.5...0.5] under the
 *   conversion function will cover the set R=[0...1],G=[0...1],B=[0...1]
 *   (the resulting matrix can be converted for other input/output ranges
 *   outside this function).
 * Under these conditions the given parameters lr, lg, lb uniquely
 * determine the mapping of Y, U, V to R, G, B.
 */
static pl_matrix3x3 luma_coeffs(float lr, float lg, float lb)
{
    pl_assert(fabs(lr+lg+lb - 1) < 1e-6);
    return (pl_matrix3x3) {{
        {1, 0,                    2 * (1-lr)          },
        {1, -2 * (1-lb) * lb/lg, -2 * (1-lr) * lr/lg  },
        {1,  2 * (1-lb),          0                   },
    }};
}

// Applies hue and saturation controls to a YCbCr->RGB matrix
static inline void apply_hue_sat(pl_matrix3x3 *m,
                                 const struct pl_color_adjustment *params)
{
    // Hue is equivalent to rotating input [U, V] subvector around the origin.
    // Saturation scales [U, V].
    float huecos = params->saturation * cos(params->hue);
    float huesin = params->saturation * sin(params->hue);
    for (int i = 0; i < 3; i++) {
        float u = m->m[i][1], v = m->m[i][2];
        m->m[i][1] = huecos * u - huesin * v;
        m->m[i][2] = huesin * u + huecos * v;
    }
}

pl_transform3x3 pl_color_repr_decode(struct pl_color_repr *repr,
                                     const struct pl_color_adjustment *params)
{
    params = PL_DEF(params, &pl_color_adjustment_neutral);

    pl_matrix3x3 m;
    switch (repr->sys) {
    case PL_COLOR_SYSTEM_BT_709:     m = luma_coeffs(0.2126, 0.7152, 0.0722); break;
    case PL_COLOR_SYSTEM_BT_601:     m = luma_coeffs(0.2990, 0.5870, 0.1140); break;
    case PL_COLOR_SYSTEM_SMPTE_240M: m = luma_coeffs(0.2122, 0.7013, 0.0865); break;
    case PL_COLOR_SYSTEM_BT_2020_NC: m = luma_coeffs(0.2627, 0.6780, 0.0593); break;
    case PL_COLOR_SYSTEM_BT_2020_C:
        // Note: This outputs into the [-0.5,0.5] range for chroma information.
        m = (pl_matrix3x3) {{
            {0, 0, 1},
            {1, 0, 0},
            {0, 1, 0},
        }};
        break;
    case PL_COLOR_SYSTEM_BT_2100_PQ: {
        // Reversed from the matrix in the spec, hard-coded for efficiency
        // and precision reasons. Exact values truncated from ITU-T H-series
        // Supplement 18.
        static const float lm_t = 0.008609, lm_p = 0.111029625;
        m = (pl_matrix3x3) {{
            {1.0,  lm_t,  lm_p},
            {1.0, -lm_t, -lm_p},
            {1.0, 0.560031, -0.320627},
        }};
        break;
    }
    case PL_COLOR_SYSTEM_BT_2100_HLG: {
        // Similar to BT.2100 PQ, exact values truncated from WolframAlpha
        static const float lm_t = 0.01571858011, lm_p = 0.2095810681;
        m = (pl_matrix3x3) {{
            {1.0,  lm_t,  lm_p},
            {1.0, -lm_t, -lm_p},
            {1.0, 1.02127108, -0.605274491},
        }};
        break;
    }
    case PL_COLOR_SYSTEM_DOLBYVISION:
        m = repr->dovi->nonlinear;
        break;
    case PL_COLOR_SYSTEM_YCGCO:
        m = (pl_matrix3x3) {{
            {1,  -1,  1},
            {1,   1,  0},
            {1,  -1, -1},
        }};
        break;
    case PL_COLOR_SYSTEM_UNKNOWN: // fall through
    case PL_COLOR_SYSTEM_RGB:
        m = pl_matrix3x3_identity;
        break;
    case PL_COLOR_SYSTEM_XYZ: {
        // For lack of anything saner to do, just assume the caller wants
        // DCI-P3 primaries, which is a reasonable assumption.
        const struct pl_raw_primaries *dst = pl_raw_primaries_get(PL_COLOR_PRIM_DCI_P3);
        m = pl_get_xyz2rgb_matrix(dst);
        // DCDM X'Y'Z' is expected to have equal energy white point (EG 432-1 Annex H)
        apply_chromatic_adaptation((struct pl_cie_xy)CIE_E, dst->white, &m);
        break;
    }
    case PL_COLOR_SYSTEM_COUNT:
        pl_unreachable();
    }

    // Apply hue and saturation in the correct way depending on the colorspace.
    if (pl_color_system_is_ycbcr_like(repr->sys)) {
        apply_hue_sat(&m, params);
    } else if (params->saturation != 1.0 || params->hue != 0.0) {
        // Arbitrarily simulate hue shifts using the BT.709 YCbCr model
        pl_matrix3x3 yuv2rgb = luma_coeffs(0.2126, 0.7152, 0.0722);
        pl_matrix3x3 rgb2yuv = yuv2rgb;
        pl_matrix3x3_invert(&rgb2yuv);
        apply_hue_sat(&yuv2rgb, params);
        // M := RGB<-YUV * YUV<-RGB * M
        pl_matrix3x3_rmul(&rgb2yuv, &m);
        pl_matrix3x3_rmul(&yuv2rgb, &m);
    }

    // Apply color temperature adaptation, relative to BT.709 primaries
    if (params->temperature) {
        struct pl_cie_xy src = pl_white_from_temp(6500);
        struct pl_cie_xy dst = pl_white_from_temp(6500 + 3500 * params->temperature);
        pl_matrix3x3 adapt = pl_get_adaptation_matrix(src, dst);
        pl_matrix3x3_rmul(&adapt, &m);
    }

    pl_transform3x3 out = { .mat = m };
    int bit_depth = PL_DEF(repr->bits.sample_depth,
                    PL_DEF(repr->bits.color_depth, 8));

    double ymax, ymin, cmax, cmid;
    double scale = (1LL << bit_depth) / ((1LL << bit_depth) - 1.0);

    switch (pl_color_levels_guess(repr)) {
    case PL_COLOR_LEVELS_LIMITED: {
        ymax = 235 / 256. * scale;
        ymin =  16 / 256. * scale;
        cmax = 240 / 256. * scale;
        cmid = 128 / 256. * scale;
        break;
    }
    case PL_COLOR_LEVELS_FULL:
        // Note: For full-range YUV, there are multiple, subtly inconsistent
        // standards. So just pick the sanest implementation, which is to
        // assume MAX_INT == 1.0.
        ymax = 1.0;
        ymin = 0.0;
        cmax = 1.0;
        cmid = 128 / 256. * scale; // *not* exactly 0.5
        break;
    default:
        pl_unreachable();
    }

    double ymul = 1.0 / (ymax - ymin);
    double cmul = 0.5 / (cmax - cmid);

    double mul[3]   = { ymul, ymul, ymul };
    double black[3] = { ymin, ymin, ymin };

#ifdef PL_HAVE_DOVI
    if (repr->sys == PL_COLOR_SYSTEM_DOLBYVISION) {
        // The RPU matrix already includes levels normalization, but in this
        // case we also have to respect the signalled color offsets
        for (int i = 0; i < 3; i++) {
            mul[i] = 1.0;
            black[i] = repr->dovi->nonlinear_offset[i] * scale;
        }
    } else
#endif
    if (pl_color_system_is_ycbcr_like(repr->sys)) {
        mul[1]   = mul[2]   = cmul;
        black[1] = black[2] = cmid;
    }

    // Contrast scales the output value range (gain)
    // Brightness scales the constant output bias (black lift/boost)
    for (int i = 0; i < 3; i++) {
        mul[i]   *= params->contrast;
        out.c[i] += params->brightness;
    }

    // Multiply in the texture multiplier and adjust `c` so that black[j] keeps
    // on mapping to RGB=0 (black to black)
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            out.mat.m[i][j] *= mul[j];
            out.c[i] -= out.mat.m[i][j] * black[j];
        }
    }

    // Finally, multiply in the scaling factor required to get the color up to
    // the correct representation.
    pl_matrix3x3_scale(&out.mat, pl_color_repr_normalize(repr));

    // Update the metadata to reflect the change.
    repr->sys    = PL_COLOR_SYSTEM_RGB;
    repr->levels = PL_COLOR_LEVELS_FULL;

    return out;
}

bool pl_icc_profile_equal(const struct pl_icc_profile *p1,
                          const struct pl_icc_profile *p2)
{
    if (p1->len != p2->len)
        return false;

    // Ignore signatures on length-0 profiles, as a special case
    return !p1->len || p1->signature == p2->signature;
}

void pl_icc_profile_compute_signature(struct pl_icc_profile *profile)
{
    if (!profile->len)
        profile->signature = 0;

    // In theory, we could get this value from the profile header itself if
    // lcms is available, but I'm not sure if it's even worth the trouble. Just
    // hard-code this to a pl_mem_hash(), which is decently fast anyway.
    profile->signature = pl_mem_hash(profile->data, profile->len);
}