perf: cache text fonts per size and initialize the font module on demand - #21
Conversation
drawText built a new pygame.font.Font on every call. Constructing a Font parses and rasterizes the TrueType file (3.899 ms/call measured locally) while the render() it exists to serve costs 0.005 ms, so nearly all of drawText's cost was rebuilt work paid again every frame, by every label, and by every drawButton. Cache one Font per size on the instance instead. A Font that outlives a font.quit()/init() cycle is freed memory and segfaults on use, so the cache is dropped when the font module is observed down or when the display surface object changes (which is what restarting pygame does). drawText also called pygame.font.Font without ensuring the font module was up, so a consumer who initialized only the display -- all Graphik's own constructor needs -- hit a bare "font not initialized" error from inside the library. The text path now initializes the module itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Self-review Verified the two cache-invalidation guards empirically rather than from pygame's docs, because the failure mode here is a segfault rather than an exception. Confirmed locally on pygame 2.6.1 / Python 3.10:
No behaviour change to Three non-blocking observations:
|
Summary
pygame.font.Fontinstances per size on theGraphikinstance instead of building a new one on everydrawText. Measured locally (pygame 2.6.1, Python 3.10): constructing aFontcosts 3.899 ms, therender()it exists to serve costs 0.005 ms — so ~99.9% ofdrawTextwas rebuilt work, paid again every frame by every label and by everydrawButton.drawTextnow measures 0.035 ms/call over 200 calls. Closes perf: drawText rebuilds a pygame Font on every call #17.Graphik's constructor only sets up a display (it callsset_mode()itself), so a consumer could reasonably reachdrawTextwith the font module still down and get a barepygame.error: font not initializedsurfacing from inside graphik. Closes drawText fails with a bare "font not initialized" error when only the display was initialized #18.drawRectangle/drawImageare untouched.Cache invalidation — the part worth reviewing
A
Fontthat outlives afont.quit()/init()cycle points at freed SDL_ttf memory and segfaults on use (confirmed locally, exit 139), and pygame exposes no way to test aFontfor validity — no generation counter, and nothing inpygame.font's module surface changes across a restart (I diffed every attribute). So the cache is dropped on two signals:pygame.font.get_init()is false) — coverspygame.font.quit()followed by our owninit().pygame.quit()/pygame.init()) drops the display surface, sopygame.display.get_surface()returning a different object (orNone) marks a new session. A plain resize returns the same surface object, so this does not throw the cache away on everyset_mode().Guard 2 is load-bearing: with it defeated, the restart case segfaults; with it in place the instance fails the same loud way it did before this change (
pygame.error: display Surface quit, from blitting to its stale display surface). Both behaviours are covered by tests.Known limitation, documented in the code: a consumer that calls
pygame.font.quit()and thenpygame.font.init()itself, leaving the display alone, is indistinguishable from an untouched module through pygame's public API — aGraphikbuilt before such a restart must be rebuilt after it. This is the same constraint that applies to any pygame code holding aFontacross a font-module restart.Vendored consumers
No breaking change: no public method signature or rendered output changes, so the vendored copies in Roam, Apex, Ophidian, Patchwork and Tic-Tak-Toe can take this as a drop-in. The only new instance state is
_fonts/_fontDisplay, both private and set in__init__.Test plan
pytest— 19 passed (5 new tests)drawTextcalls; one font per distinct sizedrawTextworks after onlypygame.display.init()/set_mode(), with no consumerpygame.font.init()pygame.errorrather than a segfaultpython -m compileall src/main/pythondrawText/drawButtonpixel-output tests unchanged and passingCloses #17
Closes #18
This PR description was drafted during a Gardener session (Stephenson-Software/gardener).