Repository navigation
Let draw_text reuse only labels not drawn this frame - #2925
Merged
Merged
Conversation
draw_text kept at most 8 labels per style, so a changing line would reuse its own old label instead of making a new one every frame. With 9 or more unchanged lines in one style, though, the lines took each other's labels and every line was laid out again every frame: 20 lines cost 5.6 ms per frame instead of 0.6 ms. Instead, each label records the GLOBAL_CLOCK tick it was last drawn in. On a cache miss, the style's least recently used label is reused only if it wasn't drawn this frame. Lines are drawn in the same order each frame, so a changing line reuses its own previous label, and unchanged lines keep theirs however many there are. The 256 label limit stays. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a
draw_textslowdown I introduced in #2919, which I found while measuring for the text docs (#2924).The problem
#2919 kept at most 8 cached labels per style. That way, a line whose text changes every frame (a timer) reuses its own old label, which costs about 300 µs to re-lay out, instead of creating a new
Textevery frame, which costs about 600 µs. But with 9 or more unchanged lines in the same style, there was never room for all of them. Line 9 took line 1's label, line 1 then took line 2's, and so on. Every line was laid out again every frame, going from about 30 µs to about 300 µs per line.The fix
The cap is gone. Instead, each label records the
GLOBAL_CLOCKtick it was last drawn in. The window's event loop ticks that clock once per frame, before drawing, and the Tetris example already usesGLOBAL_CLOCK.ticksas a frame number. On a cache miss, the style's least recently used label is reused only if it wasn't drawn this frame:The limit of 256 labels in total stays.
Without the event loop, for example in a custom loop or tests, the clock doesn't advance. Every label then looks in use, so changing text creates new labels until the 256 limit forgets the oldest. That's slower for changing text, but it never thrashes.
I'd planned to group labels by style so a miss wouldn't scan the cache. But scanning all 256 entries costs 2.7 µs at most, and only on a miss, which then spends 300–600 µs on layout. An index would also need to stay in sync with
ctx.label_cache, which code and tests clear directly. So I left it out.Measurements
µs per frame, with the clock ticking each frame as in the event loop, alternating old and new:
The changing-line case also uses fewer labels now. Before, the timer kept creating labels until it reached the cap.
Tests
In
tests/unit/text/test_draw_text_cache.py:test_same_style_lines_keep_their_labelsticks the clock each frame, like the event loop. It checks that the timer line reuses its own label and that the cache holds exactly 5 labels.test_many_same_style_lines_keep_their_labelswith 9 and 30 lines, which keep their labels across frames.test_line_that_stops_frees_its_label(a new line takes the label of one that stopped being drawn) andtest_new_line_in_same_frame_gets_new_label.test_changing_text_without_frames_is_limited, the no-event-loop fallback.On
development, 4 fail. The pixel test from #2919 still passes.Full suite on pyglet 3.0.dev11: 1571 passed, no warnings, plus the same 3 render tests that fail only on my machine. Ruff is clean, and mypy has no new errors.
Changelog: the cap came in with #2919, which isn't released yet, so I updated its Unreleased entry instead of adding a "fixed" entry for a bug that never shipped.
🤖 Generated with Claude Code