Font fallback: Fix the font used to render a character sometimes dependent on the order in which characters appear on screen

We ameliorate the performance hit by storing a hash table mapping cell
text to the loaded fallback font index so that lookups for previously
seen text are still fast.

Fixes #6865
This commit is contained in:
Kovid Goyal 2023-12-11 20:27:21 +05:30
parent 7cec82f453
commit d9ccbcd0ce
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
6 changed files with 85 additions and 14 deletions

View file

@ -61,6 +61,8 @@ Detailed list of changes
- macOS: Fix returning from full screen via the button when the titlebar is hidden not hiding the buttons (:iss:`6883`)
- Font fallback: Fix the font used to render a character sometimes dependent on the order in which characters appear on screen (:iss:`6865`)
0.31.0 [2023-11-08]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View file

@ -240,7 +240,7 @@ find_substitute_face(CFStringRef str, CTFontRef old_font, CPUCell *cpu_cell) {
new_font = manually_search_fallback_fonts(old_font, cpu_cell);
if (new_font) return new_font;
}
PyErr_SetString(PyExc_ValueError, "Failed to find fallback CTFont other than the LastResort font");
PyErr_Format(PyExc_ValueError, "Failed to find fallback CTFont other than the LastResort font for: %s", [(NSString *)str UTF8String]);
return NULL;
}
return new_font;
@ -270,7 +270,22 @@ create_fallback_face(PyObject *base_face, CPUCell* cell, bool UNUSED bold, bool
}
else { search_for_fallback(); }
if (new_font == NULL) return NULL;
return (PyObject*)ct_face(new_font, fg);
NSURL *url = (NSURL*)CTFontCopyAttribute(new_font, kCTFontURLAttribute);
const char *font_path = [[url path] UTF8String];
ssize_t idx = -1;
PyObject *q, *ans = NULL;
while ((q = iter_fallback_faces(fg, &idx))) {
CTFace *qf = (CTFace*)q;
const char *qpath;
if (qf->path && (qpath = PyUnicode_AsUTF8(qf->path)) && strcmp(qpath, font_path) == 0) {
ans = PyLong_FromSsize_t(idx);
break;
}
}
[url release];
if (ans == NULL) return (PyObject*)ct_face(new_font, fg);
CFRelease(new_font);
return ans;
}
unsigned int

View file

@ -450,7 +450,15 @@ create_fallback_face(PyObject UNUSED *base_face, CPUCell* cell, bool bold, bool
size_t num = cell_as_unicode_for_fallback(cell, char_buf);
add_charset(pat, num);
PyObject *d = _fc_match(pat);
if (d) { ans = face_from_descriptor(d, fg); Py_CLEAR(d); }
if (d) {
ssize_t idx = -1;
PyObject *q;
while ((q = iter_fallback_faces(fg, &idx))) {
if (face_equals_descriptor(q, d)) { ans = PyLong_FromSsize_t(idx); Py_CLEAR(d); goto end; }
}
ans = face_from_descriptor(d, fg);
Py_CLEAR(d);
}
end:
if (pat != NULL) FcPatternDestroy(pat);
return ans;

View file

@ -12,6 +12,7 @@
#include "unicode-data.h"
#include "charsets.h"
#include "glyph-cache.h"
#include "kitty-uthash.h"
#define MISSING_GLYPH (NUM_UNDERLINE_STYLES + 2)
#define MAX_NUM_EXTRA_GLYPHS_PUA 4u
@ -71,6 +72,12 @@ typedef struct Canvas {
unsigned current_cells, alloced_cells;
} Canvas;
typedef struct fallback_font_map {
const char *cell_text;
size_t font_idx;
UT_hash_handle hh;
} fallback_font_map_t;
typedef struct {
FONTS_DATA_HEAD
id_type id;
@ -80,6 +87,7 @@ typedef struct {
Font *fonts;
Canvas canvas;
GPUSpriteTracker sprite_tracker;
fallback_font_map_t *fallback_font_map;
} FontGroup;
static FontGroup* font_groups = NULL;
@ -152,6 +160,15 @@ static void
del_font_group(FontGroup *fg) {
free(fg->canvas.buf); fg->canvas.buf = NULL; fg->canvas = (Canvas){0};
fg->sprite_map = free_sprite_map(fg->sprite_map);
if (fg->fallback_font_map) {
fallback_font_map_t *current, *tmp;
HASH_ITER(hh, fg->fallback_font_map, current, tmp) {
free((void*)current->cell_text);
HASH_DEL(fg->fallback_font_map, current);
free(current);
}
fg->fallback_font_map = NULL;
}
for (size_t i = 0; i < fg->fonts_count; i++) del_font(fg->fonts + i);
free(fg->fonts); fg->fonts = NULL;
}
@ -451,6 +468,16 @@ output_cell_fallback_data(CPUCell *cell, bool bold, bool italic, bool emoji_pres
printf("\n");
}
PyObject*
iter_fallback_faces(FONTS_DATA_HANDLE fgh, ssize_t *idx) {
FontGroup *fg = (FontGroup*)fgh;
if (*idx + 1 < (ssize_t)fg->fallback_fonts_count) {
*idx += 1;
return fg->fonts[fg->first_fallback_font_idx + *idx].face;
}
return NULL;
}
static ssize_t
load_fallback_font(FontGroup *fg, CPUCell *cell, bool bold, bool italic, bool emoji_presentation) {
if (fg->fallback_fonts_count > 100) { log_error("Too many fallback fonts"); return MISSING_FONT; }
@ -464,6 +491,7 @@ load_fallback_font(FontGroup *fg, CPUCell *cell, bool bold, bool italic, bool em
if (face == NULL) { PyErr_Print(); return MISSING_FONT; }
if (face == Py_None) { Py_DECREF(face); return MISSING_FONT; }
if (global_state.debug_font_fallback) output_cell_fallback_data(cell, bold, italic, emoji_presentation, face, true);
if (PyLong_Check(face)) { ssize_t ans = fg->first_fallback_font_idx + PyLong_AsSsize_t(face); Py_DECREF(face); return ans; }
set_size_for_face(face, fg->cell_height, true, (FONTS_DATA_HANDLE)fg);
ensure_space_for(fg, fonts, Font, fg->fonts_count + 1, fonts_capacity, 5, true);
@ -495,17 +523,24 @@ fallback_font(FontGroup *fg, CPUCell *cpu_cell, GPUCell *gpu_cell) {
bool bold = gpu_cell->attrs.bold;
bool italic = gpu_cell->attrs.italic;
bool emoji_presentation = has_emoji_presentation(cpu_cell, gpu_cell);
// Check if one of the existing fallback fonts has this text
for (size_t i = 0, j = fg->first_fallback_font_idx; i < fg->fallback_fonts_count; i++, j++) {
Font *ff = fg->fonts +j;
if (ff->bold == bold && ff->italic == italic && ff->emoji_presentation == emoji_presentation && has_cell_text(ff, cpu_cell)) {
if (global_state.debug_font_fallback) output_cell_fallback_data(cpu_cell, bold, italic, emoji_presentation, ff->face, false);
return j;
char cell_text[6 + arraysz(cpu_cell->cc_idx) * 4];
const size_t cell_text_len = cell_as_utf8(cpu_cell, true, cell_text, ' ');
if (fg->fallback_font_map) {
fallback_font_map_t *s;
HASH_FIND_STR(fg->fallback_font_map, cell_text, s);
/* printf("cache %s\n", (s ? "hit" : "miss")); */
if (s) return s->font_idx;
}
ssize_t idx = load_fallback_font(fg, cpu_cell, bold, italic, emoji_presentation);
fallback_font_map_t *ffm = calloc(1, sizeof(fallback_font_map_t));
if (ffm) {
ffm->font_idx = idx;
ffm->cell_text = strndup(cell_text, cell_text_len);
if (ffm->cell_text) {
HASH_ADD_KEYPTR(hh, fg->fallback_font_map, ffm->cell_text, cell_text_len, ffm);
}
}
return load_fallback_font(fg, cpu_cell, bold, italic, emoji_presentation);
return idx;
}
static ssize_t

View file

@ -30,6 +30,8 @@ PyObject* create_fallback_face(PyObject *base_face, CPUCell* cell, bool bold, bo
PyObject* specialize_font_descriptor(PyObject *base_descriptor, FONTS_DATA_HANDLE);
PyObject* face_from_path(const char *path, int index, FONTS_DATA_HANDLE);
PyObject* face_from_descriptor(PyObject*, FONTS_DATA_HANDLE);
PyObject* iter_fallback_faces(FONTS_DATA_HANDLE fgh, ssize_t *idx);
bool face_equals_descriptor(PyObject *face_, PyObject *descriptor);
const char* postscript_name_for_face(const PyObject*);
void sprite_tracker_current_layout(FONTS_DATA_HANDLE data, unsigned int *x, unsigned int *y, unsigned int *z);

View file

@ -218,6 +218,17 @@ set_load_error(const char *path, int error) {
return NULL;
}
bool
face_equals_descriptor(PyObject *face_, PyObject *descriptor) {
Face *face = (Face*)face_;
PyObject *t = PyDict_GetItemString(descriptor, "path");
if (!t) return false;
if (PyObject_RichCompareBool(face->path, t, Py_EQ) != 1) return false;
t = PyDict_GetItemString(descriptor, "index");
if (t && PyLong_AsLong(t) != face->index) return false;
return true;
}
PyObject*
face_from_descriptor(PyObject *descriptor, FONTS_DATA_HANDLE fg) {
#define D(key, conv, missing_ok) { \
@ -285,7 +296,6 @@ repr(Face *self) {
);
}
const char*
postscript_name_for_face(const PyObject *face_) {
const Face *self = (const Face*)face_;
@ -723,7 +733,6 @@ static PyMethodDef methods[] = {
{NULL} /* Sentinel */
};
PyTypeObject Face_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.Face",