Coverage Report

Created: 2026-05-16 07:24

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mpv/video/out/gpu/lcms.c
Line
Count
Source
1
/*
2
 * This file is part of mpv.
3
 *
4
 * mpv is free software; you can redistribute it and/or
5
 * modify it under the terms of the GNU Lesser General Public
6
 * License as published by the Free Software Foundation; either
7
 * version 2.1 of the License, or (at your option) any later version.
8
 *
9
 * mpv is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU Lesser General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU Lesser General Public
15
 * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
#include <string.h>
19
#include <math.h>
20
21
#include "mpv_talloc.h"
22
23
#include "config.h"
24
25
#include "stream/stream.h"
26
#include "common/common.h"
27
#include "misc/bstr.h"
28
#include "misc/hash.h"
29
#include "misc/io_utils.h"
30
#include "common/msg.h"
31
#include "options/m_option.h"
32
#include "options/path.h"
33
#include "video/csputils.h"
34
#include "lcms.h"
35
36
#include "osdep/io.h"
37
38
#if HAVE_LCMS2
39
40
#include <lcms2.h>
41
#include <libavutil/sha.h>
42
#include <libavutil/mem.h>
43
44
struct gl_lcms {
45
    void *icc_data;
46
    size_t icc_size;
47
    struct AVBufferRef *vid_profile;
48
    char *current_profile;
49
    bool using_memory_profile;
50
    bool changed;
51
    enum pl_color_primaries current_prim;
52
    enum pl_color_transfer current_trc;
53
54
    struct mp_log *log;
55
    struct mpv_global *global;
56
    struct mp_icc_opts *opts;
57
};
58
59
static void lcms2_error_handler(cmsContext ctx, cmsUInt32Number code,
60
                                const char *msg)
61
0
{
62
0
    struct gl_lcms *p = cmsGetContextUserData(ctx);
63
0
    MP_ERR(p, "lcms2: %s\n", msg);
64
0
}
65
66
static void load_profile(struct gl_lcms *p)
67
0
{
68
0
    talloc_free(p->icc_data);
69
0
    p->icc_data = NULL;
70
0
    p->icc_size = 0;
71
0
    p->using_memory_profile = false;
72
0
    talloc_free(p->current_profile);
73
0
    p->current_profile = NULL;
74
75
0
    if (!p->opts->profile || !p->opts->profile[0])
76
0
        return;
77
78
0
    char *fname = mp_get_user_path(NULL, p->global, p->opts->profile);
79
0
    MP_VERBOSE(p, "Opening ICC profile '%s'\n", fname);
80
0
    struct bstr iccdata = stream_read_file(fname, p, p->global,
81
0
                                           100000000); // 100 MB
82
0
    talloc_free(fname);
83
0
    if (!iccdata.len)
84
0
        return;
85
86
0
    talloc_free(p->icc_data);
87
88
0
    p->icc_data = iccdata.start;
89
0
    p->icc_size = iccdata.len;
90
0
    p->current_profile = talloc_strdup(p, p->opts->profile);
91
0
}
92
93
static void gl_lcms_destructor(void *ptr)
94
0
{
95
0
    struct gl_lcms *p = ptr;
96
0
    av_buffer_unref(&p->vid_profile);
97
0
}
98
99
struct gl_lcms *gl_lcms_init(void *talloc_ctx, struct mp_log *log,
100
                             struct mpv_global *global,
101
                             struct mp_icc_opts *opts)
102
0
{
103
0
    struct gl_lcms *p = talloc_ptrtype(talloc_ctx, p);
104
0
    talloc_set_destructor(p, gl_lcms_destructor);
105
0
    *p = (struct gl_lcms) {
106
0
        .global = global,
107
0
        .log = log,
108
0
        .opts = opts,
109
0
    };
110
0
    gl_lcms_update_options(p);
111
0
    return p;
112
0
}
113
114
void gl_lcms_update_options(struct gl_lcms *p)
115
0
{
116
0
    if ((p->using_memory_profile && !p->opts->profile_auto) ||
117
0
        !bstr_equals(bstr0(p->opts->profile), bstr0(p->current_profile)))
118
0
    {
119
0
        load_profile(p);
120
0
    }
121
122
0
    p->changed = true; // probably
123
0
}
124
125
// Warning: profile.start must point to a ta allocation, and the function
126
//          takes over ownership.
127
// Returns whether the internal profile was changed.
128
bool gl_lcms_set_memory_profile(struct gl_lcms *p, bstr profile)
129
0
{
130
0
    if (!p->opts->profile_auto || (p->opts->profile && p->opts->profile[0])) {
131
0
        talloc_free(profile.start);
132
0
        return false;
133
0
    }
134
135
0
    if (p->using_memory_profile &&
136
0
        p->icc_data && profile.start &&
137
0
        profile.len == p->icc_size &&
138
0
        memcmp(profile.start, p->icc_data, p->icc_size) == 0)
139
0
    {
140
0
        talloc_free(profile.start);
141
0
        return false;
142
0
    }
143
144
0
    p->changed = true;
145
0
    p->using_memory_profile = true;
146
147
0
    talloc_free(p->icc_data);
148
149
0
    p->icc_data = talloc_steal(p, profile.start);
150
0
    p->icc_size = profile.len;
151
152
0
    return true;
153
0
}
154
155
// Guards against NULL and uses bstr_equals to short-circuit some special cases
156
static bool vid_profile_eq(struct AVBufferRef *a, struct AVBufferRef *b)
157
0
{
158
0
    if (!a || !b)
159
0
        return a == b;
160
161
0
    return bstr_equals((struct bstr){ a->data, a->size },
162
0
                       (struct bstr){ b->data, b->size });
163
0
}
164
165
// Return whether the profile or config has changed since the last time it was
166
// retrieved. If it has changed, gl_lcms_get_lut3d() should be called.
167
bool gl_lcms_has_changed(struct gl_lcms *p, enum pl_color_primaries prim,
168
                         enum pl_color_transfer trc, struct AVBufferRef *vid_profile)
169
0
{
170
0
    if (p->changed || p->current_prim != prim || p->current_trc != trc)
171
0
        return true;
172
173
0
    return !vid_profile_eq(p->vid_profile, vid_profile);
174
0
}
175
176
// Whether a profile is set. (gl_lcms_get_lut3d() is expected to return a lut,
177
// but it could still fail due to runtime errors, such as invalid icc data.)
178
bool gl_lcms_has_profile(struct gl_lcms *p)
179
0
{
180
0
    return p->icc_size > 0;
181
0
}
182
183
static cmsHPROFILE get_vid_profile(struct gl_lcms *p, cmsContext cms,
184
                                   cmsHPROFILE disp_profile,
185
                                   enum pl_color_primaries prim, enum pl_color_transfer trc)
186
0
{
187
0
    if (p->opts->use_embedded && p->vid_profile) {
188
        // Try using the embedded ICC profile
189
0
        cmsHPROFILE prof = cmsOpenProfileFromMemTHR(cms, p->vid_profile->data,
190
0
                                                    p->vid_profile->size);
191
0
        if (prof) {
192
0
            MP_VERBOSE(p, "Successfully opened embedded ICC profile\n");
193
0
            return prof;
194
0
        }
195
196
        // Otherwise, warn the user and generate the profile as usual
197
0
        MP_WARN(p, "Video contained an invalid ICC profile! Ignoring...\n");
198
0
    }
199
200
    // The input profile for the transformation is dependent on the video
201
    // primaries and transfer characteristics
202
0
    const struct pl_raw_primaries *csp = pl_raw_primaries_get(prim);
203
0
    cmsCIExyY wp_xyY = {csp->white.x, csp->white.y, 1.0};
204
0
    cmsCIExyYTRIPLE prim_xyY = {
205
0
        .Red   = {csp->red.x,   csp->red.y,   1.0},
206
0
        .Green = {csp->green.x, csp->green.y, 1.0},
207
0
        .Blue  = {csp->blue.x,  csp->blue.y,  1.0},
208
0
    };
209
210
0
    cmsToneCurve *tonecurve[3] = {0};
211
0
    switch (trc) {
212
0
    case PL_COLOR_TRC_LINEAR:  tonecurve[0] = cmsBuildGamma(cms, 1.0); break;
213
0
    case PL_COLOR_TRC_GAMMA18: tonecurve[0] = cmsBuildGamma(cms, 1.8); break;
214
0
    case PL_COLOR_TRC_GAMMA20: tonecurve[0] = cmsBuildGamma(cms, 2.0); break;
215
0
    case PL_COLOR_TRC_GAMMA22: tonecurve[0] = cmsBuildGamma(cms, 2.2); break;
216
0
    case PL_COLOR_TRC_GAMMA24: tonecurve[0] = cmsBuildGamma(cms, 2.4); break;
217
0
    case PL_COLOR_TRC_GAMMA26: tonecurve[0] = cmsBuildGamma(cms, 2.6); break;
218
0
    case PL_COLOR_TRC_GAMMA28: tonecurve[0] = cmsBuildGamma(cms, 2.8); break;
219
220
0
    case PL_COLOR_TRC_ST428:
221
0
        tonecurve[0] = cmsBuildParametricToneCurve(cms, 2,
222
0
                (double[3]){2.6, pow(52.37/48.0, 1/2.6), 0.0});
223
0
        break;
224
225
0
    case PL_COLOR_TRC_SRGB:
226
        // Values copied from Little-CMS
227
0
        tonecurve[0] = cmsBuildParametricToneCurve(cms, 4,
228
0
                (double[5]){2.40, 1/1.055, 0.055/1.055, 1/12.92, 0.04045});
229
0
        break;
230
231
0
    case PL_COLOR_TRC_PRO_PHOTO:
232
0
        tonecurve[0] = cmsBuildParametricToneCurve(cms, 4,
233
0
                (double[5]){1.8, 1.0, 0.0, 1/16.0, 0.03125});
234
0
        break;
235
236
0
    case PL_COLOR_TRC_BT_1886: {
237
0
        double src_black[3];
238
0
        if (p->opts->contrast < 0) {
239
            // User requested infinite contrast, return 2.4 profile
240
0
            tonecurve[0] = cmsBuildGamma(cms, 2.4);
241
0
            break;
242
0
        } else if (p->opts->contrast > 0) {
243
0
            MP_VERBOSE(p, "Using specified contrast: %d\n", p->opts->contrast);
244
0
            for (int i = 0; i < 3; i++)
245
0
                src_black[i] = 1.0 / p->opts->contrast;
246
0
        } else {
247
            // To build an appropriate BT.1886 transformation we need access to
248
            // the display's black point, so we use LittleCMS' detection
249
            // function. Relative colorimetric is used since we want to
250
            // approximate the BT.1886 to the target device's actual black
251
            // point even in e.g. perceptual mode
252
0
            const int intent = PL_INTENT_RELATIVE_COLORIMETRIC;
253
0
            cmsCIEXYZ bp_XYZ;
254
0
            if (!cmsDetectBlackPoint(&bp_XYZ, disp_profile, intent, 0))
255
0
                return false;
256
257
            // Map this XYZ value back into the (linear) source space
258
0
            cmsHPROFILE rev_profile;
259
0
            cmsToneCurve *linear = cmsBuildGamma(cms, 1.0);
260
0
            rev_profile = cmsCreateRGBProfileTHR(cms, &wp_xyY, &prim_xyY,
261
0
                    (cmsToneCurve*[3]){linear, linear, linear});
262
0
            cmsHPROFILE xyz_profile = cmsCreateXYZProfile();
263
0
            cmsHTRANSFORM xyz2src = cmsCreateTransformTHR(cms,
264
0
                    xyz_profile, TYPE_XYZ_DBL, rev_profile, TYPE_RGB_DBL,
265
0
                    intent, cmsFLAGS_NOCACHE | cmsFLAGS_NOOPTIMIZE);
266
0
            cmsFreeToneCurve(linear);
267
0
            cmsCloseProfile(rev_profile);
268
0
            cmsCloseProfile(xyz_profile);
269
0
            if (!xyz2src)
270
0
                return false;
271
272
0
            cmsDoTransform(xyz2src, &bp_XYZ, src_black, 1);
273
0
            cmsDeleteTransform(xyz2src);
274
275
0
            double contrast = 3.0 / (src_black[0] + src_black[1] + src_black[2]);
276
0
            MP_VERBOSE(p, "Detected ICC profile contrast: %f\n", contrast);
277
0
        }
278
279
        // Build the parametric BT.1886 transfer curve, one per channel
280
0
        for (int i = 0; i < 3; i++) {
281
0
            const double gamma = 2.40;
282
0
            double binv = pow(src_black[i], 1.0/gamma);
283
0
            tonecurve[i] = cmsBuildParametricToneCurve(cms, 6,
284
0
                    (double[4]){gamma, 1.0 - binv, binv, 0.0});
285
0
        }
286
0
        break;
287
0
    }
288
289
0
    default:
290
0
        abort();
291
0
    }
292
293
0
    if (!tonecurve[0])
294
0
        return false;
295
296
0
    if (!tonecurve[1]) tonecurve[1] = tonecurve[0];
297
0
    if (!tonecurve[2]) tonecurve[2] = tonecurve[0];
298
299
0
    cmsHPROFILE *vid_profile = cmsCreateRGBProfileTHR(cms, &wp_xyY, &prim_xyY,
300
0
                                                      tonecurve);
301
302
0
    if (tonecurve[2] != tonecurve[0]) cmsFreeToneCurve(tonecurve[2]);
303
0
    if (tonecurve[1] != tonecurve[0]) cmsFreeToneCurve(tonecurve[1]);
304
0
    cmsFreeToneCurve(tonecurve[0]);
305
306
0
    return vid_profile;
307
0
}
308
309
bool gl_lcms_get_lut3d(struct gl_lcms *p, struct lut3d **result_lut3d,
310
                       enum pl_color_primaries prim, enum pl_color_transfer trc,
311
                       struct AVBufferRef *vid_profile)
312
0
{
313
0
    int s_r, s_g, s_b;
314
0
    bool result = false;
315
316
0
    p->changed = false;
317
0
    p->current_prim = prim;
318
0
    p->current_trc = trc;
319
320
    // We need to hold on to a reference to the video's ICC profile for as long
321
    // as we still need to perform equality checking, so generate a new
322
    // reference here
323
0
    av_buffer_unref(&p->vid_profile);
324
0
    if (vid_profile) {
325
0
        MP_VERBOSE(p, "Got an embedded ICC profile.\n");
326
0
        p->vid_profile = av_buffer_ref(vid_profile);
327
0
        MP_HANDLE_OOM(p->vid_profile);
328
0
    }
329
330
0
    if (!gl_parse_3dlut_size(p->opts->size_str, &s_r, &s_g, &s_b))
331
0
        return false;
332
333
0
    if (!gl_lcms_has_profile(p))
334
0
        return false;
335
336
    // For simplicity, default to 65x65x65, which is large enough to cover
337
    // typical profiles with good accuracy while not being too wasteful
338
0
    s_r = s_r ? s_r : 65;
339
0
    s_g = s_g ? s_g : 65;
340
0
    s_b = s_b ? s_b : 65;
341
342
0
    void *tmp = talloc_new(NULL);
343
0
    uint16_t *output = talloc_array(tmp, uint16_t, s_r * s_g * s_b * 4);
344
0
    struct lut3d *lut = NULL;
345
0
    cmsContext cms = NULL;
346
347
0
    char *cache_file = NULL;
348
0
    if (p->opts->cache) {
349
        // Gamma is included in the header to help uniquely identify it,
350
        // because we may change the parameter in the future or make it
351
        // customizable, same for the primaries.
352
0
        bstr cache_info = {0};
353
0
        bstr_xappend_asprintf(tmp, &cache_info,
354
0
            "ver=1.4, intent=%d, size=%dx%dx%d, prim=%d, trc=%d, "
355
0
            "contrast=%d\n",
356
0
            p->opts->intent, s_r, s_g, s_b, prim, trc, p->opts->contrast);
357
0
        if (vid_profile)
358
0
            bstr_xappend(tmp, &cache_info, (bstr){vid_profile->data, vid_profile->size});
359
0
        bstr_xappend(tmp, &cache_info, (bstr){p->icc_data, p->icc_size});
360
0
        bstr hashstr = mp_hash_to_bstr(tmp, cache_info.start, cache_info.len, "SHA256");
361
362
0
        char *cache_dir = p->opts->cache_dir;
363
0
        if (cache_dir && cache_dir[0]) {
364
0
            cache_dir = mp_get_user_path(tmp, p->global, cache_dir);
365
0
        } else {
366
0
            cache_dir = mp_find_user_file(tmp, p->global, "cache", "");
367
0
        }
368
369
0
        if (cache_dir && cache_dir[0]) {
370
0
            cache_file = mp_path_join_bstr(tmp, bstr0(cache_dir), hashstr);
371
0
            mp_mkdirp(cache_dir);
372
0
        }
373
0
    }
374
375
    // check cache
376
0
    if (cache_file && stat(cache_file, &(struct stat){0}) == 0) {
377
0
        MP_VERBOSE(p, "Opening 3D LUT cache in file '%s'.\n", cache_file);
378
0
        struct bstr cachedata = stream_read_file(cache_file, tmp, p->global,
379
0
                                                 1000000000); // 1 GB
380
0
        if (cachedata.len == talloc_get_size(output)) {
381
0
            memcpy(output, cachedata.start, cachedata.len);
382
0
            goto done;
383
0
        } else {
384
0
            MP_WARN(p, "3D LUT cache invalid!\n");
385
0
        }
386
0
    }
387
388
0
    cms = cmsCreateContext(NULL, p);
389
0
    if (!cms)
390
0
        goto error_exit;
391
0
    cmsSetLogErrorHandlerTHR(cms, lcms2_error_handler);
392
393
0
    cmsHPROFILE profile =
394
0
        cmsOpenProfileFromMemTHR(cms, p->icc_data, p->icc_size);
395
0
    if (!profile)
396
0
        goto error_exit;
397
398
0
    cmsHPROFILE vid_hprofile = get_vid_profile(p, cms, profile, prim, trc);
399
0
    if (!vid_hprofile) {
400
0
        cmsCloseProfile(profile);
401
0
        goto error_exit;
402
0
    }
403
404
0
    cmsHTRANSFORM trafo = cmsCreateTransformTHR(cms, vid_hprofile, TYPE_RGB_16,
405
0
                                                profile, TYPE_RGBA_16,
406
0
                                                p->opts->intent,
407
0
                                                cmsFLAGS_NOCACHE |
408
0
                                                cmsFLAGS_NOOPTIMIZE |
409
0
                                                cmsFLAGS_BLACKPOINTCOMPENSATION);
410
0
    cmsCloseProfile(profile);
411
0
    cmsCloseProfile(vid_hprofile);
412
413
0
    if (!trafo)
414
0
        goto error_exit;
415
416
    // transform a (s_r)x(s_g)x(s_b) cube, with 3 components per channel
417
0
    uint16_t *input = talloc_array(tmp, uint16_t, s_r * 3);
418
0
    for (int b = 0; b < s_b; b++) {
419
0
        for (int g = 0; g < s_g; g++) {
420
0
            for (int r = 0; r < s_r; r++) {
421
0
                input[r * 3 + 0] = r * 65535 / (s_r - 1);
422
0
                input[r * 3 + 1] = g * 65535 / (s_g - 1);
423
0
                input[r * 3 + 2] = b * 65535 / (s_b - 1);
424
0
            }
425
0
            size_t base = (b * s_r * s_g + g * s_r) * 4;
426
0
            cmsDoTransform(trafo, input, output + base, s_r);
427
0
        }
428
0
    }
429
430
0
    cmsDeleteTransform(trafo);
431
432
0
    if (cache_file)
433
0
        mp_save_to_file(cache_file, output, talloc_get_size(output));
434
435
0
done: ;
436
437
0
    lut = talloc_ptrtype(NULL, lut);
438
0
    *lut = (struct lut3d) {
439
0
        .data = talloc_steal(lut, output),
440
0
        .size = {s_r, s_g, s_b},
441
0
    };
442
443
0
    *result_lut3d = lut;
444
0
    result = true;
445
446
0
error_exit:
447
448
0
    if (cms)
449
0
        cmsDeleteContext(cms);
450
451
0
    if (!lut)
452
0
        MP_FATAL(p, "Error loading ICC profile.\n");
453
454
0
    talloc_free(tmp);
455
0
    return result;
456
0
}
457
458
#else /* HAVE_LCMS2 */
459
460
struct gl_lcms *gl_lcms_init(void *talloc_ctx, struct mp_log *log,
461
                             struct mpv_global *global,
462
                             struct mp_icc_opts *opts)
463
{
464
    return (struct gl_lcms *) talloc_new(talloc_ctx);
465
}
466
467
void gl_lcms_update_options(struct gl_lcms *p) { }
468
bool gl_lcms_set_memory_profile(struct gl_lcms *p, bstr profile) {return false;}
469
470
bool gl_lcms_has_changed(struct gl_lcms *p, enum pl_color_primaries prim,
471
                         enum pl_color_transfer trc, struct AVBufferRef *vid_profile)
472
{
473
    return false;
474
}
475
476
bool gl_lcms_has_profile(struct gl_lcms *p)
477
{
478
    return false;
479
}
480
481
bool gl_lcms_get_lut3d(struct gl_lcms *p, struct lut3d **result_lut3d,
482
                       enum pl_color_primaries prim, enum pl_color_transfer trc,
483
                       struct AVBufferRef *vid_profile)
484
{
485
    return false;
486
}
487
488
#endif
489
490
static inline OPT_STRING_VALIDATE_FUNC(validate_3dlut_size_opt)
491
372
{
492
372
    int p1, p2, p3;
493
372
    return gl_parse_3dlut_size(*value, &p1, &p2, &p3) ? 0 : M_OPT_INVALID;
494
372
}
495
496
#define OPT_BASE_STRUCT struct mp_icc_opts
497
const struct m_sub_options mp_icc_conf = {
498
    .opts = (const m_option_t[]) {
499
        {"use-embedded-icc-profile", OPT_BOOL(use_embedded)},
500
        {"icc-profile", OPT_STRING(profile), .flags = M_OPT_FILE},
501
        {"icc-profile-auto", OPT_BOOL(profile_auto)},
502
        {"icc-cache", OPT_BOOL(cache)},
503
        {"icc-cache-dir", OPT_STRING(cache_dir), .flags = M_OPT_FILE},
504
        {"icc-intent", OPT_INT(intent)},
505
        {"icc-force-contrast", OPT_CHOICE(contrast, {"no", 0}, {"inf", -1}),
506
            M_RANGE(0, 1000000)},
507
        {"icc-3dlut-size", OPT_STRING_VALIDATE(size_str, validate_3dlut_size_opt)},
508
        {"icc-use-luma", OPT_BOOL(icc_use_luma)},
509
        {0}
510
    },
511
    .size = sizeof(struct mp_icc_opts),
512
    .defaults = &(const struct mp_icc_opts) {
513
        .size_str = "auto",
514
        .intent = PL_INTENT_RELATIVE_COLORIMETRIC,
515
        .use_embedded = true,
516
        .cache = true,
517
    },
518
};