From c655a592f9e09f863cd43acccf2eec00e1dea2d9 Mon Sep 17 00:00:00 2001 From: Jesse Miller Date: Tue, 8 Sep 2026 12:00:09 -0600 Subject: [PATCH 1/2] Add shaped run cache Per-font verstable map of exact run keys to glyph infos, positions and cell groups. Skip runs longer than 32 cells. Full-drop at 2048 entries or 1 MiB of live storage. The intention here is to avoid shaping the same runs over and over again on an active screen. This used to not have much of an impact, but now "modern" full screen TUIs are actively drawing the screens in such a way that harfbuzz shaping is showing up as a more significant portion in profiling runs. The cache is sized to not be a long-lived history but able to cache most repeated runs on a few active screens. --- kitty/shaped-run-cache.c | 249 +++++++++++++++++++++++++++++++++++++++ kitty/shaped-run-cache.h | 57 +++++++++ 2 files changed, 306 insertions(+) create mode 100644 kitty/shaped-run-cache.c create mode 100644 kitty/shaped-run-cache.h diff --git a/kitty/shaped-run-cache.c b/kitty/shaped-run-cache.c new file mode 100644 index 000000000..b3257c693 --- /dev/null +++ b/kitty/shaped-run-cache.c @@ -0,0 +1,249 @@ +/* + * shaped-run-cache.c + * Copyright (C) 2026 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "shaped-run-cache.h" + +#define SHAPED_RUN_MAX_CELLS 32u +#define SHAPED_RUN_MAX_ENTRIES 2048u +#define SHAPED_RUN_MAX_BYTES (1024u * 1024u) + +typedef struct ShapedRunKey { + uint32_t keysz_in_bytes; + uint8_t disable_ligature, force_ltr, scale, subscale; + uint8_t data[]; +} ShapedRunKey; +static_assert(sizeof(ShapedRunKey) == 8, "Fix the ordering of ShapedRunKey"); + +typedef struct ShapedRunValue { + unsigned num_glyphs, num_groups; +} ShapedRunValue; + +static inline hb_glyph_info_t * +val_info(ShapedRunValue *v) { + return (hb_glyph_info_t *)(v + 1); +} + +static inline hb_glyph_position_t * +val_positions(ShapedRunValue *v) { + return (hb_glyph_position_t *)(val_info(v) + v->num_glyphs); +} + +static inline ShapeGroup * +val_groups(ShapedRunValue *v) { + return (ShapeGroup *)(val_positions(v) + v->num_glyphs); +} + +static inline size_t +val_size(unsigned num_glyphs, unsigned num_groups) { + return sizeof(ShapedRunValue) + num_glyphs * (sizeof(hb_glyph_info_t) + sizeof(hb_glyph_position_t)) + num_groups * sizeof(ShapeGroup); +} + +#define NAME shaped_run_map +#define KEY_TY const ShapedRunKey * +#define VAL_TY ShapedRunValue * +static uint64_t shaped_run_map_hash(KEY_TY key); +#define HASH_FN shaped_run_map_hash +static bool shaped_run_map_cmpr(KEY_TY a, KEY_TY b); +#define CMPR_FN shaped_run_map_cmpr +#define MA_NAME Key +#define MA_BLOCK_SIZE 16u +static_assert(MA_BLOCK_SIZE > sizeof(ShapedRunKey), "increase arena block size"); +#define MA_ARENA_NUM_BLOCKS (2048u / MA_BLOCK_SIZE) +#include "arena.h" +#define MA_NAME Val +#define MA_BLOCK_SIZE 16u +#define MA_ARENA_NUM_BLOCKS (2048u / MA_BLOCK_SIZE) +#include "arena.h" + +#include "kitty-verstable.h" + +static uint64_t +shaped_run_map_hash(const ShapedRunKey *key) { + return vt_hash_bytes(key, sizeof(ShapedRunKey) + key->keysz_in_bytes); +} + +static bool +shaped_run_map_cmpr(const ShapedRunKey *a, const ShapedRunKey *b) { + return a->keysz_in_bytes == b->keysz_in_bytes && memcmp(a, b, sizeof(ShapedRunKey) + a->keysz_in_bytes) == 0; +} + +typedef struct HashTable { + shaped_run_map table; + KeyMonotonicArena keys; + ValMonotonicArena vals; + struct { + ShapedRunKey *key; + size_t capacity; + } scratch; +} HashTable; + +SHAPED_RUN_MAP_HANDLE +create_shaped_run_hash_table(void) { + HashTable *ans = calloc(1, sizeof(HashTable)); + if (ans) vt_init(&ans->table); + return (SHAPED_RUN_MAP_HANDLE)ans; +} + +void +free_shaped_run_hash_table(SHAPED_RUN_MAP_HANDLE *map) { + HashTable **mapref = (HashTable **)map; + if (*mapref) { + vt_cleanup(&mapref[0]->table); + Key_free_all(&mapref[0]->keys); + Val_free_all(&mapref[0]->vals); + free(mapref[0]->scratch.key); + free(mapref[0]); + mapref[0] = NULL; + } +} + +static bool +shaped_run_too_long(index_type num_cells) { + return num_cells > SHAPED_RUN_MAX_CELLS; +} + +static bool +fill_key( + HashTable *ht, const CPUCell *cells, index_type num_cells, const TextCache *tc, bool disable_ligature, bool force_ltr, uint8_t scale, uint8_t subscale) { + const size_t keysz_in_bytes = (size_t)num_cells * (sizeof(uint32_t) + MAX_NUM_CODEPOINTS_PER_CELL * sizeof(char_type)); + if (!ht->scratch.key || keysz_in_bytes > ht->scratch.capacity) { + const size_t newsz = sizeof(ht->scratch.key[0]) + keysz_in_bytes + 64; + ht->scratch.key = realloc(ht->scratch.key, newsz); + if (!ht->scratch.key) { + ht->scratch.capacity = 0; + return false; + } + ht->scratch.capacity = newsz - sizeof(ht->scratch.key[0]); + memset(ht->scratch.key, 0, newsz); + } +#define scratch ht->scratch.key + RAII_ListOfChars(lc); + size_t used = 0; + scratch->disable_ligature = disable_ligature; + scratch->force_ltr = force_ltr; + scratch->scale = scale; + scratch->subscale = subscale; + for (; num_cells; cells++, num_cells--) { + if (cells->is_multicell && cells->x) continue; + text_in_cell(cells, tc, &lc); + uint16_t advance = 1; + if (cells->is_multicell) advance = (uint16_t)(cells->width * cells->scale); + uint32_t hdr = ((uint32_t)advance << 16) | (uint32_t)lc.count; + memcpy(scratch->data + used, &hdr, sizeof(hdr)); + used += sizeof(hdr); + if (lc.count) { + memcpy(scratch->data + used, lc.chars, lc.count * sizeof(char_type)); + used += lc.count * sizeof(char_type); + } + } + scratch->keysz_in_bytes = (uint32_t)used; + return true; +#undef scratch +} + +bool +shaped_run_get( + SHAPED_RUN_MAP_HANDLE map, + const CPUCell *cells, + index_type num_cells, + const TextCache *tc, + bool disable_ligature, + bool force_ltr, + uint8_t scale, + uint8_t subscale, + ShapedRun *result) { + HashTable *ht = (HashTable *)map; + if (!ht) return false; + if (shaped_run_too_long(num_cells)) return false; + if (!fill_key(ht, cells, num_cells, tc, disable_ligature, force_ltr, scale, subscale)) return false; +#define scratch ht->scratch.key + shaped_run_map_itr n = vt_get(&ht->table, scratch); + if (vt_is_end(n)) return false; + ShapedRunValue *v = n.data->val; + result->num_glyphs = v->num_glyphs; + result->num_groups = v->num_groups; + result->info = v->num_glyphs ? val_info(v) : NULL; + result->positions = v->num_glyphs ? val_positions(v) : NULL; + result->groups = v->num_groups ? val_groups(v) : NULL; + return true; +#undef scratch +} + +static size_t +key_arena_alloc(const KeyMonotonicArena *a) { + size_t n = a->capacity * sizeof(a->blocks[0]); + for (size_t i = 0; i < a->count; i++) n += a->blocks[i].capacity; + return n; +} + +static size_t +val_arena_alloc(const ValMonotonicArena *a) { + size_t n = a->capacity * sizeof(a->blocks[0]); + for (size_t i = 0; i < a->count; i++) n += a->blocks[i].capacity; + return n; +} + +static size_t +shaped_run_live_bytes(const HashTable *ht) { + const size_t buckets = vt_bucket_count(&ht->table); + const size_t scratch = ht->scratch.key ? sizeof(ShapedRunKey) + ht->scratch.capacity : 0; + return sizeof(HashTable) + scratch + key_arena_alloc(&ht->keys) + val_arena_alloc(&ht->vals) + buckets * (sizeof(shaped_run_map_bucket) + sizeof(uint16_t)); +} + +static bool +shaped_run_at_cap(const HashTable *ht) { + if (vt_size(&ht->table) >= SHAPED_RUN_MAX_ENTRIES) return true; + if (shaped_run_live_bytes(ht) >= SHAPED_RUN_MAX_BYTES) return true; + return false; +} + +static void +shaped_run_drop(HashTable *ht) { + vt_cleanup(&ht->table); + Key_free_all(&ht->keys); + Val_free_all(&ht->vals); + vt_init(&ht->table); +} + +void +shaped_run_put( + SHAPED_RUN_MAP_HANDLE map, + const CPUCell *cells, + index_type num_cells, + const TextCache *tc, + bool disable_ligature, + bool force_ltr, + uint8_t scale, + uint8_t subscale, + const hb_glyph_info_t *info, + const hb_glyph_position_t *positions, + unsigned num_glyphs, + const ShapeGroup *groups, + unsigned num_groups) { + HashTable *ht = (HashTable *)map; + if (!ht) return; + if (shaped_run_too_long(num_cells)) return; + if ((num_glyphs && (!info || !positions)) || (num_groups && !groups)) return; + if (!fill_key(ht, cells, num_cells, tc, disable_ligature, force_ltr, scale, subscale)) return; +#define scratch ht->scratch.key + if (!vt_is_end(vt_get(&ht->table, scratch))) return; + if (shaped_run_at_cap(ht)) shaped_run_drop(ht); + ShapedRunKey *key = Key_get(&ht->keys, sizeof(ShapedRunKey) + scratch->keysz_in_bytes); + if (!key) return; + memcpy(key, scratch, sizeof(scratch[0]) + scratch->keysz_in_bytes); + ShapedRunValue *val = Val_get(&ht->vals, val_size(num_glyphs, num_groups)); + if (!val) return; + val->num_glyphs = num_glyphs; + val->num_groups = num_groups; + if (num_glyphs) { + memcpy(val_info(val), info, num_glyphs * sizeof(info[0])); + memcpy(val_positions(val), positions, num_glyphs * sizeof(positions[0])); + } + if (num_groups) memcpy(val_groups(val), groups, num_groups * sizeof(groups[0])); + if (vt_is_end(vt_insert(&ht->table, key, val))) return; +#undef scratch +} diff --git a/kitty/shaped-run-cache.h b/kitty/shaped-run-cache.h new file mode 100644 index 000000000..9087c6710 --- /dev/null +++ b/kitty/shaped-run-cache.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2026 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#pragma once + +#include "line.h" +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wpedantic" +#include +#pragma GCC diagnostic pop + +typedef struct ShapeGroup { + unsigned int first_glyph_idx, first_cell_idx, num_glyphs, num_cells; + bool has_special_glyph, started_with_infinite_ligature; +} ShapeGroup; + +typedef struct ShapedRun { + const hb_glyph_info_t *info; + const hb_glyph_position_t *positions; + const ShapeGroup *groups; + unsigned num_glyphs, num_groups; +} ShapedRun; + +typedef struct { + int x; +} *SHAPED_RUN_MAP_HANDLE; + +SHAPED_RUN_MAP_HANDLE +create_shaped_run_hash_table(void); +void free_shaped_run_hash_table(SHAPED_RUN_MAP_HANDLE *handle); +bool shaped_run_get( + SHAPED_RUN_MAP_HANDLE map, + const CPUCell *cells, + index_type num_cells, + const TextCache *tc, + bool disable_ligature, + bool force_ltr, + uint8_t scale, + uint8_t subscale, + ShapedRun *result); +void shaped_run_put( + SHAPED_RUN_MAP_HANDLE map, + const CPUCell *cells, + index_type num_cells, + const TextCache *tc, + bool disable_ligature, + bool force_ltr, + uint8_t scale, + uint8_t subscale, + const hb_glyph_info_t *info, + const hb_glyph_position_t *positions, + unsigned num_glyphs, + const ShapeGroup *groups, + unsigned num_groups); From 073957f0141b90e2067dc0dcd9631a8b14f07b67 Mon Sep 17 00:00:00 2001 From: Jesse Miller Date: Tue, 8 Sep 2026 12:00:09 -0600 Subject: [PATCH 2/2] 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) {