From ee4149dc4962b03260e714c81d4a35724880c7c3 Mon Sep 17 00:00:00 2001 From: Chuck <33324927+ChuckBuilds@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:18:05 -0400 Subject: [PATCH] fix(vegas): refresh scroll buffer on live score updates (#299) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(vegas): refresh scroll buffer when plugins report live data updates should_recompose() only checked for cycle completion or staging buffer content, but plugin updates go to _pending_updates — not the staging buffer. The scroll display kept showing the old pre-rendered image until the full cycle ended, even though fresh scores were already fetched and logged. Add has_pending_updates() check so hot_swap_content() triggers immediately when plugins have new data. Fixes #230 Co-Authored-By: Claude Opus 4.6 (1M context) * fix(vegas): scope hot-swap to visible segments; use monotonic clock 1. Replace has_pending_updates() with has_pending_updates_for_visible_segments() so hot_swap_content() only fires when a pending update affects a plugin that is actually in the active scroll buffer (with images). Avoids unnecessary recomposition when non-visible plugins report updates. 2. Switch all display-loop timing (start_time, elapsed, _next_live_priority_check) from time.time() to time.monotonic() to prevent clock-stepping issues from NTP adjustments on Raspberry Pi. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- src/display_controller.py | 16 ++++++++-------- src/vegas_mode/render_pipeline.py | 4 ++++ src/vegas_mode/stream_manager.py | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/display_controller.py b/src/display_controller.py index 6fed01b6..34635f99 100644 --- a/src/display_controller.py +++ b/src/display_controller.py @@ -79,7 +79,7 @@ class DisplayController: logger.info("Display modes initialized in %.3f seconds", time.time() - init_time) self.force_change = False - self._next_live_priority_check = 0.0 # timestamp for throttled live priority checks + self._next_live_priority_check = 0.0 # monotonic timestamp for throttled live priority checks # All sports and content managers now handled via plugins logger.info("All sports and content managers now handled via plugin system") @@ -1727,7 +1727,7 @@ class DisplayController: ) target_duration = max_duration - start_time = time.time() + start_time = time.monotonic() def _should_exit_dynamic(elapsed_time: float) -> bool: if not dynamic_enabled: @@ -1793,8 +1793,8 @@ class DisplayController: # Check for live priority every ~30s so live # games can interrupt long display durations - elapsed = time.time() - start_time - now = time.time() + elapsed = time.monotonic() - start_time + now = time.monotonic() if not self.on_demand_active and now >= self._next_live_priority_check: self._next_live_priority_check = now + 30.0 live_mode = self._check_live_priority() @@ -1843,7 +1843,7 @@ class DisplayController: time.sleep(display_interval) self._tick_plugin_updates() - elapsed = time.time() - start_time + elapsed = time.monotonic() - start_time if elapsed >= target_duration: logger.debug( "Reached standard target duration %.2fs for mode %s", @@ -1875,7 +1875,7 @@ class DisplayController: # Check for live priority every ~30s so live # games can interrupt long display durations - now = time.time() + now = time.monotonic() if not self.on_demand_active and now >= self._next_live_priority_check: self._next_live_priority_check = now + 30.0 live_mode = self._check_live_priority() @@ -1915,13 +1915,13 @@ class DisplayController: and not loop_completed and not needs_high_fps ): - elapsed = time.time() - start_time + elapsed = time.monotonic() - start_time remaining_sleep = max(0.0, max_duration - elapsed) if remaining_sleep > 0: self._sleep_with_plugin_updates(remaining_sleep) if dynamic_enabled: - elapsed_total = time.time() - start_time + elapsed_total = time.monotonic() - start_time cycle_done = self._plugin_cycle_complete(manager_to_display) # Log cycle completion status and metrics diff --git a/src/vegas_mode/render_pipeline.py b/src/vegas_mode/render_pipeline.py index 2b93a07f..138ee1d2 100644 --- a/src/vegas_mode/render_pipeline.py +++ b/src/vegas_mode/render_pipeline.py @@ -265,6 +265,10 @@ class RenderPipeline: if buffer_status['staging_count'] > 0: return True + # Trigger recompose when pending updates affect visible segments + if self.stream_manager.has_pending_updates_for_visible_segments(): + return True + return False def hot_swap_content(self) -> bool: diff --git a/src/vegas_mode/stream_manager.py b/src/vegas_mode/stream_manager.py index 17efc357..06082951 100644 --- a/src/vegas_mode/stream_manager.py +++ b/src/vegas_mode/stream_manager.py @@ -199,6 +199,21 @@ class StreamManager: logger.debug("Plugin %s marked for update", plugin_id) + def has_pending_updates(self) -> bool: + """Check if any plugins have pending updates awaiting processing.""" + with self._buffer_lock: + return len(self._pending_updates) > 0 + + def has_pending_updates_for_visible_segments(self) -> bool: + """Check if pending updates affect plugins currently in the active buffer.""" + with self._buffer_lock: + if not self._pending_updates: + return False + active_ids = { + seg.plugin_id for seg in self._active_buffer if seg.images + } + return bool(active_ids & self._pending_updates.keys()) + def process_updates(self) -> None: """ Process pending plugin updates.