fix(vegas): refresh scroll buffer on live score updates (#299)

* 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) <noreply@anthropic.com>

* 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) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-03-28 13:18:05 -04:00
committed by GitHub
parent 5ddf8b1aea
commit ee4149dc49
3 changed files with 27 additions and 8 deletions

View File

@@ -79,7 +79,7 @@ class DisplayController:
logger.info("Display modes initialized in %.3f seconds", time.time() - init_time) logger.info("Display modes initialized in %.3f seconds", time.time() - init_time)
self.force_change = False 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 # All sports and content managers now handled via plugins
logger.info("All sports and content managers now handled via plugin system") logger.info("All sports and content managers now handled via plugin system")
@@ -1727,7 +1727,7 @@ class DisplayController:
) )
target_duration = max_duration target_duration = max_duration
start_time = time.time() start_time = time.monotonic()
def _should_exit_dynamic(elapsed_time: float) -> bool: def _should_exit_dynamic(elapsed_time: float) -> bool:
if not dynamic_enabled: if not dynamic_enabled:
@@ -1793,8 +1793,8 @@ class DisplayController:
# Check for live priority every ~30s so live # Check for live priority every ~30s so live
# games can interrupt long display durations # games can interrupt long display durations
elapsed = time.time() - start_time elapsed = time.monotonic() - start_time
now = time.time() now = time.monotonic()
if not self.on_demand_active and now >= self._next_live_priority_check: if not self.on_demand_active and now >= self._next_live_priority_check:
self._next_live_priority_check = now + 30.0 self._next_live_priority_check = now + 30.0
live_mode = self._check_live_priority() live_mode = self._check_live_priority()
@@ -1843,7 +1843,7 @@ class DisplayController:
time.sleep(display_interval) time.sleep(display_interval)
self._tick_plugin_updates() self._tick_plugin_updates()
elapsed = time.time() - start_time elapsed = time.monotonic() - start_time
if elapsed >= target_duration: if elapsed >= target_duration:
logger.debug( logger.debug(
"Reached standard target duration %.2fs for mode %s", "Reached standard target duration %.2fs for mode %s",
@@ -1875,7 +1875,7 @@ class DisplayController:
# Check for live priority every ~30s so live # Check for live priority every ~30s so live
# games can interrupt long display durations # 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: if not self.on_demand_active and now >= self._next_live_priority_check:
self._next_live_priority_check = now + 30.0 self._next_live_priority_check = now + 30.0
live_mode = self._check_live_priority() live_mode = self._check_live_priority()
@@ -1915,13 +1915,13 @@ class DisplayController:
and not loop_completed and not loop_completed
and not needs_high_fps and not needs_high_fps
): ):
elapsed = time.time() - start_time elapsed = time.monotonic() - start_time
remaining_sleep = max(0.0, max_duration - elapsed) remaining_sleep = max(0.0, max_duration - elapsed)
if remaining_sleep > 0: if remaining_sleep > 0:
self._sleep_with_plugin_updates(remaining_sleep) self._sleep_with_plugin_updates(remaining_sleep)
if dynamic_enabled: if dynamic_enabled:
elapsed_total = time.time() - start_time elapsed_total = time.monotonic() - start_time
cycle_done = self._plugin_cycle_complete(manager_to_display) cycle_done = self._plugin_cycle_complete(manager_to_display)
# Log cycle completion status and metrics # Log cycle completion status and metrics

View File

@@ -265,6 +265,10 @@ class RenderPipeline:
if buffer_status['staging_count'] > 0: if buffer_status['staging_count'] > 0:
return True return True
# Trigger recompose when pending updates affect visible segments
if self.stream_manager.has_pending_updates_for_visible_segments():
return True
return False return False
def hot_swap_content(self) -> bool: def hot_swap_content(self) -> bool:

View File

@@ -199,6 +199,21 @@ class StreamManager:
logger.debug("Plugin %s marked for update", plugin_id) 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: def process_updates(self) -> None:
""" """
Process pending plugin updates. Process pending plugin updates.