diff --git a/.builds/alpine.yml b/.builds/alpine.yml index 72b944829..4f4a08cd0 100644 --- a/.builds/alpine.yml +++ b/.builds/alpine.yml @@ -2,6 +2,7 @@ image: alpine/edge packages: - eudev-dev - glslang + - lcms2-dev - libdisplay-info-dev - libinput-dev - libliftoff-dev diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml index 3e1552204..cac8edac8 100644 --- a/.builds/archlinux.yml +++ b/.builds/archlinux.yml @@ -1,6 +1,7 @@ image: archlinux packages: - clang + - lcms2 - libinput - libdisplay-info - libliftoff diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index bb24105a1..193f59a33 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -5,6 +5,7 @@ packages: - devel/meson # implies ninja - devel/pkgconf - graphics/glslang + - graphics/lcms2 - graphics/libdrm - graphics/libliftoff - graphics/mesa-libs diff --git a/include/meson.build b/include/meson.build index e66980030..ff8d767fa 100644 --- a/include/meson.build +++ b/include/meson.build @@ -20,6 +20,9 @@ endif if not features.get('vulkan-renderer') exclude_files += 'render/vulkan.h' endif +if not features.get('color-management') + exclude_files += 'render/color.h' +endif if not features.get('session') exclude_files += 'backend/session.h' endif diff --git a/include/render/color.h b/include/render/color.h new file mode 100644 index 000000000..e18f12eeb --- /dev/null +++ b/include/render/color.h @@ -0,0 +1,39 @@ +#ifndef RENDER_COLOR_H +#define RENDER_COLOR_H + +#include +#include + +/** + * The formula is approximated via a 3D look-up table. A 3D LUT is a + * three-dimensional array where each element is an RGB triplet. The flat lut_3d + * array has a length of dim_lenĀ³. + * + * Color channel values in the range [0.0, 1.0] are mapped linearly to + * 3D LUT indices such that 0.0 maps exactly to the first element and 1.0 maps + * exactly to the last element in each dimension. + * + * The offset of the RGB triplet given red, green and blue indices r_index, + * g_index and b_index is: + * + * offset = 3 * (r_index + dim_len * g_index + dim_len * dim_len * b_index) + */ +struct wlr_color_transform_lut3d { + float *lut_3d; + size_t dim_len; +}; + +enum wlr_color_transform_type { + COLOR_TRANSFORM_SRGB, + COLOR_TRANSFORM_LUT_3D, +}; + +struct wlr_color_transform { + int ref_count; + struct wlr_addon_set addons; // per-renderer helper state + + enum wlr_color_transform_type type; + struct wlr_color_transform_lut3d lut3d; +}; + +#endif diff --git a/include/wlr/config.h.in b/include/wlr/config.h.in index 6a53d2174..e03049da6 100644 --- a/include/wlr/config.h.in +++ b/include/wlr/config.h.in @@ -14,4 +14,6 @@ #mesondefine WLR_HAS_SESSION +#mesondefine WLR_HAS_COLOR_MANAGEMENT + #endif diff --git a/include/wlr/render/color.h b/include/wlr/render/color.h new file mode 100644 index 000000000..ddd4408b4 --- /dev/null +++ b/include/wlr/render/color.h @@ -0,0 +1,55 @@ +/* + * This an unstable interface of wlroots. No guarantees are made regarding the + * future consistency of this API. + */ +#ifndef WLR_USE_UNSTABLE +#error "Add -DWLR_USE_UNSTABLE to enable unstable wlroots features" +#endif + +#ifndef WLR_RENDER_COLOR_H +#define WLR_RENDER_COLOR_H + +#include +#include + +/** + * A color transformation formula, which maps a linear color space with + * sRGB primaries to an output color space. + * + * For ease of use, this type is heap allocated and reference counted. + * Use wlr_color_transform_ref()/wlr_color_transform_unref(). The initial reference + * count after creation is 1. + * + * Color transforms are immutable; their type/parameters should not be changed, + * and this API provides no functions to modify them after creation. + * + * This formula may be implemented using a 3d look-up table, or some other + * means. + */ +struct wlr_color_transform; + +/** + * Initialize a color transformation to convert linear + * (with sRGB(?) primaries) to an ICC profile. Returns NULL on failure. + */ +struct wlr_color_transform *wlr_color_transform_init_linear_to_icc( + const void *data, size_t size); + +/** + * Initialize a color transformation to apply sRGB encoding. + * Returns NULL on failure. + */ +struct wlr_color_transform *wlr_color_transform_init_srgb(void); + +/** + * Increase the reference count of the color transform by 1. + */ +void wlr_color_transform_ref(struct wlr_color_transform *tr); + +/** + * Reduce the reference count of the color transform by 1; freeing it and + * all associated resources when the reference count hits zero. + */ +void wlr_color_transform_unref(struct wlr_color_transform *tr); + +#endif diff --git a/meson.build b/meson.build index c3ea7fc67..8641291aa 100644 --- a/meson.build +++ b/meson.build @@ -95,6 +95,7 @@ features = { 'vulkan-renderer': false, 'gbm-allocator': false, 'session': false, + 'color-management': false, } internal_features = { 'xcb-errors': false, diff --git a/meson_options.txt b/meson_options.txt index 6977643ca..0c9a1a8ce 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -7,3 +7,4 @@ option('backends', type: 'array', choices: ['auto', 'drm', 'libinput', 'x11'], v option('allocators', type: 'array', choices: ['auto', 'gbm'], value: ['auto'], description: 'Select built-in allocators') option('session', type: 'feature', value: 'auto', description: 'Enable session support') +option('color-management', type: 'feature', value: 'auto', description: 'Enable support for color management') diff --git a/render/color.c b/render/color.c new file mode 100644 index 000000000..838971ccf --- /dev/null +++ b/render/color.c @@ -0,0 +1,155 @@ +#include +#include +#include +#include +#include +#include +#include "render/color.h" + +static const cmsCIExyY srgb_whitepoint = { 0.3127, 0.3291, 1 }; + +static const cmsCIExyYTRIPLE srgb_primaries = { + .Red = { 0.64, 0.33, 1 }, + .Green = { 0.3, 0.6, 1 }, + .Blue = { 0.15, 0.06, 1}, +}; + +static void handle_lcms_error(cmsContext ctx, cmsUInt32Number code, const char *text) { + wlr_log(WLR_ERROR, "[lcms] %s", text); +} + +struct wlr_color_transform *wlr_color_transform_init_linear_to_icc( + const void *data, size_t size) { + struct wlr_color_transform *tx = NULL; + + cmsContext ctx = cmsCreateContext(NULL, NULL); + if (ctx == NULL) { + wlr_log(WLR_ERROR, "cmsCreateContext failed"); + return false; + } + + cmsSetLogErrorHandlerTHR(ctx, handle_lcms_error); + + cmsHPROFILE icc_profile = cmsOpenProfileFromMemTHR(ctx, data, size); + if (icc_profile == NULL) { + wlr_log(WLR_ERROR, "cmsOpenProfileFromMemTHR failed"); + goto out_ctx; + } + + if (cmsGetDeviceClass(icc_profile) != cmsSigDisplayClass) { + wlr_log(WLR_ERROR, "ICC profile must have the Display device class"); + goto out_icc_profile; + } + + + cmsToneCurve *linear_tone_curve = cmsBuildGamma(ctx, 1); + if (linear_tone_curve == NULL) { + wlr_log(WLR_ERROR, "cmsBuildGamma failed"); + goto out_icc_profile; + } + + cmsToneCurve *linear_tf[] = { + linear_tone_curve, + linear_tone_curve, + linear_tone_curve, + }; + cmsHPROFILE srgb_profile = cmsCreateRGBProfileTHR(ctx, &srgb_whitepoint, + &srgb_primaries, linear_tf); + if (srgb_profile == NULL) { + wlr_log(WLR_ERROR, "cmsCreateRGBProfileTHR failed"); + goto out_linear_tone_curve; + } + + cmsHTRANSFORM lcms_tr = cmsCreateTransformTHR(ctx, + srgb_profile, TYPE_RGB_FLT, icc_profile, TYPE_RGB_FLT, + INTENT_RELATIVE_COLORIMETRIC, 0); + if (lcms_tr == NULL) { + wlr_log(WLR_ERROR, "cmsCreateTransformTHR failed"); + goto out_srgb_profile; + } + + size_t dim_len = 33; + float *lut_3d = calloc(3 * dim_len * dim_len * dim_len, sizeof(float)); + if (lut_3d == NULL) { + wlr_log_errno(WLR_ERROR, "Allocation failed"); + goto out_lcms_tr; + } + + float factor = 1.0f / (dim_len - 1); + for (size_t b_index = 0; b_index < dim_len; b_index++) { + for (size_t g_index = 0; g_index < dim_len; g_index++) { + for (size_t r_index = 0; r_index < dim_len; r_index++) { + float rgb_in[3] = { + r_index * factor, + g_index * factor, + b_index * factor, + }; + float rgb_out[3]; + // TODO: use a single call to cmsDoTransform for the entire calculation? + // this does require allocating an extra temp buffer + cmsDoTransform(lcms_tr, rgb_in, rgb_out, 1); + + size_t offset = 3 * (r_index + dim_len * g_index + dim_len * dim_len * b_index); + // TODO: maybe clamp values to [0.0, 1.0] here? + lut_3d[offset] = rgb_out[0]; + lut_3d[offset + 1] = rgb_out[1]; + lut_3d[offset + 2] = rgb_out[2]; + } + } + } + + tx = calloc(1, sizeof(struct wlr_color_transform)); + if (!tx) { + goto out_lcms_tr; + } + tx->type = COLOR_TRANSFORM_LUT_3D; + tx->lut3d.dim_len = dim_len; + tx->lut3d.lut_3d = lut_3d; + tx->ref_count = 1; + wlr_addon_set_init(&tx->addons); + +out_lcms_tr: + cmsDeleteTransform(lcms_tr); +out_linear_tone_curve: + cmsFreeToneCurve(linear_tone_curve); +out_srgb_profile: + cmsCloseProfile(srgb_profile); +out_icc_profile: + cmsCloseProfile(icc_profile); +out_ctx: + cmsDeleteContext(ctx); + return tx; +} + + +struct wlr_color_transform *wlr_color_transform_init_srgb(void) { + struct wlr_color_transform *tx = calloc(1, sizeof(struct wlr_color_transform)); + if (!tx) { + return NULL; + } + tx->type = COLOR_TRANSFORM_SRGB; + tx->ref_count = 1; + wlr_addon_set_init(&tx->addons); + return tx; +} + +static void color_transform_destroy(struct wlr_color_transform *tr) { + free(tr->lut3d.lut_3d); + wlr_addon_set_finish(&tr->addons); + free(tr); +} + +void wlr_color_transform_ref(struct wlr_color_transform *tr) { + tr->ref_count += 1; +} + +void wlr_color_transform_unref(struct wlr_color_transform *tr) { + if (!tr) { + return; + } + assert(tr->ref_count > 0); + tr->ref_count -= 1; + if (tr->ref_count == 0) { + color_transform_destroy(tr); + } +} diff --git a/render/meson.build b/render/meson.build index f09905c71..df8d73823 100644 --- a/render/meson.build +++ b/render/meson.build @@ -39,3 +39,10 @@ endif subdir('pixman') subdir('allocator') + +lcms2 = dependency('lcms2', required: get_option('color-management')) +if lcms2.found() + wlr_deps += lcms2 + wlr_files += files('color.c') + features += { 'color-management': true } +endif