From 073957f0141b90e2067dc0dcd9631a8b14f07b67 Mon Sep 17 00:00:00 2001 From: Jesse Miller Date: Tue, 8 Sep 2026 12:00:09 -0600 Subject: [PATCH] Cache HarfBuzz text shaping results Wire the shaper-cache to actually be used for shaped runs. On a 10s loop of the same short-word screen (~16ms/frame), main-thread CPU dropped ~28% vs master and hb_shape samples went to 0. Parse speed remains unchanged (and fast), verified with kitten __benchmark__. --- docs/changelog.rst | 2 + kitty/fonts.c | 185 +++++++++++++++++++++++++++++++++++++++------ 2 files changed, 162 insertions(+), 25 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 49910b362..f19e5af15 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -39,6 +39,8 @@ should see a 15-35% improvement depending on workload. Some details: #. Speed up pixel compositing with :term:`SIMD` vectorization: alpha blending of graphics protocol images and animation frames is 2-3.5x faster and glyph alpha masks are composited onto canvases using the same vectorized primitives. +#. Cache HarfBuzz results for repeated short runs (≤32 cells) so redraws of the same on-screen text are not reshaped + Vertical tabs [0.48] ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/kitty/fonts.c b/kitty/fonts.c index 2b4b6a5a1..4e7b59dbe 100644 --- a/kitty/fonts.c +++ b/kitty/fonts.c @@ -13,6 +13,7 @@ #include "char-props.h" #include "decorations.h" #include "glyph-cache.h" +#include "shaped-run-cache.h" #include "print-graphics.h" #include "simd-string.h" @@ -70,6 +71,7 @@ typedef struct { hb_feature_t *ffs_hb_features; size_t num_ffs_hb_features; GLYPH_PROPERTIES_MAP_HANDLE glyph_properties_hash_table; + SHAPED_RUN_MAP_HANDLE shaped_run_hash_table; bool bold, italic, emoji_presentation; SpacerStrategy spacer_strategy; } Font; @@ -237,6 +239,7 @@ void free_maps(Font *font) { free_sprite_position_hash_table(&font->sprite_position_hash_table); free_glyph_properties_hash_table(&font->glyph_properties_hash_table); + free_shaped_run_hash_table(&font->shaped_run_hash_table); } static void @@ -523,6 +526,11 @@ init_hash_tables(Font *f) { PyErr_NoMemory(); return false; } + f->shaped_run_hash_table = create_shaped_run_hash_table(); + if (!f->shaped_run_hash_table) { + PyErr_NoMemory(); + return false; + } return true; } @@ -1420,10 +1428,7 @@ typedef struct { char_type current_codepoint; } CellData; -typedef struct { - unsigned int first_glyph_idx, first_cell_idx, num_glyphs, num_cells; - bool has_special_glyph, started_with_infinite_ligature; -} Group; +typedef ShapeGroup Group; typedef struct { uint32_t previous_cluster; @@ -1433,19 +1438,70 @@ typedef struct { size_t groups_capacity, group_idx, glyph_idx, cell_idx, num_cells, num_glyphs; CPUCell *first_cpu_cell, *last_cpu_cell; GPUCell *first_gpu_cell, *last_gpu_cell; - hb_glyph_info_t *info; - hb_glyph_position_t *positions; + hb_glyph_info_t *info, *info_storage; + hb_glyph_position_t *positions, *pos_storage; + size_t glyph_capacity; } GroupState; static GroupState group_state = {0}; +static bool +ensure_group_glyph_storage(size_t n) { + if (group_state.glyph_capacity >= n) return true; + size_t cap = MAX(n, group_state.glyph_capacity * 2); + if (!cap) cap = 64; + hb_glyph_info_t *info = realloc(group_state.info_storage, cap * sizeof(*info)); + if (!info) return false; + group_state.info_storage = info; + hb_glyph_position_t *pos = realloc(group_state.pos_storage, cap * sizeof(*pos)); + if (!pos) return false; + group_state.pos_storage = pos; + group_state.glyph_capacity = cap; + return true; +} + +static bool +ensure_group_capacity(size_t num_cells, size_t num_groups) { + size_t needed = MAX(num_groups, 2 * num_cells); + if (group_state.groups_capacity >= needed && group_state.groups) return true; + size_t cap = MAX(128u, needed); + Group *groups = realloc(group_state.groups, sizeof(Group) * cap); + if (!groups) return false; + group_state.groups = groups; + group_state.groups_capacity = cap; + return true; +} + +static bool +restore_shaped_run(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells, const ShapedRun *cached) { + if (!ensure_group_capacity(num_cells, cached->num_groups)) return false; + zero_at_ptr_count(group_state.groups, group_state.groups_capacity); + if (cached->num_groups) memcpy(group_state.groups, cached->groups, cached->num_groups * sizeof(Group)); + if (cached->num_glyphs) { + if (!ensure_group_glyph_storage(cached->num_glyphs)) return false; + memcpy(group_state.info_storage, cached->info, cached->num_glyphs * sizeof(cached->info[0])); + memcpy(group_state.pos_storage, cached->positions, cached->num_glyphs * sizeof(cached->positions[0])); + group_state.info = group_state.info_storage; + group_state.positions = group_state.pos_storage; + } else { + group_state.info = NULL; + group_state.positions = NULL; + } + group_state.num_glyphs = cached->num_glyphs; + group_state.num_cells = num_cells; + group_state.group_idx = cached->num_groups ? cached->num_groups - 1 : 0; + group_state.glyph_idx = 0; + group_state.cell_idx = 0; + group_state.first_cpu_cell = first_cpu_cell; + group_state.first_gpu_cell = first_gpu_cell; + group_state.last_cpu_cell = first_cpu_cell + (num_cells ? num_cells - 1 : 0); + group_state.last_gpu_cell = first_gpu_cell + (num_cells ? num_cells - 1 : 0); + return true; +} + static void shape(CPUCell *first_cpu_cell, GPUCell *first_gpu_cell, index_type num_cells, hb_font_t *font, Font *fobj, bool disable_ligature, const TextCache *tc) { - if (group_state.groups_capacity <= 2 * num_cells) { - group_state.groups_capacity = MAX(128u, 2 * num_cells); // avoid unnecessary reallocs - group_state.groups = realloc(group_state.groups, sizeof(Group) * group_state.groups_capacity); - if (!group_state.groups) fatal("Out of memory"); - } + if (!ensure_group_capacity(num_cells, 0)) fatal("Out of memory"); RAII_ListOfChars(lc); text_in_cell(first_cpu_cell, tc, &lc); group_state.previous_cluster = UINT32_MAX; @@ -1821,6 +1877,15 @@ shape_run( const TextCache *tc, ListOfChars *lc) { float scale = apply_scale_to_font_group(fg, &rf); + const uint8_t key_scale = (uint8_t)MAX(1u, rf.scale); + uint8_t subscale = ((rf.subscale_n & 0xf) << 4) | (rf.subscale_d & 0xf); + ShapedRun cached; + if (shaped_run_get(font->shaped_run_hash_table, first_cpu_cell, num_cells, tc, disable_ligature, OPT(force_ltr), key_scale, subscale, &cached)) { + if (restore_shaped_run(first_cpu_cell, first_gpu_cell, num_cells, &cached)) { + if (scale != 1.f) apply_scale_to_font_group(fg, NULL); + return scale; + } + } if (scale != 1.f) if (!face_apply_scaling(font->face, (FONTS_DATA_HANDLE)fg) && PyErr_Occurred()) PyErr_Print(); hb_font_t *hbf = harfbuzz_font_for_face(font->face); @@ -1834,6 +1899,20 @@ shape_run( hb_buffer_serialize_glyphs(harfbuzz_buffer, 0, group_state.num_glyphs, dbuf, sizeof(dbuf), NULL, harfbuzz_font_for_face(font->face), HB_BUFFER_SERIALIZE_FORMAT_TEXT, HB_BUFFER_SERIALIZE_FLAG_DEFAULT | HB_BUFFER_SERIALIZE_FLAG_GLYPH_EXTENTS); printf("\n%s\n", dbuf); #endif + shaped_run_put( + font->shaped_run_hash_table, + first_cpu_cell, + num_cells, + tc, + disable_ligature, + OPT(force_ltr), + key_scale, + subscale, + G(info), + G(positions), + (unsigned)G(num_glyphs), + G(groups), + (unsigned)(G(group_idx) + 1)); if (scale != 1.f) { apply_scale_to_font_group(fg, NULL); if (!face_apply_scaling(font->face, (FONTS_DATA_HANDLE)fg) && PyErr_Occurred()) PyErr_Print(); @@ -1914,6 +1993,45 @@ render_groups(FontGroup *fg, RunFont rf, bool center_glyph, const TextCache *tc) } } +static PyObject * +shaped_groups_as_python(void) { + PyObject *ans = PyList_New(0); + if (!ans) return NULL; + unsigned int idx = 0; + while (idx <= G(group_idx)) { + Group *group = G(groups) + idx; + if (!group->num_cells) break; + glyph_index first_glyph = group->num_glyphs ? G(info)[group->first_glyph_idx].codepoint : 0; + PyObject *eg = PyTuple_New(group->num_glyphs); + if (!eg) { + Py_DECREF(ans); + return NULL; + } + for (size_t g = 0; g < group->num_glyphs; g++) { + PyObject *gid = Py_BuildValue("H", G(info)[group->first_glyph_idx + g].codepoint); + if (!gid) { + Py_DECREF(eg); + Py_DECREF(ans); + return NULL; + } + PyTuple_SET_ITEM(eg, g, gid); + } + PyObject *item = Py_BuildValue("IIHN", group->num_cells, group->num_glyphs, first_glyph, eg); + if (!item) { + Py_DECREF(ans); + return NULL; + } + if (PyList_Append(ans, item) != 0) { + Py_DECREF(item); + Py_DECREF(ans); + return NULL; + } + Py_DECREF(item); + idx++; + } + return ans; +} + static PyObject * test_shape(PyObject UNUSED *self, PyObject *args) { Line *line; @@ -1938,28 +2056,38 @@ test_shape(PyObject UNUSED *self, PyObject *args) { face = face_from_path(path, index, (FONTS_DATA_HANDLE)font_groups); if (face == NULL) return NULL; font = calloc(1, sizeof(Font)); + if (!font) { + Py_CLEAR(face); + return PyErr_NoMemory(); + } font->face = face; - if (!init_hash_tables(font)) return NULL; + if (!init_hash_tables(font)) { + Py_CLEAR(face); + free_maps(font); + free(font); + return NULL; + } } else { font = fg->fonts + fg->medium_font_idx; } RunFont rf = {0}; RAII_ListOfChars(lc); shape_run(line->cpu_cells, line->gpu_cells, num, font, rf, fg, false, line->text_cache, &lc); - - PyObject *ans = PyList_New(0); - unsigned int idx = 0; - glyph_index first_glyph; - while (idx <= G(group_idx)) { - Group *group = G(groups) + idx; - if (!group->num_cells) break; - first_glyph = group->num_glyphs ? G(info)[group->first_glyph_idx].codepoint : 0; - - PyObject *eg = PyTuple_New(group->num_glyphs); - for (size_t g = 0; g < group->num_glyphs; g++) PyTuple_SET_ITEM(eg, g, Py_BuildValue("H", G(info)[group->first_glyph_idx + g].codepoint)); - PyList_Append(ans, Py_BuildValue("IIHN", group->num_cells, group->num_glyphs, first_glyph, eg)); - idx++; + PyObject *ans = shaped_groups_as_python(); + if (!ans) goto done; + shape_run(line->cpu_cells, line->gpu_cells, num, font, rf, fg, false, line->text_cache, &lc); + PyObject *cached = shaped_groups_as_python(); + if (!cached) { + Py_CLEAR(ans); + goto done; } + int same = PyObject_RichCompareBool(ans, cached, Py_EQ); + Py_DECREF(cached); + if (same != 1) { + Py_CLEAR(ans); + if (same == 0) PyErr_SetString(PyExc_RuntimeError, "shaped run cache returned a different result than HarfBuzz"); + } +done: if (face) { Py_CLEAR(face); free_maps(font); @@ -2370,8 +2498,15 @@ finalize(void) { harfbuzz_buffer = NULL; } free(group_state.groups); + free(group_state.info_storage); + free(group_state.pos_storage); group_state.groups = NULL; + group_state.info_storage = NULL; + group_state.pos_storage = NULL; + group_state.info = NULL; + group_state.positions = NULL; group_state.groups_capacity = 0; + group_state.glyph_capacity = 0; free(global_glyph_render_scratch.glyphs); free(global_glyph_render_scratch.sprite_positions); if (global_glyph_render_scratch.lc) {