fix(display): Vegas resumes after live priority, and six smaller runtime fixes (#644)

- Vegas: a live-priority pause was only lifted from inside run_frame(),
  which returns before that check while paused, so the ticker never came
  back until a restart. run_iteration() now resumes it (the controller
  only calls it when nothing preempts Vegas); start()/stop() clear the
  pause state. Iteration length is timed with the monotonic clock.
- Dim schedule: a per-day disabled day now updates the minute-gate cache,
  so brightness no longer flips back to dim within each minute.
- On-demand: a second request no longer overwrites the rotation resume
  index with the first request's mode.
- Render pipeline: reset() drops the prepared group and deferred queue,
  and a prefetch in flight across a reset discards its result.
- Sync: stop() removes the status file (and the controller's cleanup now
  calls it), standalone removes a stale one at startup, and writes use a
  unique mkstemp temp file.
- render_gate.swap_releases_gil() delegates to frame_timing.
- Stale docstrings/comments corrected.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-28 08:25:26 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent bcef1957a9
commit 6bc13a8934
14 changed files with 401 additions and 32 deletions
+29 -7
View File
@@ -273,6 +273,10 @@ class VegasModeCoordinator:
self._is_active = True
self._should_stop = False
# A pause belongs to the run it happened in; carrying it into a
# new run would have run_frame() refuse every frame.
self._is_paused = False
self._live_priority_active = False
self._start_time = time.time()
# A fresh run starts with a clean health slate: no stale
# "was degraded" from the previous run, and a heartbeat that is
@@ -298,6 +302,8 @@ class VegasModeCoordinator:
self._should_stop = True
self._is_active = False
self._is_paused = False
self._live_priority_active = False
if self._start_time:
self.stats['total_runtime_seconds'] += time.time() - self._start_time
@@ -473,6 +479,20 @@ class VegasModeCoordinator:
if not self.start():
return False
# A live-priority pause is only ever lifted by _check_live_priority(),
# and run_frame() returns before reaching it while paused -- so once
# paused, every later iteration returned False at its first frame and
# the ticker never came back until a restart. The display controller
# only calls run_iteration() when nothing preempts Vegas (no live mode,
# or live content is kept in the ticker), so being called at all means
# the live content that paused us has ended.
with self._state_lock:
paused_for_live = self._is_paused and self._live_priority_active
if paused_for_live:
self._live_priority_active = False
self.resume()
logger.info("Live priority ended - resuming Vegas")
if self.vegas_config.continuous_scroll:
# The strip is continuously extended and trimmed, so its width says
# nothing about how long to run. This is only how often control
@@ -481,7 +501,11 @@ class VegasModeCoordinator:
duration = float(self.vegas_config.max_cycle_duration)
else:
duration = self.render_pipeline.get_dynamic_duration()
start_time = time.time()
# Monotonic for the same reason as the per-frame clock below: this
# bounds how long the iteration runs, and an NTP step on an RTC-less
# Pi would otherwise end it at once (forward) or stretch it by the
# size of the correction (backward).
start_time = time.monotonic()
frame_count = 0
fps_log_interval = 5.0 # Sample FPS every 5 seconds
# Health state lives on the coordinator, not here: run_iteration() is
@@ -490,10 +514,8 @@ class VegasModeCoordinator:
# of every iteration rather than once per interval, and a recovery
# that crossed an iteration boundary was never reported at all --
# was_degraded had already gone back to False.
# Monotonic, and deliberately not start_time: start_time is wall
# clock and is used below to report the iteration's duration. Mixing
# the two here would make every delta hugely negative and silence the
# frame-rate reporting altogether.
# Monotonic. Never mix it with a wall-clock value: every delta would
# be hugely negative and silence the frame-rate reporting altogether.
last_fps_log_time = time.monotonic()
fps_frame_count = 0
# A mean hides stutter completely. At 120fps a five-second window is
@@ -633,7 +655,7 @@ class VegasModeCoordinator:
).start()
# Check elapsed time
elapsed = time.time() - start_time
elapsed = time.monotonic() - start_time
if elapsed >= duration:
break
@@ -652,7 +674,7 @@ class VegasModeCoordinator:
# cycle content multiple times within one iteration — acceptable for
# a continuous ticker.
logger.info("Vegas iteration completed after %.1fs", time.time() - start_time)
logger.info("Vegas iteration completed after %.1fs", time.monotonic() - start_time)
return True
def _check_live_priority(self) -> bool:
+20 -1
View File
@@ -52,6 +52,12 @@ class RenderPipeline:
# A panel measured within this fraction of its cap is keeping up with it.
REFRESH_TOLERANCE = 0.03
# Bumped by reset(). A prefetch thread records the value it started under
# and drops its group if a reset happened meanwhile, so a fetch still in
# flight when Vegas stops cannot land in the next run. Class-level so
# pipelines built without __init__ (tests) still have it.
_prefetch_generation = 0
def __init__(
self,
config: VegasModeConfig,
@@ -373,6 +379,7 @@ class RenderPipeline:
return
if self._prepared_group is not None:
return # already have one waiting
generation = self._prefetch_generation
def _work():
# Deprioritise against the render loop. Linux applies nice
@@ -394,6 +401,8 @@ class RenderPipeline:
logger.exception("Background prefetch failed")
group = []
with self._prefetch_lock:
if generation != self._prefetch_generation:
return # Vegas was reset while this was fetching
self._prepared_group = group
self._prefetch_thread = threading.Thread(
@@ -602,7 +611,8 @@ class RenderPipeline:
"""
Render a single frame to the display.
Should be called at ~125 FPS (8ms intervals).
Called once per frame by the coordinator, which paces the calls by
frame_interval (see that property) rather than a fixed rate.
Returns:
True if frame was rendered, False if no content
@@ -917,6 +927,15 @@ class RenderPipeline:
self._segments_in_scroll = []
self._frame_times = deque(maxlen=100)
# Content lined up for the old run belongs to it. Left in place, the
# first extension after Vegas is switched back on appended that stale
# group -- including plugins disabled in the meantime -- and the
# deferred queue went on fetching the old run's plugins.
with self._prefetch_lock:
self._prefetch_generation += 1
self._prepared_group = None
self._deferred_queue = []
self.display_manager.set_scrolling_state(False)
logger.info("RenderPipeline reset")