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__.
This commit is contained in:
Jesse Miller 2026-09-08 12:00:09 -06:00
parent c655a592f9
commit 073957f014
2 changed files with 162 additions and 25 deletions

View file

@ -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]
~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -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) {