A fast, lightweight and minimalistic Wayland terminal emulator
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

3721 lines
119 KiB

#include "render.h"
#include <string.h>
#include <wctype.h>
#include <unistd.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/timerfd.h>
#include <sys/epoll.h>
#include <pthread.h>
#include "macros.h"
#if HAS_INCLUDE(<pthread_np.h>)
#include <pthread_np.h>
#define pthread_setname_np(thread, name) (pthread_set_name_np(thread, name), 0)
#elif defined(__NetBSD__)
#define pthread_setname_np(thread, name) pthread_setname_np(thread, "%s", (void *)name)
#endif
#include <wayland-cursor.h>
#include <xdg-shell.h>
#include <presentation-time.h>
#include <fcft/fcft.h>
#define LOG_MODULE "render"
#define LOG_ENABLE_DBG 0
#include "log.h"
#include "box-drawing.h"
#include "config.h"
#include "grid.h"
#include "hsl.h"
#include "ime.h"
#include "quirks.h"
#include "selection.h"
#include "shm.h"
#include "sixel.h"
#include "url-mode.h"
#include "util.h"
#include "xmalloc.h"
#define TIME_SCROLL_DAMAGE 0
struct renderer {
struct fdm *fdm;
struct wayland *wayl;
};
static struct {
size_t total;
size_t zero; /* commits presented in less than one frame interval */
size_t one; /* commits presented in one frame interval */
size_t two; /* commits presented in two or more frame intervals */
} presentation_statistics = {0};
static void fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data);
struct renderer *
render_init(struct fdm *fdm, struct wayland *wayl)
{
struct renderer *renderer = malloc(sizeof(*renderer));
if (unlikely(renderer == NULL)) {
LOG_ERRNO("malloc() failed");
return NULL;
}
*renderer = (struct renderer) {
.fdm = fdm,
.wayl = wayl,
};
if (!fdm_hook_add(fdm, &fdm_hook_refresh_pending_terminals, renderer,
FDM_HOOK_PRIORITY_NORMAL))
{
LOG_ERR("failed to register FDM hook");
free(renderer);
return NULL;
}
return renderer;
}
void
render_destroy(struct renderer *renderer)
{
if (renderer == NULL)
return;
fdm_hook_del(renderer->fdm, &fdm_hook_refresh_pending_terminals,
FDM_HOOK_PRIORITY_NORMAL);
free(renderer);
}
static void DESTRUCTOR
log_presentation_statistics(void)
{
if (presentation_statistics.total == 0)
return;
const size_t total = presentation_statistics.total;
LOG_INFO("presentation statistics: zero=%f%%, one=%f%%, two=%f%%",
100. * presentation_statistics.zero / total,
100. * presentation_statistics.one / total,
100. * presentation_statistics.two / total);
}
static void
sync_output(void *data,
struct wp_presentation_feedback *wp_presentation_feedback,
struct wl_output *output)
{
}
struct presentation_context {
struct terminal *term;
struct timeval input;
struct timeval commit;
};
static void
presented(void *data,
struct wp_presentation_feedback *wp_presentation_feedback,
uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec,
uint32_t refresh, uint32_t seq_hi, uint32_t seq_lo, uint32_t flags)
{
struct presentation_context *ctx = data;
struct terminal *term = ctx->term;
const struct timeval *input = &ctx->input;
const struct timeval *commit = &ctx->commit;
const struct timeval presented = {
.tv_sec = (uint64_t)tv_sec_hi << 32 | tv_sec_lo,
.tv_usec = tv_nsec / 1000,
};
bool use_input = (input->tv_sec > 0 || input->tv_usec > 0) &&
timercmp(&presented, input, >);
char msg[1024];
int chars = 0;
if (use_input && timercmp(&presented, input, <))
return;
else if (timercmp(&presented, commit, <))
return;
LOG_DBG("commit: %lu s %lu µs, presented: %lu s %lu µs",
commit->tv_sec, commit->tv_usec, presented.tv_sec, presented.tv_usec);
if (use_input) {
struct timeval diff;
timersub(commit, input, &diff);
chars += snprintf(
&msg[chars], sizeof(msg) - chars,
"input - %llu µs -> ", (unsigned long long)diff.tv_usec);
}
struct timeval diff;
timersub(&presented, commit, &diff);
chars += snprintf(
&msg[chars], sizeof(msg) - chars,
"commit - %llu µs -> ", (unsigned long long)diff.tv_usec);
if (use_input) {
xassert(timercmp(&presented, input, >));
timersub(&presented, input, &diff);
} else {
xassert(timercmp(&presented, commit, >));
timersub(&presented, commit, &diff);
}
chars += snprintf(
&msg[chars], sizeof(msg) - chars,
"presented (total: %llu µs)", (unsigned long long)diff.tv_usec);
unsigned frame_count = 0;
if (tll_length(term->window->on_outputs) > 0) {
const struct monitor *mon = tll_front(term->window->on_outputs);
frame_count = (diff.tv_sec * 1000000. + diff.tv_usec) / (1000000. / mon->refresh);
}
presentation_statistics.total++;
if (frame_count >= 2)
presentation_statistics.two++;
else if (frame_count >= 1)
presentation_statistics.one++;
else
presentation_statistics.zero++;
#define _log_fmt "%s (more than %u frames)"
if (frame_count >= 2)
LOG_ERR(_log_fmt, msg, frame_count);
else if (frame_count >= 1)
LOG_WARN(_log_fmt, msg, frame_count);
else
LOG_INFO(_log_fmt, msg, frame_count);
#undef _log_fmt
wp_presentation_feedback_destroy(wp_presentation_feedback);
free(ctx);
}
static void
discarded(void *data, struct wp_presentation_feedback *wp_presentation_feedback)
{
struct presentation_context *ctx = data;
wp_presentation_feedback_destroy(wp_presentation_feedback);
free(ctx);
}
static const struct wp_presentation_feedback_listener presentation_feedback_listener = {
.sync_output = &sync_output,
.presented = &presented,
.discarded = &discarded,
};
static struct fcft_font *
attrs_to_font(const struct terminal *term, const struct attributes *attrs)
{
int idx = attrs->italic << 1 | attrs->bold;
return term->fonts[idx];
}
static inline pixman_color_t
color_hex_to_pixman_with_alpha(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,
};
}
static inline pixman_color_t
color_hex_to_pixman(uint32_t color)
{
/* Count on the compiler optimizing this */
return color_hex_to_pixman_with_alpha(color, 0xffff);
}
static inline uint32_t
color_dim(uint32_t color)
{
int hue, sat, lum;
rgb_to_hsl(color, &hue, &sat, &lum);
return hsl_to_rgb(hue, sat, lum / 1.5);
}
static inline uint32_t
color_brighten(const struct terminal *term, uint32_t color)
{
/*
* First try to match the color against the base 8 colors. If we
* find a match, return the corresponding bright color.
*/
if (term->conf->bold_in_bright.palette_based) {
for (size_t i = 0; i < 8; i++) {
if (term->colors.table[i] == color)
return term->colors.table[i + 8];
}
}
int hue, sat, lum;
rgb_to_hsl(color, &hue, &sat, &lum);
return hsl_to_rgb(hue, sat, min(100, lum * 1.3));
}
static inline void
color_dim_for_search(pixman_color_t *color)
{
color->red /= 2;
color->green /= 2;
color->blue /= 2;
}
static inline int
font_baseline(const struct terminal *term)
{
return term->font_y_ofs + term->fonts[0]->ascent;
}
static void
draw_unfocused_block(const struct terminal *term, pixman_image_t *pix,
const pixman_color_t *color, int x, int y, int cell_cols)
{
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, pix, color, 4,
(pixman_rectangle16_t []){
{x, y, cell_cols * term->cell_width, 1}, /* top */
{x, y, 1, term->cell_height}, /* left */
{x + cell_cols * term->cell_width - 1, y, 1, term->cell_height}, /* right */
{x, y + term->cell_height - 1, cell_cols * term->cell_width, 1}, /* bottom */
});
}
static void
draw_beam_cursor(const struct terminal *term, pixman_image_t *pix,
const struct fcft_font *font,
const pixman_color_t *color, int x, int y)
{
int baseline = y + font_baseline(term) - term->fonts[0]->ascent;
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, pix, color,
1, &(pixman_rectangle16_t){
x, baseline,
term_pt_or_px_as_pixels(term, &term->conf->cursor.beam_thickness),
term->fonts[0]->ascent + term->fonts[0]->descent});
}
static void
draw_underline_with_thickness(
const struct terminal *term, pixman_image_t *pix,
const struct fcft_font *font,
const pixman_color_t *color, int x, int y, int cols, int thickness)
{
/* Make sure the line isn't positioned below the cell */
int y_ofs = font_baseline(term) -
(term->conf->use_custom_underline_offset
? -term_pt_or_px_as_pixels(term, &term->conf->underline_offset)
: font->underline.position);
y_ofs = min(y_ofs, term->cell_height - thickness);
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, pix, color,
1, &(pixman_rectangle16_t){
x, y + y_ofs, cols * term->cell_width, thickness});
}
static void
draw_underline_cursor(const struct terminal *term, pixman_image_t *pix,
const struct fcft_font *font,
const pixman_color_t *color, int x, int y, int cols)
{
int thickness = term->conf->cursor.underline_thickness.px >= 0
? term_pt_or_px_as_pixels(
term, &term->conf->cursor.underline_thickness)
: font->underline.thickness;
draw_underline_with_thickness(
term, pix, font, color, x, y + font->underline.thickness, cols,
thickness);
}
static void
draw_underline(const struct terminal *term, pixman_image_t *pix,
const struct fcft_font *font,
const pixman_color_t *color, int x, int y, int cols)
{
draw_underline_with_thickness(
term, pix, font, color, x, y, cols, font->underline.thickness);
}
static void
draw_strikeout(const struct terminal *term, pixman_image_t *pix,
const struct fcft_font *font,
const pixman_color_t *color, int x, int y, int cols)
{
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, pix, color,
1, &(pixman_rectangle16_t){
x, y + font_baseline(term) - font->strikeout.position,
cols * term->cell_width, font->strikeout.thickness});
}
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)
{
bool is_selected = cell->attrs.selected;
if (term->cursor_color.cursor >> 31) {
*cursor_color = color_hex_to_pixman(term->cursor_color.cursor);
*text_color = color_hex_to_pixman(
term->cursor_color.text >> 31
? term->cursor_color.text : term->colors.bg);
if (cell->attrs.reverse ^ is_selected) {
pixman_color_t swap = *cursor_color;
*cursor_color = *text_color;
*text_color = swap;
}
if (term->is_searching && !is_selected) {
color_dim_for_search(cursor_color);
color_dim_for_search(text_color);
}
} else {
*cursor_color = *fg;
*text_color = *bg;
}
}
static void
draw_cursor(const struct terminal *term, const struct cell *cell,
const struct fcft_font *font, pixman_image_t *pix, pixman_color_t *fg,
const pixman_color_t *bg, int x, int y, int cols)
{
pixman_color_t cursor_color;
pixman_color_t text_color;
cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color);
switch (term->cursor_style) {
case CURSOR_BLOCK:
if (unlikely(!term->kbd_focus))
draw_unfocused_block(term, pix, &cursor_color, x, y, cols);
else if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) {
*fg = text_color;
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, pix, &cursor_color, 1,
&(pixman_rectangle16_t){x, y, cols * term->cell_width, term->cell_height});
}
break;
case CURSOR_BEAM:
if (likely(term->cursor_blink.state == CURSOR_BLINK_ON ||
!term->kbd_focus))
{
draw_beam_cursor(term, pix, font, &cursor_color, x, y);
}
break;
case CURSOR_UNDERLINE:
if (likely(term->cursor_blink.state == CURSOR_BLINK_ON ||
!term->kbd_focus))
{
draw_underline_cursor(term, pix, font, &cursor_color, x, y, cols);
}
break;
}
}
static inline void
render_cell_prepass(struct terminal *term, struct row *row, int col)
{
for (; col < term->cols - 1; col++) {
if (row->cells[col].attrs.confined ||
(row->cells[col].attrs.clean == row->cells[col + 1].attrs.clean)) {
break;
}
row->cells[col].attrs.clean = 0;
row->cells[col + 1].attrs.clean = 0;
}
}
static int
render_cell(struct terminal *term, pixman_image_t *pix,
struct row *row, int col, int row_no, bool has_cursor)
{
struct cell *cell = &row->cells[col];
if (cell->attrs.clean)
return 0;
cell->attrs.clean = 1;
cell->attrs.confined = true;
int width = term->cell_width;
int height = term->cell_height;
const int x = term->margins.left + col * width;
const int y = term->margins.top + row_no * height;
xassert(cell->attrs.selected == 0 || cell->attrs.selected == 1);
bool is_selected = cell->attrs.selected;
uint32_t _fg = 0;
uint32_t _bg = 0;
uint16_t alpha = 0xffff;
if (is_selected && term->colors.use_custom_selection) {
_fg = term->colors.selection_fg;
_bg = term->colors.selection_bg;
} else {
/* Use cell specific color, if set, otherwise the default colors (possible reversed) */
_fg = cell->attrs.have_fg ? cell->attrs.fg : term->reverse ? term->colors.bg : term->colors.fg;
_bg = cell->attrs.have_bg ? cell->attrs.bg : term->reverse ? term->colors.fg : term->colors.bg;
if (cell->attrs.reverse ^ is_selected) {
uint32_t swap = _fg;
_fg = _bg;
_bg = swap;
} else if (!cell->attrs.have_bg)
alpha = term->colors.alpha;
}
if (unlikely(is_selected && _fg == _bg)) {
/* Invert bg when selected/highlighted text has same fg/bg */
_bg = ~_bg;
alpha = 0xffff;
}
if (cell->attrs.dim)
_fg = color_dim(_fg);
if (term->conf->bold_in_bright.enabled && cell->attrs.bold)
_fg = color_brighten(term, _fg);
if (cell->attrs.blink && term->blink.state == BLINK_OFF)
_fg = color_dim(_fg);
pixman_color_t fg = color_hex_to_pixman(_fg);
pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha);
if (term->is_searching && !is_selected) {
color_dim_for_search(&fg);
color_dim_for_search(&bg);
}
struct fcft_font *font = attrs_to_font(term, &cell->attrs);
const struct composed *composed = NULL;
const struct fcft_grapheme *grapheme = NULL;
const struct fcft_glyph *single = NULL;
const struct fcft_glyph **glyphs = NULL;
unsigned glyph_count = 0;
wchar_t base = cell->wc;
int cell_cols = 1;
if (base != 0) {
if (unlikely(
/* Classic box drawings */
(base >= 0x2500 && base <= 0x259f) ||
/*
* Unicode 13 "Symbols for Legacy Computing"
* sub-ranges below.
*
* Note, the full range is U+1FB00 - U+1FBF9
*/
/* Unicode 13 sextants */
(base >= 0x1fb00 && base <= 0x1fb3b) ||
/* Unicode 13 partial blocks */
/* TODO: there's more here! */
(base >= 0x1fb70 && base <= 0x1fb8b)) &&
likely(!term->conf->box_drawings_uses_font_glyphs))
{
/* Box drawing characters */
size_t idx = base >= 0x1fb00
? (base >= 0x1fb70
? base - 0x1fb70 + 220
: base - 0x1fb00 + 160)
: base - 0x2500;
xassert(idx < ALEN(term->box_drawing));
if (likely(term->box_drawing[idx] != NULL))
single = term->box_drawing[idx];
else {
mtx_lock(&term->render.workers.lock);
/* Parallel thread may have instantiated it while we took the lock */
if (term->box_drawing[idx] == NULL)
term->box_drawing[idx] = box_drawing(term, base);
mtx_unlock(&term->render.workers.lock);
single = term->box_drawing[idx];
xassert(single != NULL);
}
glyph_count = 1;
glyphs = &single;
cell_cols = single->cols;
}
else if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI)
{
composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO);
base = composed->chars[0];
if (term->conf->can_shape_grapheme && term->conf->tweak.grapheme_shaping) {
grapheme = fcft_grapheme_rasterize(
font, composed->count, composed->chars,
0, NULL, term->font_subpixel);
}
if (grapheme != NULL) {
cell_cols = composed->width;
composed = NULL;
glyphs = grapheme->glyphs;
glyph_count = grapheme->count;
}
}
if (single == NULL && grapheme == NULL) {
xassert(base != 0);
single = fcft_glyph_rasterize(font, base, term->font_subpixel);
if (single == NULL) {
glyph_count = 0;
cell_cols = 1;
} else {
glyph_count = 1;
glyphs = &single;
cell_cols = single->cols;
}
}
}
assert(glyph_count == 0 || glyphs != NULL);
const int cols_left = term->cols - col;
cell_cols = max(1, min(cell_cols, cols_left));
/*
* Determine cells that will bleed into their right neighbor and remember
* them for cleanup in the next frame.
*/
int render_width = cell_cols * width;
if (term->conf->tweak.overflowing_glyphs &&
glyph_count > 0)
{
int glyph_width = 0, advance = 0;
for (size_t i = 0; i < glyph_count; i++) {
glyph_width = max(glyph_width,
advance + glyphs[i]->x + glyphs[i]->width);
advance += glyphs[i]->advance.x;
}
if (glyph_width > render_width) {
render_width = min(glyph_width, render_width + width);
for (int i = 0; i < cell_cols; i++)
row->cells[col + i].attrs.confined = false;
}
}
pixman_region32_t clip;
pixman_region32_init_rect(
&clip, x, y,
render_width, term->cell_height);
pixman_image_set_clip_region32(pix, &clip);
/* Background */
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, pix, &bg, 1,
&(pixman_rectangle16_t){x, y, cell_cols * width, height});
if (cell->attrs.blink && term->blink.fd < 0) {
/* TODO: use a custom lock for this? */
mtx_lock(&term->render.workers.lock);
term_arm_blink_timer(term);
mtx_unlock(&term->render.workers.lock);
}
if (has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus)
draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols);
if (cell->wc == 0 || cell->wc >= CELL_SPACER || cell->wc == L'\t' ||
(unlikely(cell->attrs.conceal) && !is_selected))
{
goto draw_cursor;
}
pixman_image_t *clr_pix = pixman_image_create_solid_fill(&fg);
int pen_x = x;
for (unsigned i = 0; i < glyph_count; i++) {
const int letter_x_ofs = i == 0 ? term->font_x_ofs : 0;
const struct fcft_glyph *glyph = glyphs[i];
if (glyph == NULL)
continue;
int g_x = glyph->x;
int g_y = glyph->y;
if (i > 0 && glyph->x >= 0)
g_x -= term->cell_width;
if (unlikely(pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8)) {
/* Glyph surface is a pre-rendered image (typically a color emoji...) */
if (!(cell->attrs.blink && term->blink.state == BLINK_OFF)) {
pixman_image_composite32(
PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, 0,
pen_x + letter_x_ofs + g_x, y + font_baseline(term) - g_y,
glyph->width, glyph->height);
}
} else {
pixman_image_composite32(
PIXMAN_OP_OVER, clr_pix, glyph->pix, pix, 0, 0, 0, 0,
pen_x + letter_x_ofs + g_x, y + font_baseline(term) - g_y,
glyph->width, glyph->height);
/* Combining characters */
if (composed != NULL) {
assert(glyph_count == 1);
for (size_t i = 1; i < composed->count; i++) {
const struct fcft_glyph *g = fcft_glyph_rasterize(
font, composed->chars[i], term->font_subpixel);
if (g == NULL)
continue;
/*
* Fonts _should_ assume the pen position is now
* *after* the base glyph, and thus use negative
* offsets for combining glyphs.
*
* Not all fonts behave like this however, and we
* try to accommodate both variants.
*
* Since we haven't moved our pen position yet, we
* add a full cell width to the offset (or two, in
* case of double-width characters).
*
* If the font does *not* use negative offsets,
* we'd normally use an offset of 0. However, to
* somewhat deal with double-width glyphs we use
* an offset of *one* cell.
*/
int x_ofs = g->x < 0
? cell_cols * term->cell_width
: (cell_cols - 1) * term->cell_width;
pixman_image_composite32(
PIXMAN_OP_OVER, clr_pix, g->pix, pix, 0, 0, 0, 0,
/* Some fonts use a negative offset, while others use a
* "normal" offset */
pen_x + x_ofs + g->x,
y + font_baseline(term) - g->y,
g->width, g->height);
}
}
}
pen_x += glyph->advance.x;
}
pixman_image_unref(clr_pix);
/* Underline */
if (cell->attrs.underline)
draw_underline(term, pix, font, &fg, x, y, cell_cols);
if (cell->attrs.strikethrough)
draw_strikeout(term, pix, font, &fg, x, y, cell_cols);
if (unlikely(cell->attrs.url)) {
pixman_color_t url_color = color_hex_to_pixman(
term->conf->colors.use_custom.url
? term->conf->colors.url
: term->colors.table[3]
);
draw_underline(term, pix, font, &url_color, x, y, cell_cols);
}
draw_cursor:
if (has_cursor && (term->cursor_style != CURSOR_BLOCK || !term->kbd_focus))
draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols);
pixman_image_set_clip_region32(pix, NULL);
return cell_cols;
}
static void
render_row(struct terminal *term, pixman_image_t *pix, struct row *row,
int row_no, int cursor_col)
{
if (term->conf->tweak.overflowing_glyphs)
for (int col = term->cols - 1; col >= 0; col--)
render_cell_prepass(term, row, col);
for (int col = term->cols - 1; col >= 0; col--)
render_cell(term, pix, row, col, row_no, cursor_col == col);
}
static void
render_urgency(struct terminal *term, struct buffer *buf)
{
uint32_t red = term->colors.table[1];
if (term->is_searching)
red = color_dim(red);
pixman_color_t bg = color_hex_to_pixman(red);
int width = min(min(term->margins.left, term->margins.right),
min(term->margins.top, term->margins.bottom));
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, buf->pix[0], &bg, 4,
(pixman_rectangle16_t[]){
/* Top */
{0, 0, term->width, width},
/* Bottom */
{0, term->height - width, term->width, width},
/* Left */
{0, width, width, term->height - 2 * width},
/* Right */
{term->width - width, width, width, term->height - 2 * width},
});
}
static void
render_margin(struct terminal *term, struct buffer *buf,
int start_line, int end_line, bool apply_damage)
{
/* Fill area outside the cell grid with the default background color */
const int rmargin = term->width - term->margins.right;
const int bmargin = term->height - term->margins.bottom;
const int line_count = end_line - start_line;
uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg;
pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, term->colors.alpha);
if (term->is_searching)
color_dim_for_search(&bg);
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, buf->pix[0], &bg, 4,
(pixman_rectangle16_t[]){
/* Top */
{0, 0, term->width, term->margins.top},
/* Bottom */
{0, bmargin, term->width, term->margins.bottom},
/* Left */
{0, term->margins.top + start_line * term->cell_height,
term->margins.left, line_count * term->cell_height},
/* Right */
{rmargin, term->margins.top + start_line * term->cell_height,
term->margins.right, line_count * term->cell_height},
});
if (term->render.urgency)
render_urgency(term, buf);
/* Ensure the updated regions are copied to the next frame's
* buffer when we're double buffering */
pixman_region32_union_rect(
&buf->dirty, &buf->dirty, 0, 0, term->width, term->margins.top);
pixman_region32_union_rect(
&buf->dirty, &buf->dirty, 0, bmargin, term->width, term->margins.bottom);
pixman_region32_union_rect(
&buf->dirty, &buf->dirty, 0, 0, term->margins.left, term->height);
pixman_region32_union_rect(
&buf->dirty, &buf->dirty,
rmargin, 0, term->margins.right, term->height);
if (apply_damage) {
/* Top */
wl_surface_damage_buffer(
term->window->surface, 0, 0, term->width, term->margins.top);
/* Bottom */
wl_surface_damage_buffer(
term->window->surface, 0, bmargin, term->width, term->margins.bottom);
/* Left */
wl_surface_damage_buffer(
term->window->surface,
0, term->margins.top + start_line * term->cell_height,
term->margins.left, line_count * term->cell_height);
/* Right */
wl_surface_damage_buffer(
term->window->surface,
rmargin, term->margins.top + start_line * term->cell_height,
term->margins.right, line_count * term->cell_height);
}
}
static void
grid_render_scroll(struct terminal *term, struct buffer *buf,
const struct damage *dmg)
{
int height = (dmg->region.end - dmg->region.start - dmg->lines) * term->cell_height;
LOG_DBG(
"damage: SCROLL: %d-%d by %d lines",
dmg->region.start, dmg->region.end, dmg->lines);
if (height <= 0)
return;
#if TIME_SCROLL_DAMAGE
struct timeval start_time;
gettimeofday(&start_time, NULL);
#endif
int dst_y = term->margins.top + (dmg->region.start + 0) * term->cell_height;
int src_y = term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height;
/*
* SHM scrolling can be *much* faster, but it depends on how many
* lines we're scrolling, and how much repairing we need to do.
*
* In short, scrolling a *large* number of rows is faster with a
* memmove, while scrolling a *small* number of lines is faster
* with SHM scrolling.
*
* However, since we need to restore the scrolling regions when
* SHM scrolling, we also need to take this into account.
*
* Finally, we also have to restore the window margins, and this
* is a *huge* performance hit when scrolling a large number of
* lines (in addition to the sloweness of SHM scrolling as
* method).
*
* So, we need to figure out when to SHM scroll, and when to
* memmove.
*
* For now, assume that the both methods perform roughly the same,
* given an equal number of bytes to move/allocate, and use the
* method that results in the least amount of bytes to touch.
*
* Since number of lines directly translates to bytes, we can
* simply count lines.
*
* SHM scrolling needs to first "move" (punch hole + allocate)
* dmg->lines number of lines, and then we need to restore
* the bottom scroll region.
*
* If the total number of lines is less than half the screen - use
* SHM. Otherwise use memmove.
*/
bool try_shm_scroll =
shm_can_scroll(buf) && (
dmg->lines +
dmg->region.start +
(term->rows - dmg->region.end)) < term->rows / 2;
bool did_shm_scroll = false;
//try_shm_scroll = false;
//try_shm_scroll = true;
if (try_shm_scroll) {
did_shm_scroll = shm_scroll(
buf, dmg->lines * term->cell_height,
term->margins.top, dmg->region.start * term->cell_height,
term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height);
}
if (did_shm_scroll) {
/* Restore margins */
render_margin(
term, buf, dmg->region.end - dmg->lines, term->rows, false);
} else {
/* Fallback for when we either cannot do SHM scrolling, or it failed */
uint8_t *raw = buf->data;
memmove(raw + dst_y * buf->stride,
raw + src_y * buf->stride,
height * buf->stride);
}
#if TIME_SCROLL_DAMAGE
struct timeval end_time;
gettimeofday(&end_time, NULL);
struct timeval memmove_time;
timersub(&end_time, &start_time, &memmove_time);
LOG_INFO("scrolled %dKB (%d lines) using %s in %lds %ldus",
height * buf->stride / 1024, dmg->lines,
did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove",
memmove_time.tv_sec, memmove_time.tv_usec);
#endif
wl_surface_damage_buffer(
term->window->surface, term->margins.left, dst_y,
term->width - term->margins.left - term->margins.right, height);
}
static void
grid_render_scroll_reverse(struct terminal *term, struct buffer *buf,
const struct damage *dmg)
{
int height = (dmg->region.end - dmg->region.start - dmg->lines) * term->cell_height;
LOG_DBG(
"damage: SCROLL REVERSE: %d-%d by %d lines",
dmg->region.start, dmg->region.end, dmg->lines);
if (height <= 0)
return;
#if TIME_SCROLL_DAMAGE
struct timeval start_time;
gettimeofday(&start_time, NULL);
#endif
int src_y = term->margins.top + (dmg->region.start + 0) * term->cell_height;
int dst_y = term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height;
bool try_shm_scroll =
shm_can_scroll(buf) && (
dmg->lines +
dmg->region.start +
(term->rows - dmg->region.end)) < term->rows / 2;
bool did_shm_scroll = false;
if (try_shm_scroll) {
did_shm_scroll = shm_scroll(
buf, -dmg->lines * term->cell_height,
term->margins.top, dmg->region.start * term->cell_height,
term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height);
}
if (did_shm_scroll) {
/* Restore margins */
render_margin(
term, buf, dmg->region.start, dmg->region.start + dmg->lines, false);
} else {
/* Fallback for when we either cannot do SHM scrolling, or it failed */
uint8_t *raw = buf->data;
memmove(raw + dst_y * buf->stride,
raw + src_y * buf->stride,
height * buf->stride);
}
#if TIME_SCROLL_DAMAGE
struct timeval end_time;
gettimeofday(&end_time, NULL);
struct timeval memmove_time;
timersub(&end_time, &start_time, &memmove_time);
LOG_INFO("scrolled REVERSE %dKB (%d lines) using %s in %lds %ldus",
height * buf->stride / 1024, dmg->lines,
did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove",
memmove_time.tv_sec, memmove_time.tv_usec);
#endif
wl_surface_damage_buffer(
term->window->surface, term->margins.left, dst_y,
term->width - term->margins.left - term->margins.right, height);
}
static void
render_sixel_chunk(struct terminal *term, pixman_image_t *pix, const struct sixel *sixel,
int term_start_row, int img_start_row, int count)
{
/* Translate row/column to x/y pixel values */
const int x = term->margins.left + sixel->pos.col * term->cell_width;
const int y = term->margins.top + term_start_row * term->cell_height;
/* Width/height, in pixels - and don't touch the window margins */
const int width = max(
0,
min(sixel->width,
term->width - x - term->margins.right));
const int height = max(
0,
min(
min(count * term->cell_height, /* 'count' number of rows */
sixel->height - img_start_row * term->cell_height), /* What remains of the sixel */
term->height - y - term->margins.bottom));
/* Verify we're not stepping outside the grid */
xassert(x >= term->margins.left);
xassert(y >= term->margins.top);
xassert(width == 0 || x + width <= term->width - term->margins.right);
xassert(height == 0 || y + height <= term->height - term->margins.bottom);
//LOG_DBG("sixel chunk: %dx%d %dx%d", x, y, width, height);
pixman_image_composite32(
sixel->opaque ? PIXMAN_OP_SRC : PIXMAN_OP_OVER,
sixel->pix,
NULL,
pix,
0, img_start_row * term->cell_height,
0, 0,
x, y,
width, height);
wl_surface_damage_buffer(term->window->surface, x, y, width, height);
}
static void
render_sixel(struct terminal *term, pixman_image_t *pix,
const struct coord *cursor, const struct sixel *sixel)
{
const int view_end = (term->grid->view + term->rows - 1) & (term->grid->num_rows - 1);
const bool last_row_needs_erase = sixel->height % term->cell_height != 0;
const bool last_col_needs_erase = sixel->width % term->cell_width != 0;
int chunk_img_start = -1; /* Image-relative start row of chunk */
int chunk_term_start = -1; /* Viewport relative start row of chunk */
int chunk_row_count = 0; /* Number of rows to emit */
#define maybe_emit_sixel_chunk_then_reset() \
if (chunk_row_count != 0) { \
render_sixel_chunk( \
term, pix, sixel, \
chunk_term_start, chunk_img_start, chunk_row_count); \
chunk_term_start = chunk_img_start = -1; \
chunk_row_count = 0; \
}
/*
* Iterate all sixel rows:
*
* - ignore rows that aren't visible on-screen
* - ignore rows that aren't dirty (they have already been rendered)
* - chunk consecutive dirty rows into a 'chunk'
* - emit (render) chunk as soon as a row isn't visible, or is clean
* - emit final chunk after we've iterated all rows
*
* The purpose of this is to reduce the amount of pixels that
* needs to be composited and marked as damaged for the
* compositor.
*
* Since we do CPU based composition, rendering is a slow and
* heavy task for foot, and thus it is important to not re-render
* things unnecessarily.
*/
for (int _abs_row_no = sixel->pos.row;
_abs_row_no < sixel->pos.row + sixel->rows;
_abs_row_no++)
{
const int abs_row_no = _abs_row_no & (term->grid->num_rows - 1);
const int term_row_no =
(abs_row_no - term->grid->view + term->grid->num_rows) &
(term->grid->num_rows - 1);
/* Check if row is in the visible viewport */
if (view_end >= term->grid->view) {
/* Not wrapped */
if (!(abs_row_no >= term->grid->view && abs_row_no <= view_end)) {
/* Not visible */
maybe_emit_sixel_chunk_then_reset();
continue;
}
} else {
/* Wrapped */
if (!(abs_row_no >= term->grid->view || abs_row_no <= view_end)) {
/* Not visible */
maybe_emit_sixel_chunk_then_reset();
continue;
}
}
/* Is the row dirty? */
struct row *row = term->grid->rows[abs_row_no];
xassert(row != NULL); /* Should be visible */
if (!row->dirty) {
maybe_emit_sixel_chunk_then_reset();
continue;
}
int cursor_col = cursor->row == term_row_no ? cursor->col : -1;
/*
* If image contains transparent parts, render all (dirty)
* cells beneath it.
*
* If image is opaque, loop cells and set their 'clean' bit,
* to prevent the grid rendered from overwriting the sixel
*
* If the last sixel row only partially covers the cell row,
* 'erase' the cell by rendering them.
*
* In all cases, do *not* clear the ‘dirty’ bit on the row, to
* ensure the regular renderer includes them in the damage
* rect.
*/
if (!sixel->opaque) {
/* TODO: multithreading */
int cursor_col = cursor->row == term_row_no ? cursor->col : -1;
render_row(term, pix, row, term_row_no, cursor_col);
} else {
for (int col = sixel->pos.col;
col < min(sixel->pos.col + sixel->cols, term->cols);
col++)
{
struct cell *cell = &row->cells[col];
if (!cell->attrs.clean) {
bool last_row = abs_row_no == sixel->pos.row + sixel->rows - 1;
bool last_col = col == sixel->pos.col + sixel->cols - 1;
if ((last_row_needs_erase && last_row) ||
(last_col_needs_erase && last_col))
{
render_cell(term, pix, row, col, term_row_no, cursor_col == col);
} else {
cell->attrs.clean = 1;
cell->attrs.confined = 1;
}
}
}
}
if (chunk_term_start == -1) {
xassert(chunk_img_start == -1);
chunk_term_start = term_row_no;
chunk_img_start = _abs_row_no - sixel->pos.row;
chunk_row_count = 1;
} else
chunk_row_count++;
}
maybe_emit_sixel_chunk_then_reset();
#undef maybe_emit_sixel_chunk_then_reset
}
static void
render_sixel_images(struct terminal *term, pixman_image_t *pix,
const struct coord *cursor)
{
if (likely(tll_length(term->grid->sixel_images)) == 0)
return;
const int scrollback_end
= (term->grid->offset + term->rows) & (term->grid->num_rows - 1);
const int view_start
= (term->grid->view
- scrollback_end
+ term->grid->num_rows) & (term->grid->num_rows - 1);
const int view_end = view_start + term->rows - 1;
//LOG_DBG("SIXELS: %zu images, view=%d-%d",
// tll_length(term->grid->sixel_images), view_start, view_end);
tll_foreach(term->grid->sixel_images, it) {
const struct sixel *six = &it->item;
const int start
= (six->pos.row
- scrollback_end
+ term->grid->num_rows) & (term->grid->num_rows - 1);
const int end = start + six->rows - 1;
//LOG_DBG(" sixel: %d-%d", start, end);
if (start > view_end) {
/* Sixel starts after view ends, no need to try to render it */
continue;
} else if (end < view_start) {
/* Image ends before view starts. Since the image list is
* sorted, we can safely stop here */
break;
}
render_sixel(term, pix, cursor, &it->item);
}
}
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
static void
render_ime_preedit_for_seat(struct terminal *term, struct seat *seat,
struct buffer *buf)
{
if (likely(seat->ime.preedit.cells == NULL))
return;
if (unlikely(term->is_searching))
return;
/* Adjust cursor position to viewport */
struct coord cursor;
cursor = term->grid->cursor.point;
cursor.row += term->grid->offset;
cursor.row -= term->grid->view;
cursor.row &= term->grid->num_rows - 1;
if (cursor.row < 0 || cursor.row >= term->rows)
return;
int cells_needed = seat->ime.preedit.count;
if (seat->ime.preedit.cursor.start == cells_needed &&
seat->ime.preedit.cursor.end == cells_needed)
{
/* Cursor will be drawn *after* the pre-edit string, i.e. in
* the cell *after*. This means we need to copy, and dirty,
* one extra cell from the original grid, or we’ll leave
* trailing “cursors” after us if the user deletes text while
* pre-editing */
cells_needed++;
}
int row_idx = cursor.row;
int col_idx = cursor.col;
int ime_ofs = 0; /* Offset into pre-edit string to start rendering at */
int cells_left = term->cols - cursor.col;
int cells_used = min(cells_needed, term->cols);
/* Adjust start of pre-edit text to the left if string doesn't fit on row */
if (cells_left < cells_used)
col_idx -= cells_used - cells_left;
if (cells_needed > cells_used) {
int start = seat->ime.preedit.cursor.start;
int end = seat->ime.preedit.cursor.end;
if (start == end) {
/* Ensure *end* of pre-edit string is visible */
ime_ofs = cells_needed - cells_used;
} else {
/* Ensure the *beginning* of the cursor-area is visible */
ime_ofs = start;
/* Display as much as possible of the pre-edit string */
if (cells_needed - ime_ofs < cells_used)
ime_ofs = cells_needed - cells_used;
}
/* Make sure we don't start in the middle of a character */
while (ime_ofs < cells_needed &&
seat->ime.preedit.cells[ime_ofs].wc >= CELL_SPACER)
{
ime_ofs++;
}
}
xassert(col_idx >= 0);
xassert(col_idx < term->cols);
struct row *row = grid_row_in_view(term->grid, row_idx);
/* Don't start pre-edit text in the middle of a double-width character */
while (col_idx > 0 && row->cells[col_idx].wc >= CELL_SPACER) {
cells_used++;
col_idx--;
}
/*
* Copy original content (render_cell() reads cell data directly
* from grid), and mark all cells as dirty. This ensures they are
* re-rendered when the pre-edit text is modified or removed.
*/
struct cell *real_cells = malloc(cells_used * sizeof(real_cells[0]));
for (int i = 0; i < cells_used; i++) {
xassert(col_idx + i < term->cols);
real_cells[i] = row->cells[col_idx + i];
real_cells[i].attrs.clean = 0;
}
row->dirty = true;
/* Render pre-edit text */
xassert(seat->ime.preedit.cells[ime_ofs].wc < CELL_SPACER);
for (int i = 0, idx = ime_ofs; idx < seat->ime.preedit.count; i++, idx++) {
const struct cell *cell = &seat->ime.preedit.cells[idx];
if (cell->wc >= CELL_SPACER)
continue;
int width = max(1, wcwidth(cell->wc));
if (col_idx + i + width > term->cols)
break;
row->cells[col_idx + i] = *cell;
render_cell(term, buf->pix[0], row, col_idx + i, row_idx, false);
}
int start = seat->ime.preedit.cursor.start - ime_ofs;
int end = seat->ime.preedit.cursor.end - ime_ofs;
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 cursor_color, text_color;
cursor_colors_for_cell(
term, start_cell, &fg, &bg, &cursor_color, &text_color);
int x = term->margins.left + (col_idx + start) * term->cell_width;
int y = term->margins.top + row_idx * term->cell_height;
if (end == start) {
/* Bar */
if (start >= 0) {
struct fcft_font *font = attrs_to_font(term, &start_cell->attrs);
draw_beam_cursor(term, buf->pix[0], font, &cursor_color, x, y);
}
term_ime_set_cursor_rect(term, x, y, 1, term->cell_height);
}
else if (end > start) {
/* Hollow cursor */
if (start >= 0 && end <= term->cols) {
int cols = end - start;
draw_unfocused_block(term, buf->pix[0], &cursor_color, x, y, cols);
}
term_ime_set_cursor_rect(
term, x, y, (end - start) * term->cell_width, term->cell_height);
}
}
/* Restore original content (but do not render) */
for (int i = 0; i < cells_used; i++)
row->cells[col_idx + i] = real_cells[i];
free(real_cells);
wl_surface_damage_buffer(
term->window->surface,
term->margins.left,
term->margins.top + row_idx * term->cell_height,
term->width - term->margins.left - term->margins.right,
1 * term->cell_height);
}
#endif
static void
render_ime_preedit(struct terminal *term, struct buffer *buf)
{
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
tll_foreach(term->wl->seats, it) {
if (it->item.kbd_focus == term)
render_ime_preedit_for_seat(term, &it->item, buf);
}
#endif
}
int
render_worker_thread(void *_ctx)
{
struct render_worker_context *ctx = _ctx;
struct terminal *term = ctx->term;
const int my_id = ctx->my_id;
free(ctx);
sigset_t mask;
sigfillset(&mask);
pthread_sigmask(SIG_SETMASK, &mask, NULL);
char proc_title[16];
snprintf(proc_title, sizeof(proc_title), "foot:render:%d", my_id);
if (pthread_setname_np(pthread_self(), proc_title) < 0)
LOG_ERRNO("render worker %d: failed to set process title", my_id);
sem_t *start = &term->render.workers.start;
sem_t *done = &term->render.workers.done;
mtx_t *lock = &term->render.workers.lock;
while (true) {
sem_wait(start);
struct buffer *buf = term->render.workers.buf;
bool frame_done = false;
/* Translate offset-relative cursor row to view-relative */
struct coord cursor = {-1, -1};
if (!term->hide_cursor) {
cursor = term->grid->cursor.point;
cursor.row += term->grid->offset;
cursor.row -= term->grid->view;
cursor.row &= term->grid->num_rows - 1;
}
while (!frame_done) {
mtx_lock(lock);
xassert(tll_length(term->render.workers.queue) > 0);
int row_no = tll_pop_front(term->render.workers.queue);
mtx_unlock(lock);
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;
render_row(term, buf->pix[my_id], row, row_no, cursor_col);
break;
}
case -1:
frame_done = true;
sem_post(done);
break;
case -2:
return 0;
}
}
};
return -1;
}
struct csd_data {
int x;
int y;
int width;
int height;
};
static struct csd_data
get_csd_data(const struct terminal *term, enum csd_surface surf_idx)
{
xassert(term->window->csd_mode == CSD_YES);
/* Only title bar is rendered in maximized mode */
const int border_width = !term->window->is_maximized
? term->conf->csd.border_width * term->scale : 0;
const int title_height = term->window->is_fullscreen
? 0
: term->conf->csd.title_height * term->scale;
const int button_width = !term->window->is_fullscreen
? term->conf->csd.button_width * term->scale : 0;
const int button_close_width = term->width >= 1 * button_width
? button_width : 0;
const int button_maximize_width = term->width >= 2 * button_width
? button_width : 0;
const int button_minimize_width = term->width >= 3 * button_width
? button_width : 0;
switch (surf_idx) {
case CSD_SURF_TITLE: return (struct csd_data){ 0, -title_height, term->width, title_height};
case CSD_SURF_LEFT: return (struct csd_data){-border_width, -title_height, border_width, title_height + term->height};
case CSD_SURF_RIGHT: return (struct csd_data){ term->width, -title_height, border_width, title_height + term->height};
case CSD_SURF_TOP: return (struct csd_data){-border_width, -title_height - border_width, term->width + 2 * border_width, border_width};
case CSD_SURF_BOTTOM: return (struct csd_data){-border_width, term->height, term->width + 2 * border_width, border_width};
/* Positioned relative to CSD_SURF_TITLE */
case CSD_SURF_MINIMIZE: return (struct csd_data){term->width - 3 * button_width, 0, button_minimize_width, title_height};
case CSD_SURF_MAXIMIZE: return (struct csd_data){term->width - 2 * button_width, 0, button_maximize_width, title_height};
case CSD_SURF_CLOSE: return (struct csd_data){term->width - 1 * button_width, 0, button_close_width, title_height};
case CSD_SURF_COUNT:
break;
}
BUG("Invalid csd_surface type");
return (struct csd_data){0};
}
static void
csd_commit(struct terminal *term, struct wl_surface *surf, struct buffer *buf)
{
xassert(buf->width % term->scale == 0);
xassert(buf->height % term->scale == 0);
wl_surface_attach(surf, buf->wl_buf, 0, 0);
wl_surface_damage_buffer(surf, 0, 0, buf->width, buf->height);
wl_surface_set_buffer_scale(surf, term->scale);
wl_surface_commit(surf);
}
static void
render_csd_part(struct terminal *term,
struct wl_surface *surf, struct buffer *buf,
int width, int height, pixman_color_t *color)
{
xassert(term->window->csd_mode == CSD_YES);
pixman_image_t *src = pixman_image_create_solid_fill(color);
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, buf->pix[0], color, 1,
&(pixman_rectangle16_t){0, 0, buf->width, buf->height});
pixman_image_unref(src);
}
static void
render_csd_title(struct terminal *term, const struct csd_data *info,
struct buffer *buf)
{
xassert(term->window->csd_mode == CSD_YES);
struct wl_surface *surf = term->window->csd.surface[CSD_SURF_TITLE].surf;
xassert(info->width > 0 && info->height > 0);
xassert(info->width % term->scale == 0);
xassert(info->height % term->scale == 0);
uint32_t _color = term->conf->colors.fg;
uint16_t alpha = 0xffff;
if (term->conf->csd.color.title_set) {
_color = term->conf->csd.color.title;
alpha = _color >> 24 | (_color >> 24 << 8);
}
if (!term->visual_focus)
_color = color_dim(_color);
pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha);
render_csd_part(term, surf, buf, info->width, info->height, &color);
csd_commit(term, surf, buf);
}
static void
render_csd_border(struct terminal *term, enum csd_surface surf_idx,
const struct csd_data *info, struct buffer *buf)
{
xassert(term->window->csd_mode == CSD_YES);
xassert(surf_idx >= CSD_SURF_LEFT && surf_idx <= CSD_SURF_BOTTOM);
struct wl_surface *surf = term->window->csd.surface[surf_idx].surf;
if (info->width == 0 || info->height == 0)
return;
xassert(info->width % term->scale == 0);
xassert(info->height % term->scale == 0);
pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0);
render_csd_part(term, surf, buf, info->width, info->height, &color);
csd_commit(term, surf, buf);
}
static pixman_color_t
get_csd_button_fg_color(const struct config *conf)
{
uint32_t _color = conf->colors.bg;
uint16_t alpha = 0xffff;
if (conf->csd.color.buttons_set) {
_color = conf->csd.color.buttons;
alpha = _color >> 24 | (_color >> 24 << 8);
}
return color_hex_to_pixman_with_alpha(_color, alpha);
}
static void
render_csd_button_minimize(struct terminal *term, struct buffer *buf)
{
pixman_color_t color = get_csd_button_fg_color(term->conf);
pixman_image_t *src = pixman_image_create_solid_fill(&color);
const int max_height = buf->height / 2;
const int max_width = buf->width / 2;
int width = max_width;
int height = max_width / 2;
if (height > max_height) {
height = max_height;
width = height * 2;
}
xassert(width <= max_width);
xassert(height <= max_height);
int x_margin = (buf->width - width) / 2.;
int y_margin = (buf->height - height) / 2.;
pixman_triangle_t tri = {
.p1 = {
.x = pixman_int_to_fixed(x_margin),
.y = pixman_int_to_fixed(y_margin),
},
.p2 = {
.x = pixman_int_to_fixed(x_margin + width),
.y = pixman_int_to_fixed(y_margin),
},
.p3 = {
.x = pixman_int_to_fixed(buf->width / 2),
.y = pixman_int_to_fixed(y_margin + height),
},
};
pixman_composite_triangles(
PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1,
0, 0, 0, 0, 1, &tri);
pixman_image_unref(src);
}
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_image_t *src = pixman_image_create_solid_fill(&color);
const int max_height = buf->height / 3;
const int max_width = buf->width / 3;
int width = min(max_height, max_width);
int thick = 1 * term->scale;
const int x_margin = (buf->width - width) / 2;
const int y_margin = (buf->height - width) / 2;
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, buf->pix[0], &color, 4,
(pixman_rectangle16_t[]){
{x_margin, y_margin, width, thick},
{x_margin, y_margin + thick, thick, width - 2 * thick},
{x_margin + width - thick, y_margin + thick, thick, width - 2 * thick},
{x_margin, y_margin + width - thick, width, thick}});
pixman_image_unref(src);
}
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_image_t *src = pixman_image_create_solid_fill(&color);
const int max_height = buf->height / 2;
const int max_width = buf->width / 2;
int width = max_width;
int height = max_width / 2;
if (height > max_height) {
height = max_height;
width = height * 2;
}
xassert(width <= max_width);
xassert(height <= max_height);
int x_margin = (buf->width - width) / 2.;
int y_margin = (buf->height - height) / 2.;
pixman_triangle_t tri = {
.p1 = {
.x = pixman_int_to_fixed(buf->width / 2),
.y = pixman_int_to_fixed(y_margin),
},
.p2 = {
.x = pixman_int_to_fixed(x_margin),
.y = pixman_int_to_fixed(y_margin + height),
},
.p3 = {
.x = pixman_int_to_fixed(x_margin + width),
.y = pixman_int_to_fixed(y_margin + height),
},
};
pixman_composite_triangles(
PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1,
0, 0, 0, 0, 1, &tri);
pixman_image_unref(src);
}
static void
render_csd_button_maximize(struct terminal *term, struct buffer *buf)
{
if (term->window->is_maximized)
render_csd_button_maximize_maximized(term, buf);
else
render_csd_button_maximize_window(term, 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_image_t *src = pixman_image_create_solid_fill(&color);
const int max_height = buf->height / 3;
const int max_width = buf->width / 3;
int width = min(max_height, max_width);
const int x_margin = (buf->width - width) / 2;
const int y_margin = (buf->height - width) / 2;
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, buf->pix[0], &color, 1,
&(pixman_rectangle16_t){x_margin, y_margin, width, width});
pixman_image_unref(src);
}
static void
render_csd_button(struct terminal *term, enum csd_surface surf_idx,
const struct csd_data *info, struct buffer *buf)
{
xassert(term->window->csd_mode == CSD_YES);
xassert(surf_idx >= CSD_SURF_MINIMIZE && surf_idx <= CSD_SURF_CLOSE);
struct wl_surface *surf = term->window->csd.surface[surf_idx].surf;
if (info->width == 0 || info->height == 0)
return;
xassert(info->width % term->scale == 0);
xassert(info->height % term->scale == 0);
uint32_t _color;
uint16_t alpha = 0xffff;
bool is_active = false;
bool is_set = false;
const uint32_t *conf_color = NULL;
switch (surf_idx) {
case CSD_SURF_MINIMIZE:
_color = term->conf->colors.table[4]; /* blue */
is_set = term->conf->csd.color.minimize_set;
conf_color = &term->conf->csd.color.minimize;
is_active = term->active_surface == TERM_SURF_BUTTON_MINIMIZE;
break;
case CSD_SURF_MAXIMIZE:
_color = term->conf->colors.table[2]; /* green */
is_set = term->conf->csd.color.maximize_set;
conf_color = &term->conf->csd.color.maximize;
is_active = term->active_surface == TERM_SURF_BUTTON_MAXIMIZE;
break;
case CSD_SURF_CLOSE:
_color = term->conf->colors.table[1]; /* red */
is_set = term->conf->csd.color.close_set;
conf_color = &term->conf->csd.color.close;
is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE;
break;
default:
BUG("unhandled surface type: %u", (unsigned)surf_idx);
break;
}
if (is_active) {
if (is_set) {
_color = *conf_color;
alpha = _color >> 24 | (_color >> 24 << 8);
}
} else {
_color = 0;
alpha = 0;
}
if (!term->visual_focus)
_color = color_dim(_color);
pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha);
render_csd_part(term, 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);
break;
}
csd_commit(term, surf, buf);
}
static void
render_csd(struct terminal *term)
{
xassert(term->window->csd_mode == CSD_YES);
if (term->window->is_fullscreen)
return;
struct csd_data infos[CSD_SURF_COUNT];
int widths[CSD_SURF_COUNT];
int heights[CSD_SURF_COUNT];
for (size_t i = 0; i < CSD_SURF_COUNT; i++) {
infos[i] = get_csd_data(term, i);
const int x = infos[i].x;
const int y = infos[i].y;
const int width = infos[i].width;
const int height = infos[i].height;
struct wl_surface *surf = term->window->csd.surface[i].surf;
struct wl_subsurface *sub = term->window->csd.surface[i].sub;
xassert(surf != NULL);
xassert(sub != NULL);
if (width == 0 || height == 0) {
widths[i] = heights[i] = 0;
wl_subsurface_set_position(sub, 0, 0);
wl_surface_attach(surf, NULL, 0, 0);
wl_surface_commit(surf);
continue;
}
widths[i] = width;
heights[i] = height;
wl_subsurface_set_position(sub, x / term->scale, y / term->scale);
}
struct buffer *bufs[CSD_SURF_COUNT];
shm_get_many(term->render.chains.csd, CSD_SURF_COUNT, widths, heights, bufs);
for (size_t i = CSD_SURF_LEFT; i <= CSD_SURF_BOTTOM; i++)
render_csd_border(term, i, &infos[i], bufs[i]);
for (size_t i = CSD_SURF_MINIMIZE; i <= CSD_SURF_CLOSE; i++)
render_csd_button(term, i, &infos[i], bufs[i]);
render_csd_title(term, &infos[CSD_SURF_TITLE], bufs[CSD_SURF_TITLE]);
}
static void
render_osd(struct terminal *term,
struct wl_surface *surf, struct wl_subsurface *sub_surf,
struct buffer *buf,
const wchar_t *text, uint32_t _fg, uint32_t _bg,
unsigned x, unsigned y)
{
pixman_color_t bg = color_hex_to_pixman(_bg);
pixman_image_fill_rectangles(
PIXMAN_OP_SRC, buf->pix[0], &bg, 1,
&(pixman_rectangle16_t){0, 0, buf->width, buf->height});
struct fcft_font *font = term->fonts[0];
pixman_color_t fg = color_hex_to_pixman(_fg);
const int x_ofs = term->font_x_ofs;
for (size_t i = 0; i < wcslen(text); i++) {
const struct fcft_glyph *glyph = fcft_glyph_rasterize(
font, text[i], term->font_subpixel);
if (glyph == NULL)
continue;
pixman_image_t *src = pixman_image_create_solid_fill(&fg);
pixman_image_composite32(
PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0,
x + x_ofs + glyph->x, y + font_baseline(term) - glyph->y,
glyph->width, glyph->height);
pixman_image_unref(src);
x += term->cell_width;
}
xassert(buf->width % term->scale == 0);
xassert(buf->height % term->scale == 0);
quirk_weston_subsurface_desync_on(sub_surf);
wl_surface_attach(surf, buf->wl_buf, 0, 0);
wl_surface_damage_buffer(surf, 0, 0, buf->width, buf->height);
wl_surface_set_buffer_scale(surf, term->scale);
struct wl_region *region = wl_compositor_create_region(term->wl->compositor);
if (region != NULL) {
wl_region_add(region, 0, 0, buf->width, buf->height);
wl_surface_set_opaque_region(surf, region);
wl_region_destroy(region);
}
wl_surface_commit(surf);
quirk_weston_subsurface_desync_off(sub_surf);
}
static void
render_scrollback_position(struct terminal *term)
{
if (term->conf->scrollback.indicator.position == SCROLLBACK_INDICATOR_POSITION_NONE)
return;
struct wl_window *win = term->window;
if (term->grid->view == term->grid->offset) {
if (win->scrollback_indicator.surf != NULL)
wayl_win_subsurface_destroy(&win->scrollback_indicator);
return;
}
if (win->scrollback_indicator.surf == NULL) {
if (!wayl_win_subsurface_new(win, &win->scrollback_indicator)) {
LOG_ERR("failed to create scrollback indicator surface");
return;
}
}
xassert(win->scrollback_indicator.surf != NULL);
xassert(win->scrollback_indicator.sub != NULL);
/* Find absolute row number of the scrollback start */
int scrollback_start = term->grid->offset + term->rows;
int empty_rows = 0;
while (term->grid->rows[scrollback_start & (term->grid->num_rows - 1)] == NULL) {
scrollback_start++;
empty_rows++;
}
/* Rebase viewport against scrollback start (so that 0 is at
* the beginning of the scrollback) */
int rebased_view = term->grid->view - scrollback_start + term->grid->num_rows;
rebased_view &= term->grid->num_rows - 1;
/* How much of the scrollback is actually used? */
int populated_rows = term->grid->num_rows - empty_rows;
xassert(populated_rows > 0);
xassert(populated_rows <= term->grid->num_rows);
/*
* How far down in the scrollback we are.
*
* 0% -> at the beginning of the scrollback
* 100% -> at the bottom, i.e. where new lines are inserted
*/
double percent =
rebased_view + term->rows == populated_rows
? 1.0
: (double)rebased_view / (populated_rows - term->rows);
wchar_t _text[64];
const wchar_t *text = _text;
int cell_count = 0;
/* *What* to render */
switch (term->conf->scrollback.indicator.format) {
case SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE:
swprintf(_text, sizeof(_text) / sizeof(_text[0]), L"%u%%", (int)(100 * percent));
cell_count = 3;
break;
case SCROLLBACK_INDICATOR_FORMAT_LINENO:
swprintf(_text, sizeof(_text) / sizeof(_text[0]), L"%d", rebased_view + 1);
cell_count = 1 + (int)log10(term->grid->num_rows);
break;
case SCROLLBACK_INDICATOR_FORMAT_TEXT:
text = term->conf->scrollback.indicator.text;
cell_count = wcslen(text);
break;
}
const int scale = term->scale;
const int margin = 3 * scale;
const int width =
(2 * margin + cell_count * term->cell_width + scale - 1) / scale * scale;
const int height =
(2 * margin + term->cell_height + scale - 1) / scale * scale;
/* *Where* to render - parent relative coordinates */
int surf_top = 0;
switch (term->conf->scrollback.indicator.position) {
case SCROLLBACK_INDICATOR_POSITION_NONE:
BUG("Invalid scrollback indicator position type");
return;
case SCROLLBACK_INDICATOR_POSITION_FIXED:
surf_top = term->cell_height - margin;
break;
case SCROLLBACK_INDICATOR_POSITION_RELATIVE: {
int lines = term->rows - 2; /* Avoid using first and last rows */
if (term->is_searching) {
/* Make sure we don't collide with the scrollback search box */
lines--;
}
lines = max(lines, 0);
int pixels = max(lines * term->cell_height - height + 2 * margin, 0);
surf_top = term->cell_height - margin + (int)(percent * pixels);
break;
}
}
const int x = (term->width - margin - width) / scale * scale;
const int y = (term->margins.top + surf_top) / scale * scale;
if (y + height > term->height) {
wl_surface_attach(win->scrollback_indicator.surf, NULL, 0, 0);
wl_surface_commit(win->scrollback_indicator.surf);
return;
}
struct buffer_chain *chain = term->render.chains.scrollback_indicator;
struct buffer *buf = shm_get_buffer(chain, width, height);
wl_subsurface_set_position(
win->scrollback_indicator.sub, x / scale, y / scale);
render_osd(
term,
win->scrollback_indicator.surf,
win->scrollback_indicator.sub,
buf, text,
term->colors.table[0], term->colors.table[8 + 4],
width - margin - wcslen(text) * term->cell_width, margin);
}
static void
render_render_timer(struct terminal *term, struct timeval render_time)
{
struct wl_window *win = term->window;
wchar_t text[256];
double usecs = render_time.tv_sec * 1000000 + render_time.tv_usec;
swprintf(text, sizeof(text) / sizeof(text[0]), L"%.2f µs", usecs);
const int scale = term->scale;
const int cell_count = wcslen(text);
const int margin = 3 * scale;
const int width =
(2 * margin + cell_count * term->cell_width + scale - 1) / scale * scale;
const int height =
(2 * margin + term->cell_height + scale - 1) / scale * scale;
struct buffer_chain *chain = term->render.chains.render_timer;
struct buffer *buf = shm_get_buffer(chain, width, height);
wl_subsurface_set_position(
win->render_timer.sub,
margin / term->scale,
(term->margins.top + term->cell_height - margin) / term->scale);
render_osd(
term,
win->render_timer.surf,
win->render_timer.sub,
buf, text,
term->colors.table[0], term->colors.table[8 + 1],
margin, margin);
}
static void frame_callback(
void *data, struct wl_callback *wl_callback, uint32_t callback_data);
static const struct wl_callback_listener frame_listener = {
.done = &frame_callback,
};
static void
force_full_repaint(struct terminal *term, struct buffer *buf)
{
tll_free(term->grid->scroll_damage);
render_margin(term, buf, 0, term->rows, true);
term_damage_view(term);
}
static void
reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old)
{
static int counter = 0;
static bool have_warned = false;
if (!have_warned && ++counter > 5) {
LOG_WARN("compositor is not releasing buffers immediately; "
"expect lower rendering performance");
have_warned = true;
}
if (new->age > 1) {
memcpy(new->data, old->data, new->height * new->stride);
return;
}
/*
* TODO: remove this frame’s damage from the region we copy from
* the old frame.
*
* - this frame’s dirty region is only valid *after* we’ve applied
* its scroll damage.
* - last frame’s dirty region is only valid *before* we’ve
* applied this frame’s scroll damage.
*
* Can we transform one of the regions? It’s not trivial, since
* scroll damage isn’t just about counting lines; there may be
* multiple damage records, each with different scrolling regions.
*/
pixman_region32_t dirty;
pixman_region32_init(&dirty);
bool full_repaint_needed = true;
for (int r = 0; r < term->rows; r++) {
const struct row *row = grid_row_in_view(term->grid, r);
bool row_all_dirty = true;
for (int c = 0; c < term->cols; c++) {
if (row->cells[c].attrs.clean) {
row_all_dirty = false;
full_repaint_needed = false;
break;
}
}
if (row_all_dirty) {
pixman_region32_union_rect(
&dirty, &dirty,
term->margins.left,
term->margins.top + r * term->cell_height,
term->width - term->margins.left - term->margins.right,
term->cell_height);
}
}
if (full_repaint_needed) {
force_full_repaint(term, new);
return;
}
for (size_t i = 0; i < old->scroll_damage_count; i++) {
const struct damage *dmg = &old->scroll_damage[i];
switch (dmg->type) {
case DAMAGE_SCROLL:
if (term->grid->view == term->grid->offset)
grid_render_scroll(term, new, dmg);
break;
case DAMAGE_SCROLL_REVERSE:
if (term->grid->view == term->grid->offset)
grid_render_scroll_reverse(term, new, dmg);
break;
case DAMAGE_SCROLL_IN_VIEW:
grid_render_scroll(term, new, dmg);
break;
case DAMAGE_SCROLL_REVERSE_IN_VIEW:
grid_render_scroll_reverse(term, new, dmg);
break;
}
}
if (tll_length(term->grid->scroll_damage) == 0) {
pixman_region32_subtract(&dirty, &old->dirty, &dirty);
pixman_image_set_clip_region32(new->pix[0], &dirty);
} else
pixman_image_set_clip_region32(new->pix[0], &old->dirty);
pixman_image_composite32(
PIXMAN_OP_SRC, old->pix[0], NULL, new->pix[0],
0, 0, 0, 0, 0, 0, term->width, term->height);
pixman_image_set_clip_region32(new->pix[0], NULL);
pixman_region32_fini(&dirty);
}
static void
dirty_old_cursor(struct terminal *term)
{
if (term->render.last_cursor.row != NULL && !term->render.last_cursor.hidden) {
struct row *row = term->render.last_cursor.row;
struct cell *cell = &row->cells[term->render.last_cursor.col];
cell->attrs.clean = 0;
row->dirty = true;
}
/* Remember current cursor position, for the next frame */
term->render.last_cursor.row = grid_row(term->grid, term->grid->cursor.point.row);
term->render.last_cursor.col = term->grid->cursor.point.col;
term->render.last_cursor.hidden = term->hide_cursor;
}
static void
dirty_cursor(struct terminal *term)
{
if (term->hide_cursor)
return;
const struct coord *cursor = &term->grid->cursor.point;
struct row *row = grid_row(term->grid, cursor->row);
struct cell *cell = &row->cells[cursor->col];
cell->attrs.clean = 0;
row->dirty = true;
}
static void
grid_render(struct terminal *term)
{
if (term->is_shutting_down)
return;
struct timeval start_time, start_double_buffering = {0}, stop_double_buffering = {0};
if (term->conf->tweak.render_timer_osd || term->conf->tweak.render_timer_log)
gettimeofday(&start_time, NULL);
xassert(term->width > 0);
xassert(term->height > 0);
struct buffer_chain *chain = term->render.chains.grid;
struct buffer *buf = shm_get_buffer(chain, term->width, term->height);
/* Dirty old and current cursor cell, to ensure they’re repainted */
dirty_old_cursor(term);
dirty_cursor(term);
if (term->render.last_buf == NULL ||
term->render.last_buf->width != buf->width ||
term->render.last_buf->height != buf->height ||
term->flash.active || term->render.was_flashing ||
term->is_searching != term->render.was_searching ||
term->render.margins)
{
force_full_repaint(term, buf);
}
else if (buf->age > 0) {
LOG_DBG("buffer age: %u (%p)", buf->age, (void *)buf);
xassert(term->render.last_buf != NULL);
xassert(term->render.last_buf != buf);
xassert(term->render.last_buf->width == buf->width);
xassert(term->render.last_buf->height == buf->height);
gettimeofday(&start_double_buffering, NULL);
reapply_old_damage(term, buf, term->render.last_buf);
gettimeofday(&stop_double_buffering, NULL);
}
if (term->render.last_buf != NULL) {
shm_unref(term->render.last_buf);
term->render.last_buf = NULL;
}
term->render.last_buf = buf;
term->render.was_flashing = term->flash.active;
term->render.was_searching = term->is_searching;
shm_addref(buf);
buf->age = 0;
free(term->render.last_buf->scroll_damage);
buf->scroll_damage_count = tll_length(term->grid->scroll_damage);
buf->scroll_damage = xmalloc(
buf->scroll_damage_count * sizeof(buf->scroll_damage[0]));
{
size_t i = 0;
tll_foreach(term->grid->scroll_damage, it) {
buf->scroll_damage[i++] = it->item;
switch (it->item.type) {
case DAMAGE_SCROLL:
if (term->grid->view == term->grid->offset)
grid_render_scroll(term, buf, &it->item);
break;
case DAMAGE_SCROLL_REVERSE:
if (term->grid->view == term->grid->offset)
grid_render_scroll_reverse(term, buf, &it->item);
break;
case DAMAGE_SCROLL_IN_VIEW:
grid_render_scroll(term, buf, &it->item);
break;
case DAMAGE_SCROLL_REVERSE_IN_VIEW:
grid_render_scroll_reverse(term, buf, &it->item);
break;
}
tll_remove(term->grid->scroll_damage, it);
}
}
/*
* Ensure selected cells have their 'selected' bit set. This is
* normally "automatically" true - the bit is set when the
* selection is made.
*
* However, if the cell is updated (printed to) while the
* selection is active, the 'selected' bit is cleared. Checking
* for this and re-setting the bit in term_print() is too
* expensive performance wise.
*
* Instead, we synchronize the selection bits here and now. This
* makes the performance impact linear to the number of selected
* cells rather than to the number of updated cells.
*
* (note that selection_dirty_cells() will not set the dirty flag
* on cells where the 'selected' bit is already set)
*/
selection_dirty_cells(term);
/* Translate offset-relative row to view-relative, unless cursor
* is hidden, then we just set it to -1 */
struct coord cursor = {-1, -1};
if (!term->hide_cursor) {
cursor = term->grid->cursor.point;
cursor.row += term->grid->offset;
cursor.row -= term->grid->view;
cursor.row &= term->grid->num_rows - 1;
}
render_sixel_images(term, buf->pix[0], &cursor);
if (term->render.workers.count > 0) {
mtx_lock(&term->render.workers.lock);
term->render.workers.buf = buf