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
+64 -1
View File
@@ -13,10 +13,12 @@ Supports three display modes per plugin:
import logging
import math
import sys
import time
import threading
from typing import Optional, Dict, Any, List, Callable, TYPE_CHECKING
from src.common import render_gate
from src.vegas_mode.config import VegasModeConfig
from src.vegas_mode.plugin_adapter import PluginAdapter
from src.vegas_mode.stream_manager import StreamManager
@@ -101,7 +103,8 @@ class VegasModeCoordinator:
self.plugin_manager = plugin_manager
# Initialize components
self.plugin_adapter = PluginAdapter(display_manager, self.vegas_config)
self.plugin_adapter = PluginAdapter(
display_manager, self.vegas_config, plugin_manager=plugin_manager)
self.stream_manager = StreamManager(
self.vegas_config,
plugin_manager,
@@ -276,6 +279,8 @@ class VegasModeCoordinator:
# due immediately so the first sample confirms the marquee is up.
self._fps_last_health_log = 0.0
self._fps_was_degraded = False
self._apply_switch_interval()
self._install_render_gate()
# Line up the next group immediately, so the first extension is already
# warm rather than stalling the scroll to fetch it.
@@ -298,6 +303,9 @@ class VegasModeCoordinator:
self.stats['total_runtime_seconds'] += time.time() - self._start_time
self._start_time = None
self._restore_switch_interval()
self._remove_render_gate()
# Cleanup components
self.render_pipeline.reset()
self.stream_manager.reset()
@@ -305,6 +313,57 @@ class VegasModeCoordinator:
logger.info("Vegas mode stopped")
def _apply_switch_interval(self) -> None:
"""Shorten the GIL switch interval for the run; see VegasModeConfig."""
ms = self.vegas_config.switch_interval_ms
if not ms or ms <= 0:
return
if getattr(self, '_saved_switch_interval', None) is None:
self._saved_switch_interval = sys.getswitchinterval()
sys.setswitchinterval(ms / 1000.0)
logger.info("Vegas: GIL switch interval %.1fms (was %.1fms)",
ms, self._saved_switch_interval * 1000.0)
def _restore_switch_interval(self) -> None:
saved = getattr(self, '_saved_switch_interval', None)
if saved is not None:
sys.setswitchinterval(saved)
self._saved_switch_interval = None
def _install_render_gate(self) -> None:
"""Gate the prefetch thread on the render thread's swaps; see VegasModeConfig."""
if not self.vegas_config.prefetch_gate:
return
if getattr(self.display_manager, 'render_gate', None) is not None:
return
releases = render_gate.swap_releases_gil()
if releases is None:
logger.debug("Vegas: no prefetch gate -- no hardware binding loaded")
return
if not releases:
# On by default, so this is every stock install: say so once per
# run, not as a warning.
logger.info("Vegas: no prefetch gate -- this rgbmatrix binding keeps "
"the GIL in SwapOnVSync (scripts/build_rgbmatrix_nogil.sh)")
return
gate = render_gate.RenderGate()
# Locks the render thread takes too: never park the prefetch holding one.
gate.guard(self._state_lock,
getattr(self.stream_manager, '_buffer_lock', None),
getattr(self.render_pipeline, '_buffer_lock', None),
getattr(self.render_pipeline, '_prefetch_lock', None),
getattr(self.plugin_adapter, '_cache_lock', None))
self.display_manager.render_gate = gate
logger.info("Vegas: prefetch gated on vsync")
def _remove_render_gate(self) -> None:
gate = getattr(self.display_manager, 'render_gate', None)
if gate is None:
return
self.display_manager.render_gate = None
logger.info("Vegas: prefetch gate parked the prefetch %d times, %.1fs in all",
gate.parks, gate.parked_seconds)
def pause(self) -> None:
"""Pause Vegas mode (for live priority interruption)."""
with self._state_lock:
@@ -524,6 +583,10 @@ class VegasModeCoordinator:
fps, target, fps_frame_count,
p99 * 1000.0, frame_worst * 1000.0
)
gate = getattr(self.display_manager, 'render_gate', None)
if gate is not None:
logger.info("Vegas: prefetch parked %d times, %.1fs in all",
gate.parks, gate.parked_seconds)
self._fps_last_health_log = current_time
else:
logger.debug(