diff --git a/CHANGELOG.md b/CHANGELOG.md index 59d69f3e..152b7728 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,11 @@ * `search-bindings.delete-to-start` and `search-bindings.delete-to-end` key bindings, defaulting to `Control+u` and `Control+k` respectively ([#1972][1972]). +* Gamma-correct font rendering. Requires compositor support + (`wp_color_management_v1`, and specifically, the `ext_linear` + transfer function). Enabled by default when compositor support is + available. Can be explicitly enabled or disabled with + `gamma-correct-blending=no|yes`. [1386]: https://codeberg.org/dnkl/foot/issues/1386 [1872]: https://codeberg.org/dnkl/foot/issues/1872 @@ -87,6 +92,7 @@ * Auto-detection of URLs (i.e. not OSC-8 based URLs) are now regex based. * Rename Tokyo Night Day theme to Tokyo Night Light and update colors. +* fcft >= 3.2.0 is now required. [1925]: https://codeberg.org/dnkl/foot/issues/1925 diff --git a/client.c b/client.c index dabcc327..ceee1b29 100644 --- a/client.c +++ b/client.c @@ -67,13 +67,14 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %csystem-bell %cassertions", + "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %csystem-bell %ccolor-management %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', feature_xdg_toplevel_icon() ? '+' : '-', feature_xdg_system_bell() ? '+' : '-', + feature_wp_color_management() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } diff --git a/config.c b/config.c index df7f0031..1f287250 100644 --- a/config.c +++ b/config.c @@ -1107,6 +1107,28 @@ parse_section_main(struct context *ctx) return true; } + else if (streq(key, "gamma-correct-blending")) { + bool gamma_correct; + if (!value_to_bool(ctx, &gamma_correct)) + return false; + +#if defined(HAVE_WP_COLOR_MANAGEMENT) + conf->gamma_correct = + gamma_correct + ? GAMMA_CORRECT_ENABLED + : GAMMA_CORRECT_DISABLED; + return true; +#else + if (gamma_correct) { + LOG_CONTEXTUAL_WARN( + "ignoring; foot was built without color-management support"); + } + + conf->gamma_correct = GAMMA_CORRECT_DISABLED; + return true; +#endif + } + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -2767,6 +2789,16 @@ parse_section_tweak(struct context *ctx) else if (streq(key, "bold-text-in-bright-amount")) return value_to_float(ctx, &conf->bold_in_bright.amount); + else if (streq(key, "surface-bit-depth")) { + _Static_assert(sizeof(conf->tweak.surface_bit_depth) == sizeof(int), + "enum is not 32-bit"); + + return value_to_enum( + ctx, + (const char *[]){"8-bit", "10-bit", NULL}, + (int *)&conf->tweak.surface_bit_depth); + } + else { LOG_CONTEXTUAL_ERR("not a valid option: %s", key); return false; @@ -3300,6 +3332,11 @@ config_load(struct config *conf, const char *conf_path, .underline_thickness = {.pt = 0., .px = -1}, .strikeout_thickness = {.pt = 0., .px = -1}, .dpi_aware = false, +#if defined(HAVE_WP_COLOR_MANAGEMENT) + .gamma_correct = GAMMA_CORRECT_AUTO, +#else + .gamma_correct = GAMMA_CORRECT_DISABLED, +#endif .security = { .osc52 = OSC52_ENABLED, }, @@ -3408,6 +3445,7 @@ config_load(struct config *conf, const char *conf_path, .box_drawing_solid_shades = true, .font_monospace_warn = true, .sixel = true, + .surface_bit_depth = 8, }, .touch = { diff --git a/config.h b/config.h index deddcf04..fb019d90 100644 --- a/config.h +++ b/config.h @@ -164,6 +164,9 @@ struct config { enum { STARTUP_WINDOWED, STARTUP_MAXIMIZED, STARTUP_FULLSCREEN } startup_mode; bool dpi_aware; + enum {GAMMA_CORRECT_DISABLED, + GAMMA_CORRECT_ENABLED, + GAMMA_CORRECT_AUTO} gamma_correct; struct config_font_list fonts[4]; struct font_size_adjustment font_size_adjustment; @@ -397,6 +400,7 @@ struct config { bool box_drawing_solid_shades; bool font_monospace_warn; bool sixel; + enum { SHM_8_BIT, SHM_10_BIT } surface_bit_depth; } tweak; struct { diff --git a/doc/foot.ini.5.scd b/doc/foot.ini.5.scd index c55419d1..952c7ae2 100644 --- a/doc/foot.ini.5.scd +++ b/doc/foot.ini.5.scd @@ -198,6 +198,35 @@ empty string to be set, but it must be quoted: *KEY=""*) Default: _unset_ +*gamma-correct-blending* + Boolean. When enabled, foot will do gamma-correct blending in + linear color space. This is how font glyphs are supposed to be + rendered, but since nearly no applications or toolkits are doing + it on Linux, the result may not look like you are used to. + + Compared to the default (disabled), bright glyphs on a dark + background will appear thicker, and dark glyphs on a light + background will appear thinner. + + Also be aware that many fonts have been developed on systems that + do not do gamma-correct blending, and may therefore look thicker + than intended when rendered with gamma-correct blending, since the + font designer set the font weight based on incorrect rendering. + + FreeType can limit the effect of the latter, with a technique + called stem darkening. It is only available for CFF fonts + (OpenType, .otf) and disabled by default (in FreeType). You can + enable it by setting the environment variable + *FREETYPE_PROPERTIES="cff:no-stem-darkening=0"* before starting + foot. + + You may also want to enable 10-bit image buffers when + gamma-correct blending is enabled. Though probably only if you do + not use a transparent background (with 10-bit buffers, you only + get 2 bits alpha). See *tweak.surface-bit-depth*. + + Default: enabled when compositor support is available + *box-drawings-uses-font-glyphs* Boolean. When disabled, foot generates box/line drawing characters itself. The are several advantages to doing this instead of using @@ -1917,6 +1946,23 @@ any of these options. *bold-text-in-bright* is set to *yes* (the *palette-based* variant is not affected by this option). Default: _1.3_. +*surface-bit-depth* + Selects which RGB bit depth to use for image buffers. One of + *8-bit*, or *10-bit*. + + The default, *8-bit*, uses 8 bits for all channels, alpha + included. When *gamma-correct-blending* is disabled, this is the + best option. + + When *gamma-correct-blending* is enabled, you may want to enable + 10-bit surfaces, as that improves the color resolution. Be aware + however, that in this mode, the alpha channel is only 2 bits + instead of 8 bits. Thus, if you are using a transparent + background, you may want to use the default, *8-bit*, even if you + have gamma-correct blending enabled. + + Default: _8-bit_ + # SEE ALSO *foot*(1), *footclient*(1) diff --git a/foot-features.h b/foot-features.h index 0eef5eac..c6c9c6f4 100644 --- a/foot-features.h +++ b/foot-features.h @@ -55,3 +55,12 @@ static inline bool feature_xdg_system_bell(void) return false; #endif } + +static inline bool feature_wp_color_management(void) +{ +#if defined(HAVE_WP_COLOR_MANAGEMENT) + return true; +#else + return false; +#endif +} diff --git a/main.c b/main.c index 0ba574c9..1a001186 100644 --- a/main.c +++ b/main.c @@ -51,13 +51,14 @@ version_and_features(void) { static char buf[256]; snprintf(buf, sizeof(buf), - "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %csystem-bell %cassertions", + "version: %s %cpgo %cime %cgraphemes %ctoplevel-icon %csystem-bell %ccolor-management %cassertions", FOOT_VERSION, feature_pgo() ? '+' : '-', feature_ime() ? '+' : '-', feature_graphemes() ? '+' : '-', feature_xdg_toplevel_icon() ? '+' : '-', feature_xdg_system_bell() ? '+' : '-', + feature_wp_color_management() ? '+' : '-', feature_assertions() ? '+' : '-'); return buf; } diff --git a/meson.build b/meson.build index 27ea3d53..6505460f 100644 --- a/meson.build +++ b/meson.build @@ -146,7 +146,7 @@ if utf8proc.found() endif tllist = dependency('tllist', version: '>=1.1.0', fallback: 'tllist') -fcft = dependency('fcft', version: ['>=3.0.1', '<4.0.0'], fallback: 'fcft') +fcft = dependency('fcft', version: ['>=3.2.0', '<4.0.0'], fallback: 'fcft') wayland_protocols_datadir = wayland_protocols.get_variable('pkgdatadir') @@ -187,6 +187,13 @@ else xdg_system_bell = false endif +if wayland_protocols.version().version_compare('>=1.41') + add_project_arguments('-DHAVE_WP_COLOR_MANAGEMENT', language: 'c') + wl_proto_xml += [wayland_protocols_datadir / 'staging/color-management/color-management-v1.xml'] + wp_color_management = true +else + wp_color_management = false +endif foreach prot : wl_proto_xml wl_proto_headers += custom_target( @@ -228,6 +235,13 @@ emoji_variation_sequences = custom_target( command: [python, generate_emoji_variation_sequences, '@INPUT@', '@OUTPUT@'] ) +generate_srgb_funcs = files('scripts/srgb.py') +srgb_funcs = custom_target( + 'generate_srgb_funcs', + output: ['srgb.c', 'srgb.h'], + command: [python, generate_srgb_funcs, '@OUTPUT0@', '@OUTPUT1@'] +) + common = static_library( 'common', 'log.c', 'log.h', @@ -260,7 +274,7 @@ vtlib = static_library( 'osc.c', 'osc.h', 'sixel.c', 'sixel.h', 'vt.c', 'vt.h', - builtin_terminfo, emoji_variation_sequences, + builtin_terminfo, emoji_variation_sequences, srgb_funcs, wl_proto_src + wl_proto_headers, version, dependencies: [libepoll, pixman, fcft, tllist, wayland_client, xkb, utf8proc], @@ -424,6 +438,7 @@ summary( 'Grapheme clustering': utf8proc.found(), 'Wayland: xdg-toplevel-icon-v1': xdg_toplevel_icon, 'Wayland: xdg-system-bell-v1': xdg_system_bell, + 'Wayland: wp-color-management-v1': wp_color_management, 'utmp backend': utmp_backend, 'utmp helper default path': utmp_default_helper_path, 'Build terminfo': tic.found(), diff --git a/pgo/pgo.c b/pgo/pgo.c index 88e862b8..8a4967ba 100644 --- a/pgo/pgo.c +++ b/pgo/pgo.c @@ -128,6 +128,12 @@ render_worker_thread(void *_ctx) return 0; } +bool +render_do_linear_blending(const struct terminal *term) +{ + return false; +} + struct extraction_context * extract_begin(enum selection_kind kind, bool strip_trailing_empty) { @@ -197,7 +203,9 @@ void shm_unref(struct buffer *buf) {} void shm_chain_free(struct buffer_chain *chain) {} struct buffer_chain * -shm_chain_new(struct wl_shm *shm, bool scrollable, size_t pix_instances) +shm_chain_new( + struct wayland *wayl, bool scrollable, size_t pix_instances, + bool ten_bit_it_if_capable) { return NULL; } diff --git a/quirks.c b/quirks.c index 9769f1ff..7cc8a8f1 100644 --- a/quirks.c +++ b/quirks.c @@ -86,6 +86,7 @@ is_sway(void) void quirk_sway_subsurface_unmap(struct terminal *term) { + return; if (!is_sway()) return; diff --git a/render.c b/render.c index 701cefae..eea43c10 100644 --- a/render.c +++ b/render.c @@ -44,6 +44,7 @@ #include "selection.h" #include "shm.h" #include "sixel.h" +#include "srgb.h" #include "url-mode.h" #include "util.h" #include "xmalloc.h" @@ -232,22 +233,45 @@ attrs_to_font(const struct terminal *term, const struct attributes *attrs) return term->fonts[idx]; } -static inline pixman_color_t -color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha) +static pixman_color_t +color_hex_to_pixman_srgb(uint32_t color, uint16_t alpha) { return (pixman_color_t){ - .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)) * alpha / 0xffff, - .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)) * alpha / 0xffff, - .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)) * alpha / 0xffff, - .alpha = alpha, + .alpha = alpha, /* Consider alpha linear already? */ + .red = srgb_decode_8_to_16((color >> 16) & 0xff), + .green = srgb_decode_8_to_16((color >> 8) & 0xff), + .blue = srgb_decode_8_to_16((color >> 0) & 0xff), }; } static inline pixman_color_t -color_hex_to_pixman(uint32_t color) +color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha, bool srgb) +{ + pixman_color_t ret; + + if (srgb) + ret = color_hex_to_pixman_srgb(color, alpha); + else { + ret = (pixman_color_t){ + .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)), + .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)), + .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)), + .alpha = alpha, + }; + } + + ret.red = (uint32_t)ret.red * alpha / 0xffff; + ret.green = (uint32_t)ret.green * alpha / 0xffff; + ret.blue = (uint32_t)ret.blue * alpha / 0xffff; + + return ret; +} + +static inline pixman_color_t +color_hex_to_pixman(uint32_t color, bool srgb) { /* Count on the compiler optimizing this */ - return color_hex_to_pixman_with_alpha(color, 0xffff); + return color_hex_to_pixman_with_alpha(color, 0xffff, srgb); } static inline uint32_t @@ -568,23 +592,24 @@ draw_strikeout(const struct terminal *term, pixman_image_t *pix, static void cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, - const pixman_color_t *fg, const pixman_color_t *bg, - pixman_color_t *cursor_color, pixman_color_t *text_color) + const pixman_color_t *fg, const pixman_color_t *bg, + pixman_color_t *cursor_color, pixman_color_t *text_color, + bool gamma_correct) { if (term->colors.cursor_bg >> 31) - *cursor_color = color_hex_to_pixman(term->colors.cursor_bg); + *cursor_color = color_hex_to_pixman(term->colors.cursor_bg, gamma_correct); else *cursor_color = *fg; if (term->colors.cursor_fg >> 31) - *text_color = color_hex_to_pixman(term->colors.cursor_fg); + *text_color = color_hex_to_pixman(term->colors.cursor_fg, gamma_correct); else { *text_color = *bg; if (unlikely(text_color->alpha != 0xffff)) { /* The *only* color that can have transparency is the * default background color */ - *text_color = color_hex_to_pixman(term->colors.bg); + *text_color = color_hex_to_pixman(term->colors.bg, gamma_correct); } } @@ -592,8 +617,8 @@ cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, text_color->green == cursor_color->green && text_color->blue == cursor_color->blue) { - *text_color = color_hex_to_pixman(term->colors.bg); - *cursor_color = color_hex_to_pixman(term->colors.fg); + *text_color = color_hex_to_pixman(term->colors.bg, gamma_correct); + *cursor_color = color_hex_to_pixman(term->colors.fg, gamma_correct); } } @@ -604,7 +629,8 @@ draw_cursor(const struct terminal *term, const struct cell *cell, { pixman_color_t cursor_color; pixman_color_t text_color; - cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color); + cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color, + render_do_linear_blending(term)); if (unlikely(!term->kbd_focus)) { switch (term->conf->cursor.unfocused_style) { @@ -656,8 +682,9 @@ draw_cursor(const struct terminal *term, const struct cell *cell, } static int -render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, - struct row *row, int row_no, int col, bool has_cursor) +render_cell(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, struct row *row, int row_no, int col, + bool has_cursor) { struct cell *cell = &row->cells[col]; if (cell->attrs.clean) @@ -776,8 +803,9 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag if (cell->attrs.blink && term->blink.state == BLINK_OFF) _fg = color_decrease_luminance(_fg); - pixman_color_t fg = color_hex_to_pixman(_fg); - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); + const bool gamma_correct = render_do_linear_blending(term); + pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); struct fcft_font *font = attrs_to_font(term, &cell->attrs); const struct composed *composed = NULL; @@ -987,7 +1015,7 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag if (i > 0 && glyph->x >= 0 && cell_cols == 1) g_x -= term->cell_width; - if (unlikely(pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8)) { + if (unlikely(glyph->is_color_glyph)) { /* Glyph surface is a pre-rendered image (typically a color emoji...) */ if (!(cell->attrs.blink && term->blink.state == BLINK_OFF)) { pixman_image_composite32( @@ -1071,12 +1099,12 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag switch (range->underline.color_src) { case COLOR_BASE256: underline_color = color_hex_to_pixman( - term->colors.table[range->underline.color]); + term->colors.table[range->underline.color], gamma_correct); break; case COLOR_RGB: underline_color = - color_hex_to_pixman(range->underline.color); + color_hex_to_pixman(range->underline.color, gamma_correct); break; case COLOR_DEFAULT: @@ -1105,8 +1133,8 @@ render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damag pixman_color_t url_color = color_hex_to_pixman( term->conf->colors.use_custom.url ? term->conf->colors.url - : term->colors.table[3] - ); + : term->colors.table[3], + gamma_correct); draw_underline(term, pix, font, &url_color, x, y, cell_cols); } @@ -1119,8 +1147,9 @@ draw_cursor: } static void -render_row(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, - struct row *row, int row_no, int cursor_col) +render_row(struct terminal *term, pixman_image_t *pix, + pixman_region32_t *damage, struct row *row, + int row_no, int cursor_col) { for (int col = term->cols - 1; col >= 0; col--) render_cell(term, pix, damage, row, row_no, col, cursor_col == col); @@ -1130,7 +1159,7 @@ static void render_urgency(struct terminal *term, struct buffer *buf) { uint32_t red = term->colors.table[1]; - pixman_color_t bg = color_hex_to_pixman(red); + pixman_color_t bg = color_hex_to_pixman(red, render_do_linear_blending(term)); int width = min(min(term->margins.left, term->margins.right), min(term->margins.top, term->margins.bottom)); @@ -1161,6 +1190,7 @@ render_margin(struct terminal *term, struct buffer *buf, const int bmargin = term->height - term->margins.bottom; const int line_count = end_line - start_line; + const bool gamma_correct = render_do_linear_blending(term); const uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; uint16_t alpha = term->colors.alpha; @@ -1169,7 +1199,7 @@ render_margin(struct terminal *term, struct buffer *buf, alpha = 0xffff; } - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &bg, 4, @@ -1596,8 +1626,7 @@ render_sixel(struct terminal *term, pixman_image_t *pix, static void render_sixel_images(struct terminal *term, pixman_image_t *pix, - pixman_region32_t *damage, - const struct coord *cursor) + pixman_region32_t *damage, const struct coord *cursor) { if (likely(tll_length(term->grid->sixel_images)) == 0) return; @@ -1649,6 +1678,8 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, if (unlikely(term->is_searching)) return; + const bool gamma_correct = render_do_linear_blending(term); + /* Adjust cursor position to viewport */ struct coord cursor; cursor = term->grid->cursor.point; @@ -1753,12 +1784,12 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, if (!seat->ime.preedit.cursor.hidden) { const struct cell *start_cell = &seat->ime.preedit.cells[0]; - pixman_color_t fg = color_hex_to_pixman(term->colors.fg); - pixman_color_t bg = color_hex_to_pixman(term->colors.bg); + pixman_color_t fg = color_hex_to_pixman(term->colors.fg, gamma_correct); + pixman_color_t bg = color_hex_to_pixman(term->colors.bg, gamma_correct); pixman_color_t cursor_color, text_color; cursor_colors_for_cell( - term, start_cell, &fg, &bg, &cursor_color, &text_color); + term, start_cell, &fg, &bg, &cursor_color, &text_color, gamma_correct); int x = term->margins.left + (col_idx + start) * term->cell_width; int y = term->margins.top + row_idx * term->cell_height; @@ -1789,12 +1820,14 @@ render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, row->cells[col_idx + i] = real_cells[i]; free(real_cells); + const int damage_x = term->margins.left + col_idx * term->cell_width; + const int damage_y = term->margins.top + row_idx * term->cell_height; + const int damage_w = cells_used * term->cell_width; + const int damage_h = term->cell_height; + wl_surface_damage_buffer( term->window->surface.surf, - term->margins.left, - term->margins.top + row_idx * term->cell_height, - term->width - term->margins.left - term->margins.right, - 1 * term->cell_height); + damage_x, damage_y, damage_w, damage_h); } #endif @@ -1916,7 +1949,7 @@ render_overlay(struct terminal *term) case OVERLAY_FLASH: color = color_hex_to_pixman_with_alpha( term->conf->colors.flash, - term->conf->colors.flash_alpha); + term->conf->colors.flash_alpha, render_do_linear_blending(term)); break; case OVERLAY_NONE: @@ -2118,6 +2151,7 @@ render_worker_thread(void *_ctx) sem_wait(start); struct buffer *buf = term->render.workers.buf; + bool frame_done = false; /* Translate offset-relative cursor row to view-relative */ @@ -2138,8 +2172,6 @@ render_worker_thread(void *_ctx) switch (row_no) { default: { - xassert(buf != NULL); - struct row *row = grid_row_in_view(term->grid, row_no); int cursor_col = cursor.row == row_no ? cursor.col : -1; @@ -2259,13 +2291,14 @@ render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, pixman_image_set_clip_region32(buf->pix[0], &clip); pixman_region32_fini(&clip); + const bool gamma_correct = render_do_linear_blending(term); uint16_t alpha = _bg >> 24 | (_bg >> 24 << 8); - pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); + pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &bg, 1, &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); - pixman_color_t fg = color_hex_to_pixman(_fg); + pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); const int x_ofs = term->font_x_ofs; const size_t len = c32len(text); @@ -2312,7 +2345,7 @@ render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, for (size_t i = 0; i < glyph_count; i++) { const struct fcft_glyph *glyph = glyphs[i]; - if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { + if (unlikely(glyph->is_color_glyph)) { pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, x + x_ofs + glyph->x, y - glyph->y, @@ -2399,8 +2432,11 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, if (info->width == 0 || info->height == 0) return; + const bool gamma_correct = render_do_linear_blending(term); + { - pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0); + /* Fully transparent - no need to do a color space transform */ + pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0, gamma_correct); render_csd_part(term, surf->surf, buf, info->width, info->height, &color); } @@ -2461,7 +2497,8 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, _color = color_dim(term, _color); uint16_t alpha = _color >> 24 | (_color >> 24 << 8); - pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); + pixman_color_t color = + color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 1, @@ -2472,8 +2509,9 @@ render_csd_border(struct terminal *term, enum csd_surface surf_idx, } static pixman_color_t -get_csd_button_fg_color(const struct config *conf) +get_csd_button_fg_color(const struct terminal *term) { + const struct config *conf = term->conf; uint32_t _color = conf->colors.bg; uint16_t alpha = 0xffff; @@ -2482,13 +2520,14 @@ get_csd_button_fg_color(const struct config *conf) alpha = _color >> 24 | (_color >> 24 << 8); } - return color_hex_to_pixman_with_alpha(_color, alpha); + return color_hex_to_pixman_with_alpha( + _color, alpha, render_do_linear_blending(term)); } static void render_csd_button_minimize(struct terminal *term, struct buffer *buf) { - pixman_color_t color = get_csd_button_fg_color(term->conf); + pixman_color_t color = get_csd_button_fg_color(term); pixman_image_t *src = pixman_image_create_solid_fill(&color); const int max_height = buf->height / 3; @@ -2516,7 +2555,7 @@ static void render_csd_button_maximize_maximized( struct terminal *term, struct buffer *buf) { - pixman_color_t color = get_csd_button_fg_color(term->conf); + pixman_color_t color = get_csd_button_fg_color(term); pixman_image_t *src = pixman_image_create_solid_fill(&color); const int max_height = buf->height / 3; @@ -2548,7 +2587,7 @@ static void render_csd_button_maximize_window( struct terminal *term, struct buffer *buf) { - pixman_color_t color = get_csd_button_fg_color(term->conf); + pixman_color_t color = get_csd_button_fg_color(term); pixman_image_t *src = pixman_image_create_solid_fill(&color); const int max_height = buf->height / 3; @@ -2588,7 +2627,7 @@ render_csd_button_maximize(struct terminal *term, struct buffer *buf) static void render_csd_button_close(struct terminal *term, struct buffer *buf) { - pixman_color_t color = get_csd_button_fg_color(term->conf); + pixman_color_t color = get_csd_button_fg_color(term); pixman_image_t *src = pixman_image_create_solid_fill(&color); const int max_height = buf->height / 3; @@ -2759,14 +2798,14 @@ render_csd_button(struct terminal *term, enum csd_surface surf_idx, if (!term->visual_focus) _color = color_dim(term, _color); - pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); + const bool gamma_correct = render_do_linear_blending(term); + pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); render_csd_part(term, surf->surf, buf, info->width, info->height, &color); switch (surf_idx) { case CSD_SURF_MINIMIZE: render_csd_button_minimize(term, buf); break; case CSD_SURF_MAXIMIZE: render_csd_button_maximize(term, buf); break; case CSD_SURF_CLOSE: render_csd_button_close(term, buf); break; - break; default: BUG("unhandled surface type: %u", (unsigned)surf_idx); @@ -3618,6 +3657,7 @@ render_search_box(struct terminal *term) : term->conf->colors.use_custom.search_box_no_match; /* Background - yellow on empty/match, red on mismatch (default) */ + const bool gamma_correct = render_do_linear_blending(term); const pixman_color_t color = color_hex_to_pixman( is_match ? (custom_colors @@ -3625,13 +3665,14 @@ render_search_box(struct terminal *term) : term->colors.table[3]) : (custom_colors ? term->conf->colors.search_box.no_match.bg - : term->colors.table[1])); + : term->colors.table[1]), + gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 1, &(pixman_rectangle16_t){width - visible_width, 0, visible_width, height}); - pixman_color_t transparent = color_hex_to_pixman_with_alpha(0, 0); + pixman_color_t transparent = color_hex_to_pixman_with_alpha(0, 0, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &transparent, 1, &(pixman_rectangle16_t){0, 0, width - visible_width, height}); @@ -3641,12 +3682,14 @@ render_search_box(struct terminal *term) const int x_ofs = term->font_x_ofs; int x = x_left; int y = margin; + pixman_color_t fg = color_hex_to_pixman( custom_colors ? (is_match ? term->conf->colors.search_box.match.fg : term->conf->colors.search_box.no_match.fg) - : term->colors.table[0]); + : term->colors.table[0], + gamma_correct); /* Move offset we start rendering at, to ensure the cursor is visible */ for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; cell_idx += widths[i], i++) { @@ -3802,8 +3845,7 @@ render_search_box(struct terminal *term) continue; } - if (unlikely(pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8)) { - /* Glyph surface is a pre-rendered image (typically a color emoji...) */ + if (unlikely(glyph->is_color_glyph)) { pixman_image_composite32( PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, x + x_ofs + glyph->x, y + term->font_baseline - glyph->y, @@ -5186,3 +5228,14 @@ render_xcursor_set(struct seat *seat, struct terminal *term, seat->pointer.xcursor_pending = true; return true; } + +bool +render_do_linear_blending(const struct terminal *term) +{ +#if defined(HAVE_WP_COLOR_MANAGEMENT) + return term->conf->gamma_correct != GAMMA_CORRECT_DISABLED && + term->wl->color_management.img_description != NULL; +#else + return false; +#endif +} diff --git a/render.h b/render.h index 81d2a905..c7b8e4a5 100644 --- a/render.h +++ b/render.h @@ -47,3 +47,5 @@ struct csd_data { }; struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx); + +bool render_do_linear_blending(const struct terminal *term); diff --git a/scripts/srgb.py b/scripts/srgb.py new file mode 100755 index 00000000..7655dbe4 --- /dev/null +++ b/scripts/srgb.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 + +import argparse +import math +import sys + + +def srgb_to_linear(f: float) -> float: + assert(f >= 0 and f <= 1.0) + + if f <= 0.04045: + return f / 12.92 + + return math.pow((f + 0.055) / 1.055, 2.4) + + +def linear_to_srgb(f: float) -> float: + if f < 0.0031308: + return f * 12.92 + + return 1.055 * math.pow(f, 1 / 2.4) - 0.055 + + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('c_output', type=argparse.FileType('w')) + parser.add_argument('h_output', type=argparse.FileType('w')) + opts = parser.parse_args() + + linear_table: list[int] = [] + srgb_table: list[int] = [] + + for i in range(256): + linear_table.append(int(srgb_to_linear(float(i) / 255) * 65535 + 0.5)) + + for i in range(4096): + srgb_table.append(int(linear_to_srgb(float(i) / 4095) * 255 + 0.5)) + + for i in range(256): + while True: + linear = linear_table[i] + srgb = srgb_table[linear >> 4] + + if i == srgb: + break + + linear_table[i] += 1 + + + opts.h_output.write("#pragma once\n") + opts.h_output.write("#include \n") + opts.h_output.write("\n") + opts.h_output.write('/* 8-bit input, 16-bit output */\n') + opts.h_output.write("extern const uint16_t srgb_decode_8_to_16_table[256];") + + opts.h_output.write('\n') + opts.h_output.write('static inline uint16_t\n') + opts.h_output.write('srgb_decode_8_to_16(uint8_t v)\n') + opts.h_output.write('{\n') + opts.h_output.write(' return srgb_decode_8_to_16_table[v];\n') + opts.h_output.write('}\n') + + opts.h_output.write('\n') + opts.h_output.write('/* 8-bit input, 8-bit output */\n') + opts.h_output.write("extern const uint8_t srgb_decode_8_to_8_table[256];\n") + + opts.h_output.write('\n') + opts.h_output.write('static inline uint8_t\n') + opts.h_output.write('srgb_decode_8_to_8(uint8_t v)\n') + opts.h_output.write('{\n') + opts.h_output.write(' return srgb_decode_8_to_8_table[v];\n') + opts.h_output.write('}\n') + + opts.c_output.write('#include "srgb.h"\n') + opts.c_output.write('\n') + + opts.c_output.write("const uint16_t srgb_decode_8_to_16_table[256] = {\n") + for i in range(256): + opts.c_output.write(f' {linear_table[i]},\n') + opts.c_output.write('};\n') + + opts.c_output.write("const uint8_t srgb_decode_8_to_8_table[256] = {\n") + for i in range(256): + opts.c_output.write(f' {linear_table[i] >> 8},\n') + opts.c_output.write('};\n') + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/shm.c b/shm.c index 9a745f6c..32e6bdd0 100644 --- a/shm.c +++ b/shm.c @@ -92,6 +92,12 @@ struct buffer_chain { struct wl_shm *shm; size_t pix_instances; bool scrollable; + + pixman_format_code_t pixman_fmt_without_alpha; + enum wl_shm_format shm_format_without_alpha; + + pixman_format_code_t pixman_fmt_with_alpha; + enum wl_shm_format shm_format_with_alpha; }; static tll(struct buffer_private *) deferred; @@ -115,6 +121,7 @@ buffer_destroy_dont_close(struct buffer *buf) if (buf->pix[i] != NULL) pixman_image_unref(buf->pix[i]); } + if (buf->wl_buf != NULL) wl_buffer_destroy(buf->wl_buf); @@ -262,7 +269,9 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) wl_buf = wl_shm_pool_create_buffer( pool->wl_pool, new_offset, buf->public.width, buf->public.height, buf->public.stride, - buf->with_alpha ? WL_SHM_FORMAT_ARGB8888 : WL_SHM_FORMAT_XRGB8888); + buf->with_alpha + ? buf->chain->shm_format_with_alpha + : buf->chain->shm_format_without_alpha); if (wl_buf == NULL) { LOG_ERR("failed to create SHM buffer"); @@ -272,9 +281,12 @@ instantiate_offset(struct buffer_private *buf, off_t new_offset) /* One pixman image for each worker thread (do we really need multiple?) */ for (size_t i = 0; i < buf->public.pix_instances; i++) { pix[i] = pixman_image_create_bits_no_clear( - buf->with_alpha ? PIXMAN_a8r8g8b8 : PIXMAN_x8r8g8b8, + buf->with_alpha + ? buf->chain->pixman_fmt_with_alpha + : buf->chain->pixman_fmt_without_alpha, buf->public.width, buf->public.height, (uint32_t *)mmapped, buf->public.stride); + if (pix[i] == NULL) { LOG_ERR("failed to create pixman image"); goto err; @@ -959,14 +971,74 @@ shm_unref(struct buffer *_buf) } struct buffer_chain * -shm_chain_new(struct wl_shm *shm, bool scrollable, size_t pix_instances) +shm_chain_new(struct wayland *wayl, bool scrollable, size_t pix_instances, + bool ten_bit_if_capable) { + pixman_format_code_t pixman_fmt_without_alpha = PIXMAN_x8r8g8b8; + enum wl_shm_format shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB8888; + + pixman_format_code_t pixman_fmt_with_alpha = PIXMAN_a8r8g8b8; + enum wl_shm_format shm_fmt_with_alpha = WL_SHM_FORMAT_ARGB8888; + + static bool have_logged = false; + + + if (ten_bit_if_capable) { + if (wayl->shm_have_argb2101010 && wayl->shm_have_xrgb2101010) { + pixman_fmt_without_alpha = PIXMAN_x2r10g10b10; + shm_fmt_without_alpha = WL_SHM_FORMAT_XRGB2101010; + + pixman_fmt_with_alpha = PIXMAN_a2r10g10b10; + shm_fmt_with_alpha = WL_SHM_FORMAT_ARGB2101010; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 10-bit RGB surfaces"); + } + } + + else if (wayl->shm_have_abgr2101010 && wayl->shm_have_xbgr2101010) { + pixman_fmt_without_alpha = PIXMAN_x2b10g10r10; + shm_fmt_without_alpha = WL_SHM_FORMAT_XBGR2101010; + + pixman_fmt_with_alpha = PIXMAN_a2b10g10r10; + shm_fmt_with_alpha = WL_SHM_FORMAT_ABGR2101010; + + if (!have_logged) { + have_logged = true; + LOG_INFO("using 10-bit BGR surfaces"); + } + } + + else { + if (!have_logged) { + have_logged = true; + + LOG_WARN( + "10-bit surfaces requested, but compositor does not " + "implement ARGB2101010+XRGB2101010, or " + "ABGR2101010+XBGR2101010. Falling back to 8-bit surfaces"); + } + } + } else { + if (!have_logged) { + have_logged = true; + LOG_INFO("using 8-bit RGB surfaces"); + } + } + struct buffer_chain *chain = xmalloc(sizeof(*chain)); *chain = (struct buffer_chain){ .bufs = tll_init(), - .shm = shm, + .shm = wayl->shm, .pix_instances = pix_instances, .scrollable = scrollable, + + .pixman_fmt_without_alpha = pixman_fmt_without_alpha, + .shm_format_without_alpha = shm_fmt_without_alpha, + + .pixman_fmt_with_alpha = pixman_fmt_with_alpha, + .shm_format_with_alpha = shm_fmt_with_alpha, }; return chain; } diff --git a/shm.h b/shm.h index b4b075ca..2af185c9 100644 --- a/shm.h +++ b/shm.h @@ -9,6 +9,8 @@ #include +#include "wayland.h" + struct damage; struct buffer { @@ -43,7 +45,8 @@ void shm_set_max_pool_size(off_t max_pool_size); struct buffer_chain; struct buffer_chain *shm_chain_new( - struct wl_shm *shm, bool scrollable, size_t pix_instances); + struct wayland *wayl, bool scrollable, size_t pix_instances, + bool ten_bit_it_if_capable); void shm_chain_free(struct buffer_chain *chain); /* diff --git a/sixel.c b/sixel.c index 44a5995b..dd933d7a 100644 --- a/sixel.c +++ b/sixel.c @@ -10,6 +10,7 @@ #include "grid.h" #include "hsl.h" #include "render.h" +#include "srgb.h" #include "util.h" #include "xmalloc.h" #include "xsnprintf.h" @@ -19,6 +20,40 @@ static size_t count; static void sixel_put_generic(struct terminal *term, uint8_t c); static void sixel_put_ar_11(struct terminal *term, uint8_t c); +static uint32_t +color_decode_srgb(const struct terminal *term, uint16_t r, uint16_t g, uint16_t b) +{ + if (term->sixel.linear_blending) { + if (term->sixel.use_10bit) { + r = srgb_decode_8_to_16(r) >> 6; + g = srgb_decode_8_to_16(g) >> 6; + b = srgb_decode_8_to_16(b) >> 6; + } else { + r = srgb_decode_8_to_8(r); + g = srgb_decode_8_to_8(g); + b = srgb_decode_8_to_8(b); + } + } else { + if (term->sixel.use_10bit) { + r <<= 2; + g <<= 2; + b <<= 2; + } + } + + uint32_t color; + + if (term->sixel.use_10bit) { + if (PIXMAN_FORMAT_TYPE(term->sixel.pixman_fmt) == PIXMAN_TYPE_ARGB) + color = 0x3u << 30 | r << 20 | g << 10 | b; + else + color = 0x3u << 30 | b << 20 | g << 10 | r; + } else + color = 0xffu << 24 | r << 16 | g << 8 | b; + + return color; +} + void sixel_fini(struct terminal *term) { @@ -75,6 +110,23 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) term->sixel.image.height = 0; term->sixel.image.alloc_height = 0; term->sixel.image.bottom_pixel = 0; + term->sixel.linear_blending = render_do_linear_blending(term); + term->sixel.pixman_fmt = PIXMAN_a8r8g8b8; + + if (term->conf->tweak.surface_bit_depth == SHM_10_BIT) { + if (term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) { + term->sixel.use_10bit = true; + term->sixel.pixman_fmt = PIXMAN_a2r10g10b10; + } + + else if (term->wl->shm_have_abgr2101010 && term->wl->shm_have_xbgr2101010) { + term->sixel.use_10bit = true; + term->sixel.pixman_fmt = PIXMAN_a2b10g10r10; + } + } + + const size_t active_palette_entries = min( + ALEN(term->conf->colors.sixel), term->sixel.palette_size); if (term->sixel.use_private_palette) { xassert(term->sixel.private_palette == NULL); @@ -83,11 +135,18 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) memcpy( term->sixel.private_palette, term->conf->colors.sixel, - min(sizeof(term->conf->colors.sixel), - term->sixel.palette_size * sizeof(term->sixel.private_palette[0]))); + active_palette_entries * sizeof(term->sixel.private_palette[0])); + + if (term->sixel.linear_blending || term->sixel.use_10bit) { + for (size_t i = 0; i < active_palette_entries; i++) { + uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; + uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; + uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; + term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); + } + } term->sixel.palette = term->sixel.private_palette; - } else { if (term->sixel.shared_palette == NULL) { term->sixel.shared_palette = xcalloc( @@ -95,8 +154,16 @@ sixel_init(struct terminal *term, int p1, int p2, int p3) memcpy( term->sixel.shared_palette, term->conf->colors.sixel, - min(sizeof(term->conf->colors.sixel), - term->sixel.palette_size * sizeof(term->sixel.shared_palette[0]))); + active_palette_entries * sizeof(term->sixel.shared_palette[0])); + + if (term->sixel.linear_blending || term->sixel.use_10bit) { + for (size_t i = 0; i < active_palette_entries; i++) { + uint8_t r = (term->sixel.private_palette[i] >> 16) & 0xff; + uint8_t g = (term->sixel.private_palette[i] >> 8) & 0xff; + uint8_t b = (term->sixel.private_palette[i] >> 0) & 0xff; + term->sixel.private_palette[i] = color_decode_srgb(term, r, g, b); + } + } } else { /* Shared palette - do *not* reset palette for new sixels */ } @@ -488,7 +555,7 @@ blend_new_image_over_old(const struct terminal *term, int stride = new_width * sizeof(uint32_t); uint32_t *new_data = xmalloc(stride * new_height); pixman_image_t *pix2 = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, new_width, new_height, new_data, stride); + term->sixel.pixman_fmt, new_width, new_height, new_data, stride); #if defined(_DEBUG) /* Fill new image with an easy-to-recognize color (green) */ @@ -651,8 +718,7 @@ sixel_overwrite(struct terminal *term, struct sixel *six, } pixman_image_t *new_pix = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, - new_width, new_height, new_data, new_width * sizeof(uint32_t)); + term->sixel.pixman_fmt, new_width, new_height, new_data, new_width * sizeof(uint32_t)); struct sixel new_six = { .pix = NULL, @@ -948,7 +1014,7 @@ sixel_sync_cache(const struct terminal *term, struct sixel *six) uint8_t *scaled_data = xmalloc(scaled_height * scaled_stride); pixman_image_t *scaled_pix = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, scaled_width, scaled_height, + term->sixel.pixman_fmt, scaled_width, scaled_height, (uint32_t *)scaled_data, scaled_stride); pixman_image_composite32( @@ -1232,7 +1298,7 @@ sixel_unhook(struct terminal *term) image.pos.row, image.pos.row + image.rows); image.original.pix = pixman_image_create_bits_no_clear( - PIXMAN_a8r8g8b8, image.original.width, image.original.height, + term->sixel.pixman_fmt, image.original.width, image.original.height, img_data, stride); pixel_row_idx += height; @@ -2006,15 +2072,14 @@ decgci(struct terminal *term, uint8_t c) } case 2: { /* RGB */ - uint8_t r = 255 * min(c1, 100) / 100; - uint8_t g = 255 * min(c2, 100) / 100; - uint8_t b = 255 * min(c3, 100) / 100; + uint16_t r = 255 * min(c1, 100) / 100; + uint16_t g = 255 * min(c2, 100) / 100; + uint16_t b = 255 * min(c3, 100) / 100; - LOG_DBG("setting palette #%d = RGB %hhu/%hhu/%hhu", + LOG_DBG("setting palette #%d = RGB %hu/%hu/%hu", term->sixel.color_idx, r, g, b); - term->sixel.palette[term->sixel.color_idx] = - 0xffu << 24 | r << 16 | g << 8 | b; + term->sixel.palette[term->sixel.color_idx] = color_decode_srgb(term, r, g, b); break; } } diff --git a/terminal.c b/terminal.c index 0cd4fbca..1c607787 100644 --- a/terminal.c +++ b/terminal.c @@ -991,6 +991,7 @@ struct font_load_data { const char **names; const char *attrs; + const struct fcft_font_options *options; struct fcft_font **font; }; @@ -998,7 +999,8 @@ static int font_loader_thread(void *_data) { struct font_load_data *data = _data; - *data->font = fcft_from_name(data->count, data->names, data->attrs); + *data->font = fcft_from_name2( + data->count, data->names, data->attrs, data->options); return *data->font != NULL; } @@ -1065,14 +1067,32 @@ reload_fonts(struct terminal *term, bool resize_grid) [1] = xstrjoin(dpi, !custom_bold ? ":weight=bold" : ""), [2] = xstrjoin(dpi, !custom_italic ? ":slant=italic" : ""), [3] = xstrjoin(dpi, !custom_bold_italic ? ":weight=bold:slant=italic" : ""), - }; + }; + + struct fcft_font_options *options = fcft_font_options_create(); + + options->color_glyphs.format = PIXMAN_a8r8g8b8; + options->color_glyphs.srgb_decode = render_do_linear_blending(term); + + if (conf->tweak.surface_bit_depth == SHM_10_BIT) { + if ((term->wl->shm_have_argb2101010 && term->wl->shm_have_xrgb2101010) || + (term->wl->shm_have_abgr2101010 && term->wl->shm_have_xbgr2101010)) + { + /* + * Use a high-res buffer type for emojis. We don't want to + * use an a2r10g0b10 type of surface, since we need more + * than 2 bits for alpha. + */ + options->color_glyphs.format = PIXMAN_rgba_float; + } + } struct fcft_font *fonts[4]; struct font_load_data data[4] = { - {count_regular, names_regular, attrs[0], &fonts[0]}, - {count_bold, names_bold, attrs[1], &fonts[1]}, - {count_italic, names_italic, attrs[2], &fonts[2]}, - {count_bold_italic, names_bold_italic, attrs[3], &fonts[3]}, + {count_regular, names_regular, attrs[0], options, &fonts[0]}, + {count_bold, names_bold, attrs[1], options, &fonts[1]}, + {count_italic, names_italic, attrs[2], options, &fonts[2]}, + {count_bold_italic, names_bold_italic, attrs[3], options, &fonts[3]}, }; thrd_t tids[4] = {0}; @@ -1097,6 +1117,8 @@ reload_fonts(struct terminal *term, bool resize_grid) success = false; } + fcft_font_options_destroy(options); + for (size_t i = 0; i < 4; i++) { for (size_t j = 0; j < counts[i]; j++) free(names[i][j]); @@ -1237,6 +1259,8 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, goto err; } + const bool ten_bit_surfaces = conf->tweak.surface_bit_depth == SHM_10_BIT; + /* Initialize configure-based terminal attributes */ *term = (struct terminal) { .fdm = fdm, @@ -1320,13 +1344,14 @@ term_init(const struct config *conf, struct fdm *fdm, struct reaper *reaper, .wl = wayl, .render = { .chains = { - .grid = shm_chain_new(wayl->shm, true, 1 + conf->render_worker_count), - .search = shm_chain_new(wayl->shm, false, 1), - .scrollback_indicator = shm_chain_new(wayl->shm, false, 1), - .render_timer = shm_chain_new(wayl->shm, false, 1), - .url = shm_chain_new(wayl->shm, false, 1), - .csd = shm_chain_new(wayl->shm, false, 1), - .overlay = shm_chain_new(wayl->shm, false, 1), + .grid = shm_chain_new(wayl, true, 1 + conf->render_worker_count, + ten_bit_surfaces), + .search = shm_chain_new(wayl, false, 1 ,ten_bit_surfaces), + .scrollback_indicator = shm_chain_new(wayl, false, 1, ten_bit_surfaces), + .render_timer = shm_chain_new(wayl, false, 1, ten_bit_surfaces), + .url = shm_chain_new(wayl, false, 1, ten_bit_surfaces), + .csd = shm_chain_new(wayl, false, 1, ten_bit_surfaces), + .overlay = shm_chain_new(wayl, false, 1, ten_bit_surfaces), }, .scrollback_lines = conf->scrollback.lines, .app_sync_updates.timer_fd = app_sync_updates_fd, @@ -1468,6 +1493,9 @@ term_window_configured(struct terminal *term) if (!term->shutdown.in_progress) { xassert(term->window->is_configured); fdm_add(term->fdm, term->ptmx, EPOLLIN, &fdm_ptmx, term); + + const bool gamma_correct = render_do_linear_blending(term); + LOG_INFO("gamma-correct blending: %s", gamma_correct ? "enabled" : "disabled"); } } diff --git a/terminal.h b/terminal.h index e03b7bf7..518e36ef 100644 --- a/terminal.h +++ b/terminal.h @@ -777,6 +777,10 @@ struct terminal { bool transparent_bg; + bool linear_blending; + bool use_10bit; + pixman_format_code_t pixman_fmt; + /* Application configurable */ unsigned palette_size; /* Number of colors in palette */ unsigned max_width; /* Maximum image width, in pixels */ diff --git a/wayland.c b/wayland.c index 3a46133f..1c083a9e 100644 --- a/wayland.c +++ b/wayland.c @@ -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; diff --git a/wayland.h b/wayland.h index b3ef5a2b..ec27281a 100644 --- a/wayland.h +++ b/wayland.h @@ -28,6 +28,10 @@ #include #endif +#if defined(HAVE_WP_COLOR_MANAGEMENT) + #include +#endif + #include #include @@ -61,6 +65,9 @@ enum touch_state { struct wayl_surface { struct wl_surface *surf; struct wp_viewport *viewport; +#if defined(HAVE_WP_COLOR_MANAGEMENT) + struct wp_color_management_surface_v1 *color_management; +#endif }; struct wayl_sub_surface { @@ -459,6 +466,17 @@ struct wayland { struct xdg_system_bell_v1 *system_bell; #endif +#if defined(HAVE_WP_COLOR_MANAGEMENT) + struct { + struct wp_color_manager_v1 *manager; + struct wp_image_description_v1 *img_description; + bool have_intent_perceptual; + bool have_feat_parametric; + bool have_tf_ext_linear; + bool have_primaries_srgb; + } color_management; +#endif + bool presentation_timings; struct wp_presentation *presentation; uint32_t presentation_clock_id; @@ -474,6 +492,11 @@ struct wayland { /* WL_SHM >= 2 */ bool use_shm_release; + + bool shm_have_argb2101010:1; + bool shm_have_xrgb2101010:1; + bool shm_have_abgr2101010:1; + bool shm_have_xbgr2101010:1; }; struct wayland *wayl_init(