Commit eb14d3bd authored by Niklas Haas's avatar Niklas Haas

shaders: add support for ICC profiles and 3DLUTs

Code taken from mpv, with some modifications. The shader stuff is from
scratch, but lcms.c draws heavily from code I already wrote.

This again uses LittleCMS 2, with which I've been very satisfied.
Partially addresses #23.
parent cdfceb1a
......@@ -5,6 +5,9 @@ option('vulkan', type: 'combo', choices: ['auto', 'true', 'false'],
option('shaderc', type: 'combo', choices: ['auto', 'true', 'false'],
description: 'libshaderc SPIR-V compiler')
option('lcms', type: 'combo', choices: ['auto', 'true', 'false'],
description: 'LittleCMS 2 support')
# Miscellaneous
option('tests', type: 'boolean', value: false,
description: 'Enable building the test cases')
......
......@@ -745,3 +745,14 @@ struct pl_transform3x3 pl_color_repr_decode(struct pl_color_repr *repr,
return out;
}
bool pl_icc_profile_equal(const struct pl_icc_profile *p1,
const struct pl_icc_profile *p2)
{
// Test for presence of a pointer first
if (!!p1->data != !!p2->data)
return false;
// Otherwise, test for equality of signature+len (if a profile is present)
return !p1->data || (p1->signature == p2->signature && p1->len == p2->len);
}
......@@ -348,4 +348,23 @@ struct pl_matrix3x3 pl_get_color_mapping_matrix(const struct pl_raw_primaries *s
struct pl_transform3x3 pl_color_repr_decode(struct pl_color_repr *repr,
const struct pl_color_adjustment *params);
// Common struct to describe an ICC profile
struct pl_icc_profile {
// Points to the in-memory representation of the ICC profile. This is
// allowed to be NULL, in which case the `pl_icc_profile` represents "no
// profile”.
const void *data;
size_t len;
// If a profile is set, this signature must uniquely identify it. It could
// be, for example, a checksum of the profile contents. Alternatively, it
// could be the pointer to the ICC profile itself, as long as the user
// makes sure that this memory is used in an immutable way.
uint64_t signature;
};
// This doesn't do a comparison of the actual contents, only of the signature.
bool pl_icc_profile_equal(const struct pl_icc_profile *p1,
const struct pl_icc_profile *p2);
#endif // LIBPLACEBO_COLORSPACE_H_
......@@ -275,4 +275,75 @@ void pl_shader_dither(struct pl_shader *sh, int new_depth,
struct pl_shader_obj **dither_state,
const struct pl_dither_params *params);
struct pl_3dlut_params {
// The rendering intent to use when computing the color transformation. A
// recommended value is PL_INTENT_RELATIVE_COLORIMETRIC for color-accurate
// video reproduction, or PL_INTENT_PERCEPTUAL for profiles containing
// meaningful perceptual mapping tables.
enum pl_rendering_intent intent;
// The size of the 3DLUT to generate. If left as NULL, these individually
// default to 64, which is the recommended default for all three.
size_t size_r, size_g, size_b;
};
extern const struct pl_3dlut_params pl_3dlut_default_params;
struct pl_3dlut_profile {
// The nominal, closest approximation representation of the color profile,
// as permitted by `pl_color_space` enums. This will be used as a fallback
// in the event that an ICC profile is absent, or that parsing the ICC
// profile fails. This is also that will be returned for the corresponding
// field in `pl_3dlut_result` when the ICC profile is in use.
struct pl_color_space color;
// The ICC profile itself. (Optional)
struct pl_icc_profile profile;
};
struct pl_3dlut_result {
// The source color space. This is the color space that the colors should
// actually be in at the point in time that they're ingested by the 3DLUT.
// This may differ from the `pl_color_space color` specified in the
// `pl_color_profile`. Users should make sure to apply
// `pl_shader_color_map` in order to get the colors into this format before
// applying `pl_shader_3dlut`.
//
// Note: `pl_shader_color_map` is a no-op when the source and destination
// color spaces are the same, so this can safely be used without disturbing
// the colors in the event that an ICC profile is actually in use.
struct pl_color_space src_color;
// The destination color space. This is the color space that the colors
// will (nominally) be in at the time they exit the 3DLUT.
struct pl_color_space dst_color;
};
#if PL_HAVE_LCMS
// Updates/generates a 3DLUT. Returns success. If true, `out` will be updated
// to a struct describing the color space chosen for the input and output of
// the 3DLUT. (See `pl_color_profile`)
// If `params` is NULL, it defaults to &pl_3dlut_default_params.
//
// Note: This function must always be called before `pl_shader_3dlut`, on the
// same `pl_shader` object, The only reason it's separate from `pl_shader_3dlut`
// is to give users a chance to adapt the input colors to the color space
// chosen by the 3DLUT before applying it.
bool pl_3dlut_update(struct pl_shader *sh,
const struct pl_3dlut_profile *src,
const struct pl_3dlut_profile *dst,
struct pl_shader_obj **lut3d,
struct pl_3dlut_result *out,
const struct pl_3dlut_params *params);
// Actually applies a 3DLUT as generated by `pl_3dlut_update`. The reason this
// is separated from `pl_3dlut_update` is so that the user has the chance to
// correctly map the colors into the specified `src_color` space. This should
// be called only on the `pl_shader_obj` previously updated by
// `pl_3dlut_update`, and only when that function returned true.
void pl_3dlut_apply(struct pl_shader *sh, struct pl_shader_obj **lut3d);
#endif // PL_HAVE_LCMS
#endif // LIBPLACEBO_SHADERS_COLORSPACE_H_
/*
* 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 <lcms2.h>
#include <math.h>
#include "context.h"
#include "lcms.h"
static cmsHPROFILE get_profile(struct pl_context *ctx, cmsContext cms,
struct pl_3dlut_profile prof, cmsHPROFILE dstp,
struct pl_color_space *csp)
{
*csp = prof.color;
if (prof.profile.data) {
pl_info(ctx, "Opening ICC profile..");
cmsHPROFILE ret = cmsOpenProfileFromMemTHR(cms, prof.profile.data,
prof.profile.len);
if (ret)
return ret;
pl_err(ctx, "Failed opening ICC profile, falling back to color struct");
}
// The input profile for the transformation is dependent on the video
// primaries and transfer characteristics
const struct pl_raw_primaries *prim = pl_raw_primaries_get(csp->primaries);
csp->light = PL_COLOR_LIGHT_DISPLAY;
cmsCIExyY wp_xyY = {prim->white.x, prim->white.y, 1.0};
cmsCIExyYTRIPLE prim_xyY = {
.Red = {prim->red.x, prim->red.y, 1.0},
.Green = {prim->green.x, prim->green.y, 1.0},
.Blue = {prim->blue.x, prim->blue.y, 1.0},
};
cmsToneCurve *tonecurve[3] = {0};
switch (csp->transfer) {
case PL_COLOR_TRC_LINEAR: tonecurve[0] = cmsBuildGamma(cms, 1.0); break;
case PL_COLOR_TRC_GAMMA18: tonecurve[0] = cmsBuildGamma(cms, 1.8); break;
case PL_COLOR_TRC_GAMMA28: tonecurve[0] = cmsBuildGamma(cms, 2.8); break;
// Catch-all bucket for unimplemented TRCs
case PL_COLOR_TRC_UNKNOWN:
case PL_COLOR_TRC_PQ:
case PL_COLOR_TRC_HLG:
case PL_COLOR_TRC_S_LOG1:
case PL_COLOR_TRC_S_LOG2:
case PL_COLOR_TRC_V_LOG:
case PL_COLOR_TRC_GAMMA22:
tonecurve[0] = cmsBuildGamma(cms, 2.2);
csp->transfer = PL_COLOR_TRC_GAMMA22;
break;
case PL_COLOR_TRC_SRGB:
// Values copied from Little-CMS
tonecurve[0] = cmsBuildParametricToneCurve(cms, 4,
(double[5]) {2.40, 1/1.055, 0.055/1.055, 1/12.92, 0.04045});
break;
case PL_COLOR_TRC_PRO_PHOTO:
tonecurve[0] = cmsBuildParametricToneCurve(cms, 4,
(double[5]){1.8, 1.0, 0.0, 1/16.0, 0.03125});
break;
case PL_COLOR_TRC_BT_1886: {
if (!dstp) {
pl_info(ctx, "No destination profile data available for accurate "
"BT.1886 emulation, falling back to gamma 2.2");
tonecurve[0] = cmsBuildGamma(cms, 2.2);
csp->transfer = PL_COLOR_TRC_GAMMA22;
break;
}
// To build an appropriate BT.1886 transformation we need access to
// the display's black point, so we LittleCMS' detection function.
// Relative colorimetric is used since we want to approximate the
// BT.1886 to the target device's actual black point even in e.g.
// perceptual mode
const int intent = PL_INTENT_RELATIVE_COLORIMETRIC;
cmsCIEXYZ bp_XYZ;
if (!cmsDetectBlackPoint(&bp_XYZ, dstp, intent, 0))
return false;
// Map this XYZ value back into the (linear) source space
cmsToneCurve *linear = cmsBuildGamma(cms, 1.0);
cmsHPROFILE rev_profile = cmsCreateRGBProfileTHR(cms, &wp_xyY, &prim_xyY,
(cmsToneCurve*[3]){linear, linear, linear});
cmsHPROFILE xyz_profile = cmsCreateXYZProfile();
cmsHTRANSFORM xyz2src = cmsCreateTransformTHR(cms,
xyz_profile, TYPE_XYZ_DBL, rev_profile, TYPE_RGB_DBL,
intent, 0);
cmsFreeToneCurve(linear);
cmsCloseProfile(rev_profile);
cmsCloseProfile(xyz_profile);
if (!xyz2src)
return false;
double src_black[3];
cmsDoTransform(xyz2src, &bp_XYZ, src_black, 1);
cmsDeleteTransform(xyz2src);
// Built-in contrast failsafe
double contrast = 3.0 / (src_black[0] + src_black[1] + src_black[2]);
if (contrast > 100000) {
pl_warn(ctx, "ICC profile detected contrast very high (>100000),"
" falling back to contrast 1000 for sanity");
src_black[0] = src_black[1] = src_black[2] = 1.0 / 1000;
}
// Build the parametric BT.1886 transfer curve, one per channel
for (int i = 0; i < 3; i++) {
const double gamma = 2.40;
double binv = pow(src_black[i], 1.0/gamma);
tonecurve[i] = cmsBuildParametricToneCurve(cms, 6,
(double[4]){gamma, 1.0 - binv, binv, 0.0});
}
break;
}
case PL_COLOR_TRC_COUNT:
default: abort();
}
if (!tonecurve[0])
return NULL;
tonecurve[1] = PL_DEF(tonecurve[1], tonecurve[0]);
tonecurve[2] = PL_DEF(tonecurve[2], tonecurve[0]);
cmsHPROFILE ret = cmsCreateRGBProfileTHR(cms, &wp_xyY, &prim_xyY, tonecurve);
cmsFreeToneCurve(tonecurve[0]);
if (tonecurve[1] != tonecurve[0])
cmsFreeToneCurve(tonecurve[1]);
if (tonecurve[2] != tonecurve[0])
cmsFreeToneCurve(tonecurve[2]);
return ret;
}
static void error_callback(cmsContext cms, cmsUInt32Number code,
const char *msg)
{
struct pl_context *ctx = cmsGetContextUserData(cms);
pl_err(ctx, "lcms2: [%d] %s", (int) code, msg);
}
bool pl_lcms_compute_lut(struct pl_context *ctx, enum pl_rendering_intent intent,
struct pl_3dlut_profile src, struct pl_3dlut_profile dst,
float *out_data, int s_r, int s_g, int s_b,
struct pl_3dlut_result *out)
{
bool ret = false;
cmsHPROFILE srcp = NULL, dstp = NULL;
cmsHTRANSFORM trafo = NULL;
uint16_t *tmp = NULL;
cmsContext cms = cmsCreateContext(NULL, ctx);
if (!cms)
goto error;
cmsSetLogErrorHandlerTHR(cms, error_callback);
dstp = get_profile(ctx, cms, dst, NULL, &out->src_color);
srcp = get_profile(ctx, cms, src, dstp, &out->dst_color);
if (!srcp || !dstp)
goto error;
uint32_t flags = cmsFLAGS_HIGHRESPRECALC | cmsFLAGS_BLACKPOINTCOMPENSATION;
trafo = cmsCreateTransformTHR(cms, srcp, TYPE_RGB_16, dstp, TYPE_RGBA_FLT,
intent, flags);
if (!trafo)
goto error;
pl_assert(s_r && s_g && s_b);
tmp = talloc_array(NULL, uint16_t, s_r * 3);
for (int b = 0; b < s_b; b++) {
for (int g = 0; g < s_g; g++) {
// Fill in a single line of the temporary buffer
for (int r = 0; r < s_r; r++) {
tmp[r * 3 + 0] = r * 65535 / (s_r - 1);
tmp[r * 3 + 1] = g * 65535 / (s_g - 1);
tmp[r * 3 + 2] = b * 65535 / (s_b - 1);
}
// Transform this line into the right output position
size_t offset = (b * s_g + g) * s_r * 4;
cmsDoTransform(trafo, tmp, out_data + offset, s_r);
}
}
ret = true;
// fall through
error:
if (trafo)
cmsDeleteTransform(trafo);
if (srcp)
cmsCloseProfile(srcp);
if (dstp)
cmsCloseProfile(dstp);
if (cms)
cmsDeleteContext(cms);
TA_FREEP(&tmp);
return ret;
}
/*
* 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/>.
*/
#pragma once
#include "common.h"
#include "bstr/bstr.h"
// Compute a transformation from one color profile to another, and fill the
// provided array by the resulting 3DLUT. The array must have room for four
// components per sample.
bool pl_lcms_compute_lut(struct pl_context *ctx, enum pl_rendering_intent intent,
struct pl_3dlut_profile src, struct pl_3dlut_profile dst,
float *out_data, int s_r, int s_g, int s_b,
struct pl_3dlut_result *out);
......@@ -72,7 +72,10 @@ tests = [
# Optional components, in the following format:
# [ name, dependency, extra_sources, extra_tests ]
components = [
[
[ 'lcms',
cc.find_library('lcms2', version: '>=2.6', required: false),
[ 'lcms.c' ],
], [
'shaderc',
cc.find_library('shaderc_shared', required: false),
'spirv_shaderc.c',
......
......@@ -113,6 +113,7 @@ enum pl_shader_obj_type {
PL_SHADER_OBJ_SAMPLER,
PL_SHADER_OBJ_SAMPLER_SEP,
PL_SHADER_OBJ_DITHER,
PL_SHADER_OBJ_3DLUT,
PL_SHADER_OBJ_LUT,
};
......
......@@ -1069,3 +1069,108 @@ const struct pl_dither_params pl_dither_default_params = {
.method = PL_DITHER_BLUE_NOISE,
.temporal = false, // commonly flickers on LCDs
};
#if PL_HAVE_LCMS
#include "lcms.h"
struct sh_3dlut_obj {
struct pl_context *ctx;
enum pl_rendering_intent intent;
struct pl_3dlut_profile src, dst;
struct pl_3dlut_result result;
struct pl_shader_obj *lut_obj;
bool updated; // to detect misuse of the API
bool ok;
ident_t lut;
};
static void sh_3dlut_uninit(const struct pl_gpu *gpu, void *ptr)
{
struct sh_3dlut_obj *obj = ptr;
pl_shader_obj_destroy(&obj->lut_obj);
*obj = (struct sh_3dlut_obj) {0};
}
static void fill_3dlut(void *priv, float *data, int s_r, int s_g, int s_b)
{
struct sh_3dlut_obj *obj = priv;
struct pl_context *ctx = obj->ctx;
obj->ok = pl_lcms_compute_lut(ctx, obj->intent, obj->src, obj->dst,
data, s_r, s_g, s_b, &obj->result);
if (!obj->ok)
pl_err(ctx, "Failed computing 3DLUT!");
}
static bool color_profile_eq(const struct pl_3dlut_profile *a,
const struct pl_3dlut_profile *b)
{
return pl_icc_profile_equal(&a->profile, &b->profile) &&
pl_color_space_equal(&a->color, &b->color);
}
bool pl_3dlut_update(struct pl_shader *sh,
const struct pl_3dlut_profile *src,
const struct pl_3dlut_profile *dst,
struct pl_shader_obj **lut3d, struct pl_3dlut_result *out,
const struct pl_3dlut_params *params)
{
params = PL_DEF(params, &pl_3dlut_default_params);
size_t s_r = PL_DEF(params->size_r, 64),
s_g = PL_DEF(params->size_g, 64),
s_b = PL_DEF(params->size_b, 64);
struct sh_3dlut_obj *obj;
obj = SH_OBJ(sh, lut3d, PL_SHADER_OBJ_3DLUT,
struct sh_3dlut_obj, sh_3dlut_uninit);
if (!obj)
return false;
bool changed = !color_profile_eq(&obj->src, src) ||
!color_profile_eq(&obj->dst, dst) ||
obj->intent != params->intent;
// Update the object, since we need this information from `fill_3dlut`
obj->ctx = sh->ctx;
obj->intent = params->intent;
obj->src = *src;
obj->dst = *dst;
obj->lut = sh_lut(sh, &obj->lut_obj, SH_LUT_LINEAR, s_r, s_g, s_b, 4,
changed, obj, fill_3dlut);
if (!obj->lut || !obj->ok)
return false;
obj->updated = true;
*out = obj->result;
return true;
}
void pl_3dlut_apply(struct pl_shader *sh, struct pl_shader_obj **lut3d)
{
if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))
return;
struct sh_3dlut_obj *obj;
obj = SH_OBJ(sh, lut3d, PL_SHADER_OBJ_3DLUT,
struct sh_3dlut_obj, sh_3dlut_uninit);
if (!obj || !obj->lut || !obj->updated || !obj->ok) {
PL_ERR(sh, "pl_shader_3dlut called without prior pl_3dlut_update?");
return;
}
GLSL("// pl_shader_3dlut\n");
GLSL("color.rgba = %s(color.rgb);\n", obj->lut);
obj->updated = false;
}
const struct pl_3dlut_params pl_3dlut_default_params = {
.intent = PL_INTENT_RELATIVE_COLORIMETRIC,
.size_r = 64,
.size_g = 64,
.size_b = 64,
};
#endif // PL_HAVE_LCMS
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment