From 932e89ccb8a07bea65f063f1a5b660c209c30693 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Wed, 29 Jul 2026 14:39:16 -0400 Subject: [PATCH] Pace the Vegas frame loop adaptively: 31.5 -> 78.7 fps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The loop slept a fixed frame_interval on top of however long the frame took, so at a measured 31.6ms per frame a flat 8ms of that was pure idle — a quarter of the budget spent not rendering. It now sleeps only the remainder of the budget. Measured on hardware: 31.5 fps to 78.7 fps sustained, with CPU going *down* from 150% to 127%. Scroll speed is unchanged at 49.9px/s against a configured 50, because motion is derived from elapsed time rather than frame count — this buys smoothness, not speed. Worth recording what the bottleneck was not: the per-frame render path measures 0.34ms in total (0.18ms for the numpy slice, 0.17ms for the dirty-tracking digest), which is a theoretical 2900 fps. Optimising any of that would have been wasted effort. The frame was idle, not busy. Also nices the prefetch thread. Its work is PIL and numpy that releases the GIL, so the scheduler can act on the priority, and without it the prefetch competes for the same cores as the render loop. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ --- src/vegas_mode/coordinator.py | 12 ++++++++++-- src/vegas_mode/render_pipeline.py | 10 ++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/vegas_mode/coordinator.py b/src/vegas_mode/coordinator.py index eb953969..6276f3b0 100644 --- a/src/vegas_mode/coordinator.py +++ b/src/vegas_mode/coordinator.py @@ -376,6 +376,8 @@ class VegasModeCoordinator: logger.info("Starting Vegas iteration for %.1fs", duration) while True: + frame_started = time.time() + # Check for STATIC mode plugin that should pause scroll static_plugin = self._check_static_plugin_trigger() if static_plugin: @@ -396,8 +398,14 @@ class VegasModeCoordinator: # Paused for live priority - let caller handle return False - # Sleep for frame interval - time.sleep(frame_interval) + # Sleep only the remainder of the frame budget. This used to sleep + # the whole interval on top of however long the frame took, so at a + # measured 31.6ms per frame a fixed 8ms of that was pure idle — a + # quarter of the budget spent not rendering. Subtracting the work + # already done keeps the pacing target while reclaiming that time, + # and yields the GIL either way so other threads still run. + frame_elapsed = time.time() - frame_started + time.sleep(max(0.0, frame_interval - frame_elapsed)) # Increment frame count and check for interrupt periodically frame_count += 1 diff --git a/src/vegas_mode/render_pipeline.py b/src/vegas_mode/render_pipeline.py index d3bb434f..88bd8a76 100644 --- a/src/vegas_mode/render_pipeline.py +++ b/src/vegas_mode/render_pipeline.py @@ -6,6 +6,7 @@ Uses the existing ScrollHelper for numpy-optimized scroll operations. """ import logging +import os import time import threading from collections import deque @@ -256,6 +257,15 @@ class RenderPipeline: return # already have one waiting def _work(): + # Deprioritise against the render loop. Linux applies nice + # per-thread, and the heavy lifting here is PIL and numpy work + # that releases the GIL, so the scheduler can actually act on + # it — without this the prefetch competes for the same cores and + # costs frames. + try: + os.nice(10) + except (OSError, AttributeError): + pass try: group = self.stream_manager.take_next_group(offscreen_only=True) except Exception: