feat(vegas): render plugin content off the render thread, and keep it off the GIL when the panel needs it (#630)

DisplayManager.offscreen() gives a thread its own canvas, so Vegas renders every plugin's ticker content on its prefetch thread instead of pausing the scroll for canvas-bound plugins on the render thread. A render gate (src/common/render_gate.py, vegas_scroll.prefetch_gate, on by default with the GIL-releasing binding) lets the prefetch thread run Python only while the render thread waits in SwapOnVSync: on hdpi, frames 2+ refreshes late fell eightfold and late frames overall from 0.90% to 0.60%. See docs/OFFSCREEN_RENDERING.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-24 19:57:03 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 865d62f67b
commit 9964dd2183
13 changed files with 1903 additions and 89 deletions
+12 -4
View File
@@ -10,6 +10,7 @@ import os
import time
import threading
from collections import deque
from contextlib import nullcontext
from typing import Optional, List, Any, Dict, Deque
from PIL import Image
@@ -383,8 +384,12 @@ class RenderPipeline:
os.nice(10)
except (OSError, AttributeError):
pass
# With vegas_scroll.prefetch_gate on, run only while the render
# thread waits on vsync; see src/common/render_gate.py.
gate = getattr(self.display_manager, 'render_gate', None)
try:
group = self.stream_manager.take_next_group(offscreen_only=True)
with gate.yielding() if gate is not None else nullcontext():
group = self.stream_manager.take_next_group(offscreen_only=True)
except Exception:
logger.exception("Background prefetch failed")
group = []
@@ -509,9 +514,12 @@ class RenderPipeline:
grouped = [(pid, imgs) for pid, imgs in grouped if imgs]
if not grouped:
# Everything in this group is queued; the queue will extend the
# strip as it drains, so this is not a failure.
logger.info("Whole group deferred; strip will extend as it drains")
if deferred:
# Everything in this group is queued; the queue will extend
# the strip as it drains, so this is not a failure.
logger.info("Whole group deferred; strip will extend as it drains")
else:
logger.info("Nothing to show in this group; fetching the next")
self.start_prefetch()
return bool(deferred)