diff --git a/src/vegas_mode/config.py b/src/vegas_mode/config.py index 5f0a28fa..9c598a13 100644 --- a/src/vegas_mode/config.py +++ b/src/vegas_mode/config.py @@ -82,6 +82,14 @@ class VegasModeConfig: # drawn off the render thread. See docs/OFFSCREEN_RENDERING.md. offscreen_prefetch: bool = True + # How long another thread may hold the GIL before the render thread's + # request forces it to yield, in ms, while Vegas runs. CPython's default is + # 5ms. Plugin rendering on the prefetch thread and plugin updates hold the + # GIL in Pillow and Python code, and a frame waiting its turn for 5ms at a + # time misses its refresh. 0 leaves the interpreter default alone. + # Experimental: measured with scripts/frame_soak.py before it gets a default. + switch_interval_ms: float = 0.0 + # Keep one continuous strip, extending it with the next group of plugins as # the scroll approaches the end, instead of composing a fresh strip and # swapping it in. A swap stops the motion, substitutes every pixel at once @@ -212,6 +220,7 @@ class VegasModeConfig: sub_pixel_blend=bool(vegas_config.get('sub_pixel_blend', False)), continuous_scroll=vegas_config.get('continuous_scroll', True), offscreen_prefetch=bool(vegas_config.get('offscreen_prefetch', True)), + switch_interval_ms=float(vegas_config.get('switch_interval_ms', 0.0) or 0.0), extend_threshold_screens=float( vegas_config.get('extend_threshold_screens', 2.0)), auto_trim=vegas_config.get('auto_trim', True), @@ -254,6 +263,7 @@ class VegasModeConfig: 'sub_pixel_blend': self.sub_pixel_blend, 'continuous_scroll': self.continuous_scroll, 'offscreen_prefetch': self.offscreen_prefetch, + 'switch_interval_ms': self.switch_interval_ms, 'extend_threshold_screens': self.extend_threshold_screens, 'auto_trim': self.auto_trim, 'trim_threshold': self.trim_threshold, diff --git a/src/vegas_mode/coordinator.py b/src/vegas_mode/coordinator.py index 5551aea4..8b63b50a 100644 --- a/src/vegas_mode/coordinator.py +++ b/src/vegas_mode/coordinator.py @@ -13,6 +13,7 @@ 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 @@ -280,6 +281,7 @@ 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() # Line up the next group immediately, so the first extension is already # warm rather than stalling the scroll to fetch it. @@ -302,6 +304,8 @@ class VegasModeCoordinator: self.stats['total_runtime_seconds'] += time.time() - self._start_time self._start_time = None + self._restore_switch_interval() + # Cleanup components self.render_pipeline.reset() self.stream_manager.reset() @@ -309,6 +313,23 @@ 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 pause(self) -> None: """Pause Vegas mode (for live priority interruption).""" with self._state_lock: diff --git a/test/test_vegas_switch_interval.py b/test/test_vegas_switch_interval.py new file mode 100644 index 00000000..4195386d --- /dev/null +++ b/test/test_vegas_switch_interval.py @@ -0,0 +1,41 @@ +"""vegas_scroll.switch_interval_ms: applied for a Vegas run, restored after.""" +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from src.vegas_mode.config import VegasModeConfig # noqa: E402 +from src.vegas_mode.coordinator import VegasModeCoordinator # noqa: E402 + + +def _coordinator(ms): + c = VegasModeCoordinator.__new__(VegasModeCoordinator) + c.vegas_config = VegasModeConfig(switch_interval_ms=ms) + return c + + +def test_applied_for_the_run_and_restored_after(): + before = sys.getswitchinterval() + c = _coordinator(1.0) + try: + c._apply_switch_interval() + assert abs(sys.getswitchinterval() - 0.001) < 1e-9 + c._apply_switch_interval() # a second start must not lose the original + finally: + c._restore_switch_interval() + assert sys.getswitchinterval() == before + + +def test_zero_leaves_the_interpreter_alone(): + before = sys.getswitchinterval() + c = _coordinator(0.0) + c._apply_switch_interval() + c._restore_switch_interval() + assert sys.getswitchinterval() == before + + +def test_read_from_config(): + config = VegasModeConfig.from_config( + {"display": {"vegas_scroll": {"switch_interval_ms": 1}}}) + assert config.switch_interval_ms == 1.0 + assert VegasModeConfig.from_config({"display": {"vegas_scroll": {}}}).switch_interval_ms == 0.0