Coverage Report

Created: 2025-08-28 07:26

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