render: gamma-correct blending

This implements gamma-correct blending, which mainly affects font
rendering.

The implementation requires compile-time availability of the new
color-management protocol (available in wayland-protocols >= 1.41),
and run-time support for the same in the compositor (specifically, the
EXT_LINEAR TF function and sRGB primaries).

How it works: all colors are decoded from sRGB to linear (using a
lookup table, generated in the exact same way pixman generates it's
internal conversion tables) before being used by pixman. The resulting
image buffer is thus in decoded/linear format. We use the
color-management protocol to inform the compositor of this, by tagging
the wayland surfaces with the 'ext_linear' image attribute.

Sixes: all colors are sRGB internally, and decoded to linear before
being used in any sixels. Thus, the image buffers will contain linear
colors. This is important, since otherwise there would be a
decode/encode penalty every time a sixel is blended to the grid.

Emojis: we require fcft >= 3.2, which adds support for sRGB decoding
color glyphs. Meaning, the emoji pixman surfaces can be blended
directly to the grid, just like sixels.

Gamma-correct blending is enabled by default *when the compositor
supports it*. There's a new option to explicitly enable/disable it:
gamma-correct-blending=no|yes. If set to 'yes', and the compositor
does not implement the required color-management features, warning
logs are emitted.

There's a loss of precision when storing linear pixels in 8-bit
channels. For this reason, this patch also adds supports for 10-bit
surfaces. For now, this is disabled by default since such surfaces
only have 2 bits for alpha. It can be enabled with
tweak.surface-bit-depth=10-bit.

Perhaps, in the future, we can enable it by default if:

* gamma-correct blending is enabled
* the user has not enabled a transparent background
This commit is contained in:
Daniel Eklöf 2025-02-21 11:01:29 +01:00
parent 6d39f66eb7
commit ccf625b991
No known key found for this signature in database
GPG key ID: 5BBD4992C116573F
20 changed files with 746 additions and 101 deletions

182
wayland.c
View file

@ -237,6 +237,15 @@ seat_destroy(struct seat *seat)
static void
shm_format(void *data, struct wl_shm *wl_shm, uint32_t format)
{
struct wayland *wayl = data;
switch (format) {
case WL_SHM_FORMAT_XRGB2101010: wayl->shm_have_xrgb2101010 = true; break;
case WL_SHM_FORMAT_ARGB2101010: wayl->shm_have_argb2101010 = true; break;
case WL_SHM_FORMAT_XBGR2101010: wayl->shm_have_xbgr2101010 = true; break;
case WL_SHM_FORMAT_ABGR2101010: wayl->shm_have_abgr2101010 = true; break;
}
#if defined(_DEBUG)
bool have_description = false;
@ -666,6 +675,91 @@ static const struct wp_presentation_listener presentation_listener = {
.clock_id = &clock_id,
};
#if defined(HAVE_WP_COLOR_MANAGEMENT)
static void
color_manager_create_image_description(struct wayland *wayl)
{
if (!wayl->color_management.have_feat_parametric)
return;
if (!wayl->color_management.have_primaries_srgb)
return;
if (!wayl->color_management.have_tf_ext_linear)
return;
struct wp_image_description_creator_params_v1 *params =
wp_color_manager_v1_create_parametric_creator(wayl->color_management.manager);
wp_image_description_creator_params_v1_set_tf_named(
params, WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR);
wp_image_description_creator_params_v1_set_primaries_named(
params, WP_COLOR_MANAGER_V1_PRIMARIES_SRGB);
wayl->color_management.img_description =
wp_image_description_creator_params_v1_create(params);
}
static void
color_manager_supported_intent(void *data,
struct wp_color_manager_v1 *wp_color_manager_v1,
uint32_t render_intent)
{
struct wayland *wayl = data;
if (render_intent == WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL)
wayl->color_management.have_intent_perceptual = true;
}
static void
color_manager_supported_feature(void *data,
struct wp_color_manager_v1 *wp_color_manager_v1,
uint32_t feature)
{
struct wayland *wayl = data;
if (feature == WP_COLOR_MANAGER_V1_FEATURE_PARAMETRIC)
wayl->color_management.have_feat_parametric = true;
}
static void
color_manager_supported_tf_named(void *data,
struct wp_color_manager_v1 *wp_color_manager_v1,
uint32_t tf)
{
struct wayland *wayl = data;
if (tf == WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR)
wayl->color_management.have_tf_ext_linear = true;
}
static void
color_manager_supported_primaries_named(void *data,
struct wp_color_manager_v1 *wp_color_manager_v1,
uint32_t primaries)
{
struct wayland *wayl = data;
if (primaries == WP_COLOR_MANAGER_V1_PRIMARIES_SRGB)
wayl->color_management.have_primaries_srgb = true;
}
static void
color_manager_done(void *data,
struct wp_color_manager_v1 *wp_color_manager_v1)
{
struct wayland *wayl = data;
color_manager_create_image_description(wayl);
}
static const struct wp_color_manager_v1_listener color_manager_listener = {
.supported_intent = &color_manager_supported_intent,
.supported_feature = &color_manager_supported_feature,
.supported_primaries_named = &color_manager_supported_primaries_named,
.supported_tf_named = &color_manager_supported_tf_named,
.done = &color_manager_done,
};
#endif
static bool
verify_iface_version(const char *iface, uint32_t version, uint32_t wanted)
{
@ -1385,6 +1479,20 @@ handle_global(void *data, struct wl_registry *registry,
}
#endif
#if defined(HAVE_WP_COLOR_MANAGEMENT)
else if (streq(interface, wp_color_manager_v1_interface.name)) {
const uint32_t required = 1;
if (!verify_iface_version(interface, version, required))
return;
wayl->color_management.manager = wl_registry_bind(
wayl->registry, name, &wp_color_manager_v1_interface, required);
wp_color_manager_v1_add_listener(
wayl->color_management.manager, &color_manager_listener, wayl);
}
#endif
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
else if (streq(interface, zwp_text_input_manager_v3_interface.name)) {
const uint32_t required = 1;
@ -1707,6 +1815,13 @@ wayl_destroy(struct wayland *wayl)
zwp_text_input_manager_v3_destroy(wayl->text_input_manager);
#endif
#if defined(HAVE_WP_COLOR_MANAGEMENT)
if (wayl->color_management.img_description != NULL)
wp_image_description_v1_destroy(wayl->color_management.img_description);
if (wayl->color_management.manager != NULL)
wp_color_manager_v1_destroy(wayl->color_management.manager);
#endif
#if defined(HAVE_XDG_SYSTEM_BELL)
if (wayl->system_bell != NULL)
xdg_system_bell_v1_destroy(wayl->system_bell);
@ -1847,6 +1962,38 @@ wayl_win_init(struct terminal *term, const char *token)
}
#endif
#if defined(HAVE_WP_COLOR_MANAGEMENT)
if (term->conf->gamma_correct != GAMMA_CORRECT_DISABLED) {
if (wayl->color_management.img_description != NULL) {
xassert(wayl->color_management.manager != NULL);
win->surface.color_management = wp_color_manager_v1_get_surface(
term->wl->color_management.manager, win->surface.surf);
wp_color_management_surface_v1_set_image_description(
win->surface.color_management, wayl->color_management.img_description,
WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL);
} else if (term->conf->gamma_correct == GAMMA_CORRECT_ENABLED) {
if (wayl->color_management.manager == NULL) {
LOG_WARN(
"gamma-corrected-blending: disabling; "
"compositor does not implement the color-management protocol");
} else {
LOG_WARN(
"gamma-corrected-blending: disabling; "
"compositor does not implement all required color-management features");
LOG_WARN("use e.g. 'wayland-info' and verify the compositor implements:");
LOG_WARN(" - feature: parametric");
LOG_WARN(" - render intent: perceptual");
LOG_WARN(" - TF: ext_linear");
LOG_WARN(" - primaries: sRGB");
}
} else {
/* "auto" - don't warn */
}
}
#endif
if (conf->csd.preferred == CONF_CSD_PREFER_NONE) {
/* User specifically do *not* want decorations */
win->csd_mode = CSD_NO;
@ -1861,8 +2008,8 @@ wayl_win_init(struct terminal *term, const char *token)
zxdg_toplevel_decoration_v1_set_mode(
win->xdg_toplevel_decoration,
(conf->csd.preferred == CONF_CSD_PREFER_SERVER
? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE
: ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE));
? ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE
: ZXDG_TOPLEVEL_DECORATION_V1_MODE_CLIENT_SIDE));
zxdg_toplevel_decoration_v1_add_listener(
win->xdg_toplevel_decoration, &xdg_toplevel_decoration_listener, win);
@ -1987,7 +2134,12 @@ wayl_win_destroy(struct wl_window *win)
free(it->item);
tll_remove(win->xdg_tokens, it);
}
}
#if defined(HAVE_WP_COLOR_MANAGEMENT)
if (win->surface.color_management != NULL)
wp_color_management_surface_v1_destroy(win->surface.color_management);
#endif
if (win->fractional_scale != NULL)
wp_fractional_scale_v1_destroy(win->fractional_scale);
@ -2308,6 +2460,7 @@ wayl_win_subsurface_new_with_custom_parent(
struct wayland *wayl = win->term->wl;
surf->surface.surf = NULL;
surf->surface.viewport = NULL;
surf->sub = NULL;
struct wl_surface *main_surface
@ -2318,6 +2471,22 @@ wayl_win_subsurface_new_with_custom_parent(
return false;
}
#if defined(HAVE_WP_COLOR_MANAGEMENT)
surf->surface.color_management = NULL;
if (win->term->conf->gamma_correct &&
wayl->color_management.img_description != NULL)
{
xassert(wayl->color_management.manager != NULL);
surf->surface.color_management = wp_color_manager_v1_get_surface(
wayl->color_management.manager, main_surface);
wp_color_management_surface_v1_set_image_description(
surf->surface.color_management, wayl->color_management.img_description,
WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL);
}
#endif
struct wl_subsurface *sub = wl_subcompositor_get_subsurface(
wayl->sub_compositor, main_surface, parent);
@ -2369,6 +2538,13 @@ wayl_win_subsurface_destroy(struct wayl_sub_surface *surf)
if (surf == NULL)
return;
#if defined(HAVE_WP_COLOR_MANAGEMENT)
if (surf->surface.color_management != NULL) {
wp_color_management_surface_v1_destroy(surf->surface.color_management);
surf->surface.color_management = NULL;
}
#endif
if (surf->surface.viewport != NULL) {
wp_viewport_destroy(surf->surface.viewport);
surf->surface.viewport = NULL;