feat(pygame): add glyph caching for performance improvement

- Add _glyph_cache dict to PygameDisplay.__init__
- Cache font.render() results per (char, fg, bg) combination
- Use blits() for batch rendering instead of individual blit calls
- Add TestRenderBorder tests (8 new tests) for border rendering
- Update NullDisplay.show() to support border=True for consistency
- Add test_show_with_border_uses_render_border for TerminalDisplay

Closes #28
This commit is contained in:
2026-03-18 04:23:58 -07:00
parent 4b26c947e8
commit 60ae4f7dfb
3 changed files with 159 additions and 12 deletions

View File

@@ -33,10 +33,25 @@ class NullDisplay:
self._last_buffer = None
def show(self, buffer: list[str], border: bool = False) -> None:
from engine.display import get_monitor
from engine.display import get_monitor, render_border
# Get FPS for border (if available)
fps = 0.0
frame_time = 0.0
monitor = get_monitor()
if monitor:
stats = monitor.get_stats()
avg_ms = stats.get("pipeline", {}).get("avg_ms", 0) if stats else 0
frame_count = stats.get("frame_count", 0) if stats else 0
if avg_ms and frame_count > 0:
fps = 1000.0 / avg_ms
frame_time = avg_ms
# Apply border if requested (same as terminal display)
if border:
buffer = render_border(buffer, self.width, self.height, fps, frame_time)
self._last_buffer = buffer
monitor = get_monitor()
if monitor:
t0 = time.perf_counter()
chars_in = sum(len(line) for line in buffer)

View File

@@ -41,6 +41,7 @@ class PygameDisplay:
self._quit_requested = False
self._last_frame_time = 0.0
self._frame_period = 1.0 / target_fps if target_fps > 0 else 0
self._glyph_cache = {}
def _get_font_path(self) -> str | None:
"""Get font path for rendering."""
@@ -191,6 +192,8 @@ class PygameDisplay:
self._screen.fill((0, 0, 0))
blit_list = []
for row_idx, line in enumerate(buffer[: self.height]):
if row_idx >= self.height:
break
@@ -202,15 +205,24 @@ class PygameDisplay:
if not text:
continue
if bg != (0, 0, 0):
bg_surface = self._font.render(text, True, fg, bg)
self._screen.blit(bg_surface, (x_pos, row_idx * self.cell_height))
else:
text_surface = self._font.render(text, True, fg)
self._screen.blit(text_surface, (x_pos, row_idx * self.cell_height))
# Use None as key for no background
bg_key = bg if bg != (0, 0, 0) else None
cache_key = (text, fg, bg_key)
if cache_key not in self._glyph_cache:
# Render and cache
if bg_key is not None:
self._glyph_cache[cache_key] = self._font.render(
text, True, fg, bg_key
)
else:
self._glyph_cache[cache_key] = self._font.render(text, True, fg)
surface = self._glyph_cache[cache_key]
blit_list.append((surface, (x_pos, row_idx * self.cell_height)))
x_pos += self._font.size(text)[0]
self._screen.blits(blit_list)
self._pygame.display.flip()
elapsed_ms = (time.perf_counter() - t0) * 1000