fix(fonts): one BDF face per thread

The shared face cache now hands every loader (FontManager, element_style,
DisplayManager, the harness) the same freetype.Face. FreeType does not allow
two threads to use one face at once, since load_char rewrites its glyph
slot, so key the cache by thread as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-23 17:37:56 -04:00
co-authored by Claude Opus 5.5
parent 048a709a5e
commit 7cee46926d
2 changed files with 22 additions and 4 deletions
+8 -4
View File
@@ -101,9 +101,12 @@ def load_bdf_face(path: str, size_px: int) -> Tuple[Any, int]:
font that was asked for at the size it can do. Callers that lay out by
size need ``realised_px``, not the size they asked for.
Faces are cached and shared: a ``freetype.Face`` holds per-glyph state,
so treat one as owned by the thread that draws. Raises if the file can't
be loaded at either size.
Faces are cached per thread. A ``freetype.Face`` holds per-glyph state
(``load_char`` rewrites its glyph slot), and FreeType does not allow two
threads to use one face at once, so the display thread and a plugin's
update thread must never be handed the same object. Within a thread the
face is shared by every caller. Raises if the file can't be loaded at
either size.
"""
if freetype is None:
raise RuntimeError("freetype-py is not installed; BDF fonts need it")
@@ -111,7 +114,8 @@ def load_bdf_face(path: str, size_px: int) -> Tuple[Any, int]:
abs_path = os.path.abspath(path)
try:
st = os.stat(abs_path)
key = (abs_path, size_px, st.st_mtime_ns, st.st_size)
key = (threading.get_ident(), abs_path, size_px,
st.st_mtime_ns, st.st_size)
except OSError:
key = None # let freetype raise its own error below
+14
View File
@@ -263,6 +263,20 @@ def test_cache_is_bounded(monkeypatch):
bdf_font.clear_face_cache()
def test_each_thread_gets_its_own_face():
# FreeType forbids two threads using one face at once: load_char rewrites
# the face's glyph slot. Within a thread the face is shared.
import threading
path = str(FONTS_DIR / "5x7.bdf")
here, _ = load_bdf_face(path, 7)
assert load_bdf_face(path, 7)[0] is here
other = []
worker = threading.Thread(target=lambda: other.append(load_bdf_face(path, 7)[0]))
worker.start()
worker.join()
assert other and other[0] is not here
def test_native_size_prefers_pixel_size_over_point_size():
# 6x13.bdf is defined at 75dpi: SIZE says 12 (points), PIXEL_SIZE 13.
assert read_bdf_native_size(str(FONTS_DIR / "6x13.bdf")) == 13