fix(vegas): pause for STATIC plugins where their turn falls in the strip (#651)

The static trigger peeked at the front of StreamManager's segment buffer,
which continuous scrolling (the default) never advances -- it extends the
strip with take_next_group() -- so the same first segment was examined on
every frame. A STATIC plugin paused the scroll only if it was first, once,
at startup; otherwise it scrolled past as ordinary content. Swap mode had
the same problem for any STATIC plugin not first in its cycle.

The render pipeline now records a marker (strip column, plugin id) for
each STATIC plugin where the strip is built -- composition and every
extension -- shifts the markers when the scrolled prefix is trimmed, and
clears them on reset. The coordinator pauses when the scroll reaches the
next marker: a tuple comparison per frame instead of a lock, a plugin
lookup and a get_vegas_display_mode() call. take_next_group() no longer
renders STATIC plugins' content. The pause calls display() under the
plugin lock and is timed with the monotonic clock.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-28 10:39:14 -04:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 224847cebc
commit 1e62677257
5 changed files with 438 additions and 29 deletions
+39
View File
@@ -667,6 +667,37 @@ class StreamManager:
grouped.append((segment.plugin_id, list(segment.images)))
return grouped
def get_static_layout(self) -> List[Tuple[str, bool]]:
"""
The buffer's composition order, with STATIC segments kept in place.
get_grouped_content_for_composition() drops STATIC segments because
they contribute no columns; this says where they sat. Each entry is
(plugin_id, is_static), and only segments that composition keeps or
that are STATIC are listed, so the non-static entries line up one to
one with get_grouped_content_for_composition()'s groups.
"""
layout: List[Tuple[str, bool]] = []
with self._buffer_lock:
for segment in self._active_buffer:
if segment.display_mode == VegasDisplayMode.STATIC:
layout.append((segment.plugin_id, True))
elif segment.images:
layout.append((segment.plugin_id, False))
return layout
def is_static_plugin(self, plugin_id: str) -> bool:
"""Whether a loaded plugin asks Vegas to pause for it (STATIC mode)."""
plugin = getattr(self.plugin_manager, 'plugins', {}).get(plugin_id)
if plugin is None:
return False
try:
return plugin.get_vegas_display_mode() == VegasDisplayMode.STATIC
except Exception:
logger.debug("[%s] get_vegas_display_mode() failed; treating as not STATIC",
plugin_id, exc_info=True)
return False
def take_next_group(
self, count: Optional[int] = None, offscreen_only: bool = False
) -> List[Tuple[str, Optional[List[Image.Image]]]]:
@@ -692,6 +723,8 @@ class StreamManager:
content path now draws on a canvas of its own, so a background
fetch that comes back empty had nothing to show, and ``images``
is an empty list rather than a request for the render thread.
A STATIC plugin is also returned with an empty list, unfetched: it
pauses the scroll instead of adding to it (see is_static_plugin).
"""
if count is None:
count = self.config.plugins_per_cycle
@@ -717,6 +750,12 @@ class StreamManager:
plugin = plugins.get(plugin_id)
if not plugin:
continue
if self.is_static_plugin(plugin_id):
# A STATIC plugin pauses the scroll rather than scrolling by,
# so it contributes no columns. It keeps its place in the
# group (empty) so the pipeline can mark where its turn falls.
group.append((plugin_id, []))
continue
try:
images = self.plugin_adapter.get_content(
plugin, plugin_id, offscreen_only=offscreen_only)