From 35946f9386b2b3bf6125d9941536ce691d6111bb Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 28 Nov 2024 21:48:54 +0530 Subject: [PATCH] Improve performance of processing wide chars Store multi cell data in the CPUCell rather than in TextCache. This sends the CPUCell size back to 12 but in benchmarks ASCII performance is untouched and Unicode performace goes back to what it was before multicell --- kitty/fonts.c | 48 ++++++------- kitty/line-buf.c | 18 ++--- kitty/line.c | 54 ++++++++------- kitty/line.h | 50 ++++---------- kitty/screen.c | 143 ++++++++++++++------------------------- kitty/shaders.c | 3 +- kitty/text-cache.c | 3 +- kitty/text-cache.h | 1 - kitty_tests/multicell.py | 2 +- 9 files changed, 127 insertions(+), 195 deletions(-) diff --git a/kitty/fonts.c b/kitty/fonts.c index f03be38f6..3817c38a3 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -481,9 +481,9 @@ face_has_codepoint(const void* face, char_type cp) { } static bool -has_emoji_presentation(const ListOfChars *lc) { - if (!lc->is_multicell || !lc->is_topleft || !lc->count) return false; - return is_emoji(lc->chars[0]) && (lc->count == 1 || lc->chars[1] != VS15); +has_emoji_presentation(const CPUCell *c, const ListOfChars *lc) { + if (!c->is_multicell || c->x || c->y || !lc->count) return false; + return is_emoji(lc->chars[0]) && (lc->count == 1 || lc->chars[1] != VS15); } bool @@ -581,10 +581,10 @@ chars_as_utf8(const ListOfChars *lc, char *buf, char_type zero_char) { } static ssize_t -fallback_font(FontGroup *fg, const GPUCell *gpu_cell, const ListOfChars *lc) { +fallback_font(FontGroup *fg, const CPUCell *cpu_cell, const GPUCell *gpu_cell, const ListOfChars *lc) { bool bold = gpu_cell->attrs.bold; bool italic = gpu_cell->attrs.italic; - bool emoji_presentation = has_emoji_presentation(lc); + bool emoji_presentation = has_emoji_presentation(cpu_cell, lc); char style = emoji_presentation ? 'a' : 'A'; if (bold) style += italic ? 3 : 2; else style += italic ? 1 : 0; char cell_text[4 * 32] = {style}; @@ -640,7 +640,7 @@ START_ALLOW_CASE_RANGE case 0xf5d0 ... 0xf60d: // branch drawing characters return BOX_FONT; default: - *is_emoji_presentation = has_emoji_presentation(lc); + *is_emoji_presentation = has_emoji_presentation(cpu_cell, lc); ans = in_symbol_maps(fg, lc->chars[0]); if (ans > -1) return ans; switch(gpu_cell->attrs.bold | (gpu_cell->attrs.italic << 1)) { @@ -655,7 +655,7 @@ START_ALLOW_CASE_RANGE } if (ans < 0) ans = fg->medium_font_idx; if (!*is_emoji_presentation && has_cell_text((bool(*)(const void*, char_type))face_has_codepoint, (fg->fonts + ans)->face, false, lc)) { *is_main_font = true; return ans; } - return fallback_font(fg, gpu_cell, lc); + return fallback_font(fg, cpu_cell, gpu_cell, lc); } END_ALLOW_CASE_RANGE } @@ -776,7 +776,7 @@ static void render_box_cell(FontGroup *fg, RunFont rf, CPUCell *cpu_cell, GPUCell *gpu_cell, const TextCache *tc) { int error = 0; // We need to render multicell chars for multicell_y > 0 cell_first_char() returns 0 for such cells - char_type ch = cpu_cell->is_multicell ? tc_first_char_at_index(tc, cpu_cell->ch_or_idx) : cell_first_char(cpu_cell, tc); + char_type ch = cpu_cell->ch_is_idx ? tc_first_char_at_index(tc, cpu_cell->ch_or_idx) : cpu_cell->ch_or_idx; glyph_index glyph = box_glyph_id(ch); ensure_glyph_render_scratch_space(rf.scale); bool all_rendered = true; @@ -824,7 +824,7 @@ load_hb_buffer(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_ while (num_cells) { for (num = 0; num_cells; first_cpu_cell++, first_gpu_cell++, num_cells--) { text_in_cell(first_cpu_cell, tc, &lc); - if (lc.is_multicell && !lc.is_topleft) continue; + if (first_cpu_cell->is_multicell && (first_cpu_cell->x + first_cpu_cell->y)) continue; if (lc.count + num > arraysz(shape_buffer)) break; memcpy(shape_buffer + num, lc.chars, lc.count * sizeof(shape_buffer[0])); num += lc.count; @@ -866,7 +866,7 @@ render_group(FontGroup *fg, unsigned int num_cells, unsigned int num_glyphs, CPU ensure_canvas_can_fit(fg, num_cells + 1, rf.scale); text_in_cell(cpu_cells, tc, global_glyph_render_scratch.lc); - bool was_colored = has_emoji_presentation(global_glyph_render_scratch.lc); + bool was_colored = has_emoji_presentation(cpu_cells, global_glyph_render_scratch.lc); render_glyphs_in_cells(font->face, font->bold, font->italic, info, positions, num_glyphs, fg->canvas.buf, fg->cell_width, fg->cell_height, num_cells, fg->baseline, &was_colored, (FONTS_DATA_HANDLE)fg, center_glyph); if (PyErr_Occurred()) PyErr_Print(); @@ -982,8 +982,7 @@ check_cell_consumed(CellData *cell_data, CPUCell *last_cpu_cell, const TextCache if (cell_data->codepoints_consumed >= cell_data->num_codepoints) { uint16_t width = 1; if (cell_data->cpu_cell->is_multicell) { - MultiCellData mcd = cell_multicell_data(cell_data->cpu_cell, tc); - width = mcd.width * mcd.scale; + width = cell_data->cpu_cell->width * cell_data->cpu_cell->scale; } cell_data->cpu_cell += width; cell_data->gpu_cell += width; @@ -1304,20 +1303,20 @@ collapse_pua_space_ligature(index_type num_cells) { } static bool -is_group_calt_ligature(const Group *group, const TextCache *tc) { +is_group_calt_ligature(const Group *group) { if (group->num_cells < 2 || !group->has_special_glyph) return false; const CPUCell *first_cell = G(first_cpu_cell) + group->first_cell_idx; - return !first_cell->is_multicell || cell_multicell_data(first_cell, tc).width == 1; + return !first_cell->is_multicell || first_cell->width == 1; } static void -split_run_at_offset(index_type cursor_offset, index_type *left, index_type *right, const TextCache *tc) { +split_run_at_offset(index_type cursor_offset, index_type *left, index_type *right) { *left = 0; *right = 0; for (unsigned idx = 0; idx < G(group_idx) + 1; idx++) { Group *group = G(groups) + idx; if (group->first_cell_idx <= cursor_offset && cursor_offset < group->first_cell_idx + group->num_cells) { - if (is_group_calt_ligature(group, tc)) { + if (is_group_calt_ligature(group)) { // likely a calt ligature *left = group->first_cell_idx; *right = group->first_cell_idx + group->num_cells; } @@ -1354,8 +1353,7 @@ test_shape(PyObject UNUSED *self, PyObject *args) { while(num < line->xnum && cell_has_text(line->cpu_cells + num)) { index_type width = 1; if ((c = line->cpu_cells + num)->is_multicell) { - MultiCellData mcd = cell_multicell_data(c, line->text_cache); - width = mcd.width * mcd.scale; + width = c->width * c->scale; } num += width; } @@ -1400,7 +1398,7 @@ render_run(FontGroup *fg, CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, inde if (pua_space_ligature) collapse_pua_space_ligature(num_cells); else if (cursor_offset > -1) { // false if DISABLE_LIGATURES_NEVER index_type left, right; - split_run_at_offset(cursor_offset, &left, &right, tc); + split_run_at_offset(cursor_offset, &left, &right); if (right > left) { if (left) { shape_run(first_cpu_cell, first_gpu_cell, left, &fg->fonts[rf.font_idx], false, tc); @@ -1473,24 +1471,22 @@ render_line(FONTS_DATA_HANDLE fg_, Line *line, index_type lnum, Cursor *cursor, bool center_glyph = false; bool disable_ligature_at_cursor = cursor != NULL && disable_ligature_strategy == DISABLE_LIGATURES_CURSOR && lnum == cursor->y; index_type first_cell_in_run, i; - MultiCellData mcd; for (i=0, first_cell_in_run=0; i < line->xnum; i++) { cell_font = basic_font; CPUCell *cpu_cell = line->cpu_cells + i; if (cpu_cell->is_multicell) { - mcd = cell_multicell_data(cpu_cell, line->text_cache); if (cpu_cell->x) { - i += mcd_x_limit(mcd) - cpu_cell->x - 1; + i += mcd_x_limit(cpu_cell) - cpu_cell->x - 1; continue; } - cell_font.scale = mcd.scale; cell_font.subscale = mcd.subscale; cell_font.vertical_align = mcd.vertical_align; + cell_font.scale = cpu_cell->scale; cell_font.subscale = cpu_cell->subscale; cell_font.vertical_align = cpu_cell->vertical_align; cell_font.multicell_y = cpu_cell->y; } text_in_cell(cpu_cell, line->text_cache, lc); bool is_main_font, is_emoji_presentation; GPUCell *gpu_cell = line->gpu_cells + i; - cell_font.font_idx = font_for_cell(fg, cpu_cell, gpu_cell, &is_main_font, &is_emoji_presentation, line->text_cache, lc); const char_type first_ch = lc->chars[0]; + cell_font.font_idx = font_for_cell(fg, cpu_cell, gpu_cell, &is_main_font, &is_emoji_presentation, line->text_cache, lc); if ( cell_font.font_idx != MISSING_FONT && ((!is_main_font && !is_emoji_presentation && is_symbol(first_ch)) || (cell_font.font_idx != BOX_FONT && (is_private_use(first_ch))) || is_non_emoji_dingbat(first_ch)) @@ -1831,14 +1827,14 @@ get_fallback_font(PyObject UNUSED *self, PyObject *args) { PyObject *text; int bold, italic; if (!PyArg_ParseTuple(args, "Upp", &text, &bold, &italic)) return NULL; - GPUCell gpu_cell = {0}; + GPUCell gpu_cell = {0}; CPUCell cpu_cell = {0}; RAII_ListOfChars(lc); lc.count = PyUnicode_GET_LENGTH(text); ensure_space_for_chars(&lc, lc.count); if (!PyUnicode_AsUCS4(text, lc.chars, lc.capacity, 1)) return NULL; if (bold) gpu_cell.attrs.bold = true; if (italic) gpu_cell.attrs.italic = true; FontGroup *fg = font_groups; - ssize_t ans = fallback_font(fg, &gpu_cell, &lc); + ssize_t ans = fallback_font(fg, &cpu_cell, &gpu_cell, &lc); if (ans == MISSING_FONT) { PyErr_SetString(PyExc_ValueError, "No fallback font found"); return NULL; } if (ans < 0) { PyErr_SetString(PyExc_ValueError, "Too many fallback fonts"); return NULL; } return fg->fonts[ans].face; diff --git a/kitty/line-buf.c b/kitty/line-buf.c index c64cf7cc3..a6a2a25a0 100644 --- a/kitty/line-buf.c +++ b/kitty/line-buf.c @@ -599,17 +599,17 @@ restitch(Line *dest, index_type at, LineBuf *src, const index_type src_y, TrackC bool found_text = false; while (at < dest->xnum && x < src->xnum) { if (!found_text && cell_has_text(&cp[x])) found_text = true; - if (gp[x].attrs.width == 2) { - if (dest->xnum - at > 1) { - dest->cpu_cells[at] = cp[x]; - dest->gpu_cells[at++] = gp[x++]; - dest->cpu_cells[at] = cp[x]; - dest->gpu_cells[at] = gp[x]; - at++; x++; + if (cp[x].is_multicell) { + // TODO: handle multiline chars + if (dest->xnum - at > cp[x].width) { + for (index_type i = 0; i < cp[x].width; i++) { + dest->cpu_cells[at] = cp[x]; + dest->gpu_cells[at++] = gp[x++]; + } } else { dest->cpu_cells[at] = (CPUCell){0}; dest->gpu_cells[at] = (GPUCell){0}; - at++; + at = dest->xnum; } } else { dest->cpu_cells[at] = cp[x]; @@ -634,7 +634,7 @@ restitch(Line *dest, index_type at, LineBuf *src, const index_type src_y, TrackC } y++; } - dest->gpu_cells[dest->xnum - 1].attrs.next_char_was_wrapped = last_char_has_wrapped_flag; + dest->cpu_cells[dest->xnum - 1].next_char_was_wrapped = last_char_has_wrapped_flag; // remove the copied lines and cells from src if (num_of_lines_removed) { for (index_type i = 0; i < num_of_lines_removed; i++) { diff --git a/kitty/line.c b/kitty/line.c index 476aebfcc..3cab80e18 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -54,21 +54,21 @@ nonnegative_integer_as_utf32(unsigned num, ANSIBuf *output) { } static unsigned -write_multicell_ansi_prefix(MultiCellData mcd, ANSIBuf *output) { +write_multicell_ansi_prefix(const CPUCell *mcd, ANSIBuf *output) { unsigned pos = output->len; ensure_space_for(output, buf, output->buf[0], output->len + 128, capacity, 2048, false); #define w(x) output->buf[output->len++] = x w(0x1b); w(']'); for (unsigned i = 0; i < sizeof(xstr(TEXT_SIZE_CODE)) - 1; i++) w(xstr(TEXT_SIZE_CODE)[i]); w(';'); - if (mcd.width > 1) { - w('w'); w('='); nonnegative_integer_as_utf32(mcd.width, output); w(':'); + if (mcd->width > 1) { + w('w'); w('='); nonnegative_integer_as_utf32(mcd->width, output); w(':'); } - if (mcd.scale > 1) { - w('s'); w('='); nonnegative_integer_as_utf32(mcd.scale, output); w(':'); + if (mcd->scale > 1) { + w('s'); w('='); nonnegative_integer_as_utf32(mcd->scale, output); w(':'); } - if (mcd.subscale) { - w('S'); w('='); nonnegative_integer_as_utf32(mcd.subscale, output); w(':'); + if (mcd->subscale) { + w('S'); w('='); nonnegative_integer_as_utf32(mcd->subscale, output); w(':'); } if (output->buf[output->len - 1] == ':') output->len--; w(';'); @@ -78,18 +78,25 @@ write_multicell_ansi_prefix(MultiCellData mcd, ANSIBuf *output) { static unsigned text_in_cell_ansi(const CPUCell *c, TextCache *tc, ANSIBuf *output) { - if (c->ch_is_idx) { - if (!c->is_multicell) return tc_chars_at_index_ansi(tc, c->ch_or_idx, output); + unsigned n = 0; + if (c->is_multicell) { if (c->x || c->y) return 0; - MultiCellData mcd = cell_multicell_data(c, tc); - unsigned n = write_multicell_ansi_prefix(mcd, output); - n += tc_chars_at_index_ansi(tc, c->ch_or_idx, output) - 1; - output->buf[output->len - 1] = '\a'; - return n; + n = write_multicell_ansi_prefix(c, output); } - ensure_space_for(output, buf, output->buf[0], output->len + 1, capacity, 2048, false); - output->buf[output->len++] = c->ch_or_idx; - return 1; + if (c->ch_is_idx) { + n += tc_chars_at_index_ansi(tc, c->ch_or_idx, output); + } else { + ensure_space_for(output, buf, output->buf[0], output->len + 2, capacity, 2048, false); + if (c->ch_or_idx) { + output->buf[output->len++] = c->ch_or_idx; + n += 1; + } + } + if (c->is_multicell) { + output->buf[output->len++] = '\a'; + n++; + } + return n; } @@ -326,7 +333,7 @@ unicode_in_range(const Line *self, const index_type start, const index_type limi global_unicode_in_range_buf.capacity = ns; global_unicode_in_range_buf.chars = np; lc.chars = global_unicode_in_range_buf.chars + n; lc.capacity = global_unicode_in_range_buf.capacity - n; } - if (lc.is_multicell && !lc.is_topleft) continue; + if (self->cpu_cells[i].is_multicell && (self->cpu_cells[i].x || self->cpu_cells[i].y)) continue; if (!lc.chars[0]) { if (skip_zero_cells) continue; lc.chars[0] = ' '; @@ -449,9 +456,7 @@ line_as_ansi(Line *self, ANSIBuf *output, const GPUCell** prev_cell, index_type } unsigned n = text_in_cell_ansi(self->cpu_cells + pos, self->text_cache, output); - if (output->buf[output->len - n] == 0) { - output->buf[output->len - n] = ' '; - } + if (output->buf[output->len - n] == 0) output->buf[output->len - n] = ' '; if (output->buf[output->len - n] == '\t') { unsigned num_cells_to_skip_for_tab = 0; @@ -516,7 +521,7 @@ width(Line *self, PyObject *val) { const CPUCell *c = self->cpu_cells + x; if (!cell_has_text(c)) return 0; unsigned long ans = 1; - if (c->is_multicell) ans = c->x || c->y ? 0 : cell_multicell_data(c, self->text_cache).width; + if (c->is_multicell) ans = c->x || c->y ? 0 : c->width; return PyLong_FromUnsignedLong(ans); } @@ -678,7 +683,7 @@ line_get_char(Line *self, index_type at) { if (self->cpu_cells[at].ch_is_idx) { RAII_ListOfChars(lc); text_in_cell(self->cpu_cells + at, self->text_cache, &lc); - if (lc.is_multicell && !lc.is_topleft) return 0; + if (self->cpu_cells[at].is_multicell && (self->cpu_cells[at].x || self->cpu_cells[at].y)) return 0; return lc.chars[0]; } else return self->cpu_cells[at].ch_or_idx; } @@ -839,9 +844,8 @@ apply_mark(Line *line, const uint16_t mark, index_type *cell_pos, unsigned int * MARK; } } else if (line->cpu_cells[x].is_multicell) { - MultiCellData mcd = {.val=lc.chars[lc.count]}; *match_pos += lc.count - 1; - index_type x_limit = MIN(line->xnum, mcd_x_limit(mcd)); + index_type x_limit = MIN(line->xnum, mcd_x_limit(line->cpu_cells + x)); for (; x < x_limit; x++) { MARK; } x--; } else { diff --git a/kitty/line.h b/kitty/line.h index 0239e4574..a25bd7137 100644 --- a/kitty/line.h +++ b/kitty/line.h @@ -17,7 +17,7 @@ // TODO: Cursor rendering over multicell // TODO: Test the escape codes to delete and insert characters and lines with multicell // TODO: Handle replay of dumped graphics_command and multicell_command -// TODO: Handle rewrap of multiline chars +// TODO: Handle rewrap and restitch of multiline chars // TODO: Handle rewrap when a character is too wide/tall to fit on resized screen // TODO: Test serialization to ansi only using escape code for explicitly set multicells // TODO: Test rendering of box drawing at various scales and subscales and alignments @@ -54,38 +54,29 @@ typedef struct { } GPUCell; static_assert(sizeof(GPUCell) == 20, "Fix the ordering of GPUCell"); -typedef union MultiCellData { - struct { - char_type scale: 3; - char_type width: 3; - char_type subscale: 2; - char_type vertical_align: 3; - char_type explicitly_set: 1; - char_type : 19; - char_type msb : 1; - }; - char_type val; -} MultiCellData; -static_assert(sizeof(MultiCellData) == sizeof(char_type), "Fix the ordering of MultiCellData"); - typedef union CPUCell { struct { char_type ch_or_idx: sizeof(char_type) * 8 - 1; char_type ch_is_idx: 1; char_type hyperlink_id: sizeof(hyperlink_id_type) * 8; + char_type next_char_was_wrapped : 1; + char_type is_multicell : 1; + char_type explicitly_set: 1; char_type x : 8; char_type y : 4; - char_type is_multicell : 1; - char_type next_char_was_wrapped : 1; - char_type : 2; + char_type subscale: 2; + char_type scale: 3; + char_type width: 3; + char_type vertical_align: 3; + char_type : 21; }; struct { char_type ch_and_idx: sizeof(char_type) * 8; - char_type : sizeof(hyperlink_id_type) * 8; - char_type : 16; + char_type : 32; + char_type : 32; }; } CPUCell; -static_assert(sizeof(CPUCell) == sizeof(uint64_t), "Fix the ordering of CPUCell"); +static_assert(sizeof(CPUCell) == 12, "Fix the ordering of CPUCell"); typedef union LineAttrs { struct { @@ -130,21 +121,12 @@ static inline unsigned num_codepoints_in_cell(const CPUCell *c, const TextCache } else ans = c->ch_or_idx ? 1 : 0; return ans; } -static inline MultiCellData cell_multicell_data(const CPUCell *c, const TextCache *tc) { - return (MultiCellData){.val = tc_last_char_at_index(tc, c->ch_or_idx)}; -} -static inline unsigned mcd_x_limit(MultiCellData mcd) { return mcd.scale * mcd.width; } +static inline unsigned mcd_x_limit(const CPUCell* mcd) { return mcd->scale * mcd->width; } static inline void text_in_cell(const CPUCell *c, const TextCache *tc, ListOfChars *ans) { - ans->is_multicell = false; if (c->ch_is_idx) { tc_chars_at_index(tc, c->ch_or_idx, ans); - if (c->is_multicell) { - ans->is_multicell = true; - ans->is_topleft = c->x + c->y == 0; - if (ans->count > 0) { ans->count--; } - } } else { ans->count = 1; ans->chars[0] = c->ch_or_idx; @@ -153,14 +135,8 @@ text_in_cell(const CPUCell *c, const TextCache *tc, ListOfChars *ans) { static inline bool text_in_cell_without_alloc(const CPUCell *c, const TextCache *tc, ListOfChars *ans) { - ans->is_multicell = false; if (c->ch_is_idx) { if (!tc_chars_at_index_without_alloc(tc, c->ch_or_idx, ans)) return false; - if (c->is_multicell) { - ans->is_multicell = true; - ans->is_topleft = c->x + c->y == 0; - if (ans->count > 0) { ans->count--; } - } return true; } ans->count = 1; diff --git a/kitty/screen.c b/kitty/screen.c index 5a2c42790..9a562330e 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -650,10 +650,9 @@ static void nuke_multicell_char_at(Screen *self, index_type x_, index_type y_, bool replace_with_spaces) { CPUCell *cp; GPUCell *gp; linebuf_init_cells(self->linebuf, y_, &cp, &gp); - MultiCellData mcd = cell_multicell_data(cp + x_, self->text_cache); - index_type y_max_limit = MIN(self->lines, y_ + mcd.scale); + index_type y_max_limit = MIN(self->lines, y_ + cp[x_].scale); while (cp[x_].x && x_ > 0) x_--; - index_type x_limit = MIN(self->columns, x_ + mcd_x_limit(mcd)); + index_type x_limit = MIN(self->columns, x_ + mcd_x_limit(&cp[x_])); char_type ch = replace_with_spaces ? ' ' : 0; for (index_type y = y_; y < y_max_limit; y++) { linebuf_init_cells(self->linebuf, y, &cp, &gp); @@ -674,7 +673,7 @@ nuke_multiline_char_intersecting_with(Screen *self, index_type x_start, index_ty CPUCell *cp; GPUCell *gp; linebuf_init_cells(self->linebuf, y, &cp, &gp); for (index_type x = x_start; x < x_limit; x++) { - if (cp[x].is_multicell && cell_multicell_data(cp + x, self->text_cache).scale > 1) nuke_multicell_char_at(self, x, y, replace_with_spaces); + if (cp[x].is_multicell && cp[x].scale > 1) nuke_multicell_char_at(self, x, y, replace_with_spaces); } } } @@ -704,8 +703,7 @@ nuke_split_multicell_char_at_right_boundary(Screen *self, index_type x, index_ty CPUCell *cp = linebuf_cpu_cells_for_line(self->linebuf, y); CPUCell *c = cp + x; if (c->is_multicell) { - MultiCellData mcd = cell_multicell_data(c, self->text_cache); - unsigned max_x = mcd_x_limit(mcd) - 1; + unsigned max_x = mcd_x_limit(c) - 1; if (c->x < max_x) { nuke_multicell_char_at(self, x, y, replace_with_spaces); } @@ -720,8 +718,7 @@ nuke_incomplete_single_line_multicell_chars_in_range( linebuf_init_cells(self->linebuf, y, &cpu_cells, &gpu_cells); for (index_type x = start; x < limit; x++) { if (cpu_cells[x].is_multicell) { - MultiCellData mcd = cell_multicell_data(cpu_cells + x, self->text_cache); - index_type mcd_x_limit = x + mcd.width - cpu_cells[x].x; + index_type mcd_x_limit = x + cpu_cells[x].width - cpu_cells[x].x; if (cpu_cells[x].x || mcd_x_limit > limit) nuke_in_line(cpu_cells, gpu_cells, x, MIN(mcd_x_limit, limit), replace_with_spaces ? ' ': 0); x = mcd_x_limit; } @@ -749,25 +746,19 @@ static bool halve_multicell_width(Screen *self, index_type x_, index_type y_) { CPUCell *cp; GPUCell *gp; linebuf_init_cells(self->linebuf, y_, &cp, &gp); - MultiCellData mcd = cell_multicell_data(cp + x_, self->text_cache); int y_min_limit = -1; if (self->linebuf == self->main_linebuf) y_min_limit = -(self->historybuf->count + 1); - int expected_y_min_limit = ((int)y_) - mcd.scale; + int expected_y_min_limit = ((int)y_) - cp[x_].scale; if (expected_y_min_limit < y_min_limit) return false; y_min_limit = expected_y_min_limit; - MultiCellData new_mcd = mcd; new_mcd.width = mcd.width / 2; + unsigned new_width = cp[x_].width / 2; while (cp[x_].x && x_ > 0) x_--; - index_type x_limit = MIN(self->columns, x_ + mcd_x_limit(mcd)); + index_type x_limit = MIN(self->columns, x_ + mcd_x_limit(&cp[x_])); index_type half_x_limit = x_limit / 2; - int y_max_limit = MIN(self->lines, y_ + mcd.scale); - RAII_ListOfChars(lc); - text_in_cell(cp + x_, self->text_cache, &lc); - lc.chars[lc.count++] = new_mcd.val; - cell_set_chars(cp + x_, self->text_cache, &lc); - char_type idx = cp[x_].ch_or_idx; + int y_max_limit = MIN(self->lines, y_ + cp[x_].scale); for (int y = y_min_limit + 1; y < y_max_limit; y++) { Line *line = range_line_(self, y); cp = line->cpu_cells; gp = line->gpu_cells; - for (index_type x = 0; x < half_x_limit; x++) cp[x].ch_or_idx = idx; + for (index_type x = 0; x < half_x_limit; x++) cp[x].width = new_width; for (index_type x = half_x_limit; x < x_limit; x++) { cp[x] = (CPUCell){0}; clear_sprite_position(gp[x]); } @@ -823,29 +814,21 @@ static bool add_combining_char(Screen *self, char_type ch, index_type x, index_type y) { CPUCell *cpu_cells = linebuf_cpu_cells_for_line(self->linebuf, y); CPUCell *cell = cpu_cells + x; - if (!cell_has_text(cell)) return false; // don't allow adding combining chars to a null cell + if (!cell_has_text(cell) || (cell->is_multicell && cell->y)) return false; // don't allow adding combining chars to a null cell + text_in_cell(cell, self->text_cache, self->lc); + ensure_space_for_chars(self->lc, self->lc->count + 1); + self->lc->chars[self->lc->count++] = ch; + cell->ch_or_idx = tc_get_or_insert_chars(self->text_cache, self->lc); + cell->ch_is_idx = true; if (cell->is_multicell) { + char_type ch_and_idx = cell->ch_and_idx; while (cell->x && x) cell = cpu_cells + --x; - if (cell->y) return false; // not the top left cell - text_in_cell(cell, self->text_cache, self->lc); - MultiCellData mcd = {.val=self->lc->chars[self->lc->count]}; - ensure_space_for_chars(self->lc, self->lc->count + 2); - self->lc->chars[self->lc->count++] = ch; - self->lc->chars[self->lc->count++] = mcd.val; - const char_type idx = tc_get_or_insert_chars(self->text_cache, self->lc); - self->lc->count--; - index_type x_limit = MIN(x + mcd_x_limit(mcd), self->columns); - for (index_type v = y; v < y + mcd.scale; v++) { + index_type x_limit = MIN(x + mcd_x_limit(cell), self->columns); + for (index_type v = y; v < y + cell->scale; v++) { cpu_cells = linebuf_cpu_cells_for_line(self->linebuf, v); - for (index_type h = x; h < x_limit; h++) cpu_cells[h].ch_or_idx = idx; + for (index_type h = x; h < x_limit; h++) cpu_cells[h].ch_and_idx = ch_and_idx; linebuf_mark_line_dirty(self->linebuf, v); } - } else { - text_in_cell(cell, self->text_cache, self->lc); - ensure_space_for_chars(self->lc, self->lc->count + 1); - self->lc->chars[self->lc->count++] = ch; - cell->ch_or_idx = tc_get_or_insert_chars(self->text_cache, self->lc); - cell->ch_is_idx = true; } return true; } @@ -892,38 +875,23 @@ draw_combining_char(Screen *self, text_loop_state *s, char_type ch) { if (ch == VS16) { // emoji presentation variation marker makes default text presentation emoji (narrow emoji) into wide emoji CPUCell *cpu_cell = cp + xpos; GPUCell *gpu_cell = gp + xpos; - if (self->lc->chars[base_pos + 1] == VS16 && (!cpu_cell->is_multicell || cell_multicell_data(cpu_cell, self->text_cache).width < 2) && is_emoji_presentation_base(self->lc->chars[base_pos])) { - bool already_multicell = cpu_cell->is_multicell; - if (already_multicell) { - MultiCellData mcd = {.val=self->lc->chars[self->lc->count]}; - mcd.width *= 2; - self->lc->chars[self->lc->count++] = mcd.val; - } else { - ensure_space_for_chars(self->lc, self->lc->count + 1); - MultiCellData mcd = {.width=2, .scale=1, .msb=1}; - self->lc->chars[self->lc->count++] = mcd.val; - cpu_cell->is_multicell = true; - } - cpu_cell->ch_or_idx = tc_get_or_insert_chars(self->text_cache, self->lc); - if (!already_multicell) { - if (xpos + 1 < self->columns) { - CPUCell *second = cp + xpos + 1; - if (second->is_multicell) nuke_multicell_char_at(self, xpos, ypos, false); - zero_cells(s, second, gp + xpos + 1); - self->cursor->x++; - second->is_multicell = true; second->ch_or_idx = cpu_cell->ch_or_idx; second->ch_is_idx = true; second->x = 1; - } else move_widened_char(self, s, cpu_cell, gpu_cell, xpos, ypos); - } + if (self->lc->chars[base_pos + 1] == VS16 && (!cpu_cell->is_multicell || cpu_cell->width < 2) && is_emoji_presentation_base(self->lc->chars[base_pos])) { + cpu_cell->is_multicell = true; + cpu_cell->width = 2; + if (!cpu_cell->scale) cpu_cell->scale = 1; + if (xpos + 1 < self->columns) { + CPUCell *second = cp + xpos + 1; + if (second->is_multicell) nuke_multicell_char_at(self, xpos, ypos, false); + zero_cells(s, second, gp + xpos + 1); + self->cursor->x++; + *second = *cpu_cell; second->x = 1; + } else move_widened_char(self, s, cpu_cell, gpu_cell, xpos, ypos); } } else if (ch == VS15) { const CPUCell *cpu_cell = cp + xpos; - if (self->lc->chars[base_pos + 1] == VS15 && cpu_cell->is_multicell && is_emoji_presentation_base(self->lc->chars[base_pos])) { - MultiCellData mcd = {.val=self->lc->chars[self->lc->count]}; - if (mcd.width == 2) { - if (halve_multicell_width(self, xpos, ypos)) { - self->cursor->x -= (mcd.scale * mcd.width / 2); - } - } + if (self->lc->chars[base_pos + 1] == VS15 && cpu_cell->is_multicell && cpu_cell->width == 2 && is_emoji_presentation_base(self->lc->chars[base_pos])) { + index_type deltax = (cpu_cell->scale * cpu_cell->width) / 2; + if (halve_multicell_width(self, xpos, ypos)) self->cursor->x -= deltax; } } } @@ -1061,14 +1029,9 @@ draw_text_loop(Screen *self, const uint32_t *chars, size_t num_chars, text_loop_ CPUCell *fc = s->cp + self->cursor->x; zero_cells(s, fc, s->gp + self->cursor->x); if (char_width == 2) { - MultiCellData mcd = {.width = 2, .scale = 1, .msb = 1}; - RAII_ListOfChars(lc); - lc.chars[lc.count++] = ch; - lc.chars[lc.count++] = mcd.val; + *fc = (CPUCell){.ch_or_idx=ch, .is_multicell=true, .width=2, .scale=1}; CPUCell *second = fc + 1; if (second->is_multicell) nuke_multicell_char_at(self, self->cursor->x + 1, self->cursor->y, true); - cell_set_chars(fc, self->text_cache, &lc); - fc->x = 0; fc->y = 0; fc->is_multicell = true; *second = *fc; second->x = 1; s->gp[self->cursor->x + 1] = s->gp[self->cursor->x]; self->cursor->x += 2; @@ -1155,16 +1118,17 @@ screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const width = wcswidth_string(self->lc->chars); } if (!width) return; - MultiCellData mcd = { - .width=MIN(width, 15), .scale=MAX(1, MIN(cmd->scale, 15)), .subscale=MIN(cmd->subscale, 3), - .explicitly_set=1, .msb=1, .vertical_align=MIN(cmd->vertical_align, 7u) + CPUCell mcd = { + .width=MIN(width, 15u), .scale=MAX(1, MIN(cmd->scale, 15u)), .subscale=MIN(cmd->subscale, 3u), + .explicitly_set=1u, .vertical_align=MIN(cmd->vertical_align, 7u), .is_multicell=true }; - self->lc->chars[self->lc->count++] = mcd.val; width = mcd.width * mcd.scale; index_type height = mcd.scale; index_type max_height = self->margin_bottom - self->margin_top + 1; if (width > self->columns || height > max_height) return; PREPARE_FOR_DRAW_TEXT; + mcd.hyperlink_id = s.cc.hyperlink_id; + cell_set_chars(&mcd, self->text_cache, self->lc); if (self->columns < self->cursor->x + width) { if (self->modes.mDECAWM) { continue_to_next_line(self); @@ -1187,16 +1151,13 @@ screen_handle_multicell_command(Screen *self, const MultiCellCommand *cmd, const if (self->modes.mIRM) insert_characters(self, self->cursor->x, width, y, true); } } - CPUCell c = s.cc; - c.ch_is_idx = true; c.ch_or_idx = tc_get_or_insert_chars(self->text_cache, self->lc); - c.is_multicell = true; nuke_multicell_char_intersecting_with(self, self->cursor->x, self->cursor->x + width, self->cursor->y, self->cursor->y + height, true); for (index_type y = self->cursor->y; y < self->cursor->y + height; y++) { linebuf_init_cells(self->linebuf, y, &s.cp, &s.gp); linebuf_mark_line_dirty(self->linebuf, y); - c.x = 0; c.y = y - self->cursor->y; - for (index_type x = self->cursor->x; x < self->cursor->x + width; x++, c.x++) { - s.cp[x] = c; s.gp[x] = s.g; + mcd.x = 0; mcd.y = y - self->cursor->y; + for (index_type x = self->cursor->x; x < self->cursor->x + width; x++, mcd.x++) { + s.cp[x] = mcd; s.gp[x] = s.g; } } self->cursor->x += width; @@ -2172,8 +2133,7 @@ screen_fake_move_cursor_to_position(Screen *self, index_type start_x, index_type } found_non_empty_cell = true; if (c->is_multicell) { - MultiCellData mcd = cell_multicell_data(c, self->text_cache); - x += mcd_x_limit(mcd); + x += mcd_x_limit(c); } else x++; count += 1; // zsh requires a single arrow press to move past dualwidth chars } @@ -2369,8 +2329,7 @@ screen_insert_lines(Screen *self, unsigned int count) { cells = linebuf_cpu_cells_for_line(self->linebuf, bottom); for (index_type x = 0; x < self->columns; x++) { if (cells[x].is_multicell) { - MultiCellData mcd = cell_multicell_data(cells + x, self->text_cache); - index_type y_limit = mcd.scale; + index_type y_limit = cells[x].scale; if (cells[x].y + 1u < y_limit) { index_type orig = self->lines; self->lines = bottom + 1; @@ -3753,8 +3712,7 @@ screen_draw_overlay_line(Screen *self) { // When the last character is a split multicell, make sure the next character is visible. CPUCell *c = self->linebuf->line->cpu_cells + len - 1; if (c->is_multicell) { - MultiCellData mcd = cell_multicell_data(c, self->text_cache); - if (c->x < mcd_x_limit(mcd) - 1) { + if (c->x < mcd_x_limit(c) - 1) { do { c->is_multicell = false; c->ch_is_idx = false; c->ch_or_idx = ' '; if (!c->x) break; @@ -4819,7 +4777,7 @@ current_char_width(Screen *self, PyObject *a UNUSED) { const CPUCell *c = linebuf_cpu_cells_for_line(self->linebuf, self->cursor->y) + self->cursor->x; if (c->is_multicell) { if (c->x || c->y) ans = 0; - else ans = cell_multicell_data(c, self->text_cache).width; + else ans = c->width; } } return PyLong_FromUnsignedLong(ans); @@ -5173,16 +5131,15 @@ test_parse_written_data(Screen *screen, PyObject *args) { } static PyObject* -multicell_data_as_dict(MultiCellData mcd) { - if (!mcd.msb) { PyErr_SetString(PyExc_RuntimeError, "mcd does not have its msb set"); return NULL; } +multicell_data_as_dict(CPUCell mcd) { return Py_BuildValue("{sI sI sI sO sI}", "scale", (unsigned int)mcd.scale, "width", (unsigned int)mcd.width, "subscale", (unsigned int)mcd.subscale, "explicitly_set", mcd.explicitly_set ? Py_True : Py_False, "vertical_align", mcd.vertical_align); } static PyObject* cpu_cell_as_dict(CPUCell *c, TextCache *tc, ListOfChars *lc, HYPERLINK_POOL_HANDLE h) { text_in_cell(c, tc, lc); - RAII_PyObject(mcd, lc->is_multicell ? multicell_data_as_dict((MultiCellData){.val=lc->chars[lc->count]}) : Py_NewRef(Py_None)); - if ((lc->is_multicell && !lc->is_topleft) || (lc->count == 1 && lc->chars[0] == 0)) lc->count = 0; + RAII_PyObject(mcd, c->is_multicell ? multicell_data_as_dict(*c) : Py_NewRef(Py_None)); + if ((c->is_multicell && (c->x + c->y)) || (lc->count == 1 && lc->chars[0] == 0)) lc->count = 0; RAII_PyObject(text, PyUnicode_FromKindAndData(PyUnicode_4BYTE_KIND, lc->chars, lc->count)); const char *url = c->hyperlink_id ? get_hyperlink_for_id(h, c->hyperlink_id, false) : NULL; RAII_PyObject(hyperlink, url ? PyUnicode_FromString(url) : Py_NewRef(Py_None)); diff --git a/kitty/shaders.c b/kitty/shaders.c index f474752b5..aefc150ac 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -378,8 +378,7 @@ cell_update_uniform_block(ssize_t vao_idx, Screen *screen, int uniform_buffer, c line_for_cursor && (cursor_cell = line_for_cursor->cpu_cells + cursor->x)->is_multicell && cursor_cell->x == 0 ) { - MultiCellData mcd = cell_multicell_data(cursor_cell, screen->text_cache); - rd->cursor_w = mcd.width * mcd.scale; + rd->cursor_w = mcd_x_limit(cursor_cell); } rd->xnum = screen->columns; rd->ynum = screen->lines; diff --git a/kitty/text-cache.c b/kitty/text-cache.c index 6354e0c00..9339825b0 100644 --- a/kitty/text-cache.c +++ b/kitty/text-cache.c @@ -116,7 +116,8 @@ tc_chars_at_index_ansi(const TextCache *self, char_type idx, ANSIBuf *output) { unsigned count = 0; if (self->array.count > idx) { count = self->array.items[idx].count; - ensure_space_for(output, buf, output->buf[0], output->len + count, capacity, 2048, false); + // we ensure space for one extra byte for ANSI escape code trailer if multicell + ensure_space_for(output, buf, output->buf[0], output->len + count + 1, capacity, 2048, false); memcpy(output->buf + output->len, self->array.items[idx].chars, sizeof(output->buf[0]) * count); output->len += count; } diff --git a/kitty/text-cache.h b/kitty/text-cache.h index f4e245299..0e8224315 100644 --- a/kitty/text-cache.h +++ b/kitty/text-cache.h @@ -12,7 +12,6 @@ typedef struct ListOfChars { char_type *chars; size_t count, capacity; - bool is_multicell, is_topleft; } ListOfChars; #define LIST_OF_CHARS_STACK_SIZE 4 diff --git a/kitty_tests/multicell.py b/kitty_tests/multicell.py index 391e78c56..c11f7400a 100644 --- a/kitty_tests/multicell.py +++ b/kitty_tests/multicell.py @@ -302,7 +302,7 @@ def test_multicell(self: TestMulticell) -> None: assert_line('ab_c\0\0', -2) assert_line('\0__\0\0\0', -1) self.ae(s.historybuf.line(1).as_ansi(), f'a\x1b]{TEXT_SIZE_CODE};s=2;b\x07c') - self.ae(s.historybuf.line(0).as_ansi(), ' ') + self.ae(s.historybuf.line(0).as_ansi(), '') # Insert lines s.reset()