mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
a plugin API for content that changes while it scrolls (#696)
* perf(timing): say which render-thread work a late frame followed The soak already says how often a moving frame reached the panel late, but not what the render thread was doing just before it. Vegas does two kinds of work there between frames -- building its strip (compose, extend) and, with live elements, patching changed pixels into it -- and deciding whether either is affordable needs their own numbers. - FrameTimingRecorder.note_op(kind, nbytes) tags the next presented frame. Totals gain op_frames, late_op_frames, op_freezes and op_bytes per kind; aggregate() still takes frames without ops. The file schema is unchanged. - Vegas tags compose and every strip extension (with the bytes it copied). - frame_soak prints an "after work" table: frames, late %, freezes and MB moved per kind, only when something tagged its work. - render_bench gains --strip-screens (Vegas-sized strips), --patch-bytes / --patch-every / --patch-where (in-place column writes, as a live element update does) and --extend-every-screens / --extend-width (append + trim on a fixed cadence that holds the strip's width). No runtime behaviour changes: this is the measurement gate for live Vegas elements. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): note the frame-op attribution and bench modes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(scroll): build the strip's PIL image only when something reads it Every Vegas strip extension rebuilt ScrollHelper.cached_image from cached_array in full, twice (append, then trim), on the render thread: Image.fromarray is 1.7ms for an 8,000px strip and 3.8ms for 20,000px on a Pi 4 (measured on ledpi), about two thirds of an extension's render-thread cost. Nothing on the frame path reads the image's pixels; every frame is cut from the array. cached_image is now a property. append_content and drop_scrolled_prefix defer it; the first read builds it from the array it started with and keeps it only if the strip has not changed meanwhile, so a sync push racing an extension cannot leave a stale image cached. Assigning cached_image stores exactly what was assigned, as before. has_strip() says whether there is a strip without building its image; the helper's frame path, Vegas and the adapter's scroll-cache invalidation use it. The strip is also no longer held in memory twice. In Vegas the image is now built only by a multi-display sync push. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(vegas): live elements -- a plugin API for content that changes while it scrolls Vegas bakes each plugin's pictures into one strip, so a card already on its way across the panel keeps what it showed when it was drawn. This adds the API and bookkeeping for content that can be updated in place; the worker that redraws and swaps it follows separately. No shipped plugin implements the hook yet, so nothing changes for users. Plugin API (core 3.8.0), all no-ops by default: - BasePlugin.get_vegas_elements() -> [VegasElement(key, image, version, live, refresh_hz)]: named, fixed-width pieces of Vegas content. - BasePlugin.redraw_vegas_element(key, width, height, at): a lock-free redraw for content that changes with time. - BasePlugin.notify_vegas_data_changed(): data that lands outside update(). - src/plugin_system/vegas_elements.py (VegasElement, re-exported from base_plugin). Core: - PluginAdapter asks a plugin that implements the hook for elements on the background fetch only (under its lock, on its own canvas); every other path keeps get_vegas_content(). Live elements are pinned (padded with content_padding, never trimmed), tagged with their key, digest and data epoch in Image.info so the existing cache and group plumbing carry them unchanged, and untagged if a width budget crops them. - RenderPipeline records where each live element lands (ElementRecord), in absolute strip columns a trim does not move; the block-start arithmetic is shared with the STATIC markers. - PluginManager update listeners (add/remove_update_listener, notify_data_changed): told the moment update() completes, not at the next ~4s Vegas poll. The coordinator uses one to move each plugin's data epoch on. - vegas_scroll.live_refresh (kill switch), live_max_hz, live_min_interval, live_lead_screens; per-plugin core-owned vegas_live. Live elements are off under multi-display sync, in swap mode and with offscreen_prefetch off. - scripts/check_plugin.py checks the element contract (src/plugin_system/testing/vegas.py); test/fixtures/plugins/vegas-live-stub is a working example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,10 @@ Read by `src/vegas_mode/config.py` (`VegasScrollConfig.from_config`). See
|
||||
| `offscreen_prefetch` | bool, `true` — render every plugin's ticker content on the background thread, each on its own canvas. `false` restores handing canvas-bound plugins to the render thread, one pause at a time. Temporary; see [OFFSCREEN_RENDERING.md](OFFSCREEN_RENDERING.md) |
|
||||
| `prefetch_gate` | bool, `true` — let that background thread run Python only while the render thread is waiting for the panel, so the render thread never waits for the GIL when a refresh comes round. Only takes effect with the rebuilt rgbmatrix binding (`scripts/build_rgbmatrix_nogil.sh`). See [OFFSCREEN_RENDERING.md](OFFSCREEN_RENDERING.md) |
|
||||
| `switch_interval_ms` | float, `0` — experimental: shorten Python's GIL switch interval to this many ms while Vegas runs. `0` leaves the default (5 ms) alone |
|
||||
| `live_refresh` | bool, `true` — live elements: a plugin that supports them (scores, the flight map) has what is already scrolling updated when its data changes, instead of freezing each card as it was drawn. Always off under multi-display sync, in swap mode and with `offscreen_prefetch` off. `false` restores the frozen behaviour exactly. Per plugin: `vegas_live` in the plugin's section |
|
||||
| `live_max_hz` | float, `5` (0–10) — ceiling on how often an animated live element (a moving aircraft) is redrawn; `0` keeps data updates and turns animation off. Capped at 1 Hz without the rebuilt rgbmatrix binding |
|
||||
| `live_min_interval` | float, `2` (0.5–60) — shortest time between two data redraws of one plugin; a faster plugin is redrawn at this rate, never skipped |
|
||||
| `live_lead_screens` | float, `1` (0–5) — how far ahead of the screen, in screen widths, an animated element starts being redrawn |
|
||||
| `smooth_scroll` | bool, `true` — move a whole number of pixels per panel refresh, locked to vsync. `scroll_speed` is snapped to the nearest speed the panel can show that way (at 95Hz: 95, 47.5, 31.7 px/s…), measured against the panel's real refresh rate once scrolling starts |
|
||||
| `sub_pixel_blend` | bool, `false` — the older smoothing: advance by elapsed time and blend neighbouring pixel columns. Looks anti-aliased in the web preview but shimmers on the panel and is not locked to the refresh. Overrides `smooth_scroll` when on |
|
||||
| `extend_threshold_screens` | float, `2.0` |
|
||||
|
||||
@@ -387,6 +387,79 @@ The width Vegas wants this plugin's content to occupy, from the plugin's
|
||||
`display_manager` while it asks for content, so a plugin that sizes itself
|
||||
from `display_manager.width` does not need to read this.
|
||||
|
||||
#### Live Vegas elements
|
||||
|
||||
*New in core 3.8.0.* Content from `get_vegas_content()` is baked into the
|
||||
ticker's strip when the plugin's turn is prefetched, so a score drawn then
|
||||
scrolls past with that score however many goals are scored while it crosses
|
||||
the panel. A plugin that returns **live elements** instead gets them updated
|
||||
in place: after its `update()` the ticker asks again, compares each element
|
||||
with what the strip holds, and swaps the changed ones in between two frames
|
||||
-- on screen included -- without anything next to them moving.
|
||||
|
||||
```python
|
||||
try:
|
||||
from src.plugin_system.vegas_elements import VegasElement
|
||||
except ImportError: # core older than 3.8.0: the hook is never called
|
||||
VegasElement = None
|
||||
|
||||
class MyScoreboard(BasePlugin):
|
||||
def get_vegas_elements(self):
|
||||
if VegasElement is None:
|
||||
return None
|
||||
return [VegasElement(key=f"game:{g['id']}",
|
||||
image=self._card(g), # cache by fingerprint
|
||||
version=self._fingerprint(g)) # changes iff pixels would
|
||||
for g in self.games]
|
||||
```
|
||||
|
||||
`VegasElement(key, image, version=None, live=True, refresh_hz=0.0)`:
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| `key` | Names the element across redraws; unique in the list, stable for the same logical item (`"game:nfl:401547417"`, `"map"`). |
|
||||
| `image` | The element now, at the display's height. A live element's **width must not depend on its data**: a redraw at another width is never swapped in (it appears the next time the plugin comes round), because nothing on screen may move. |
|
||||
| `version` | Anything hashable that changes exactly when the pixels would; lets the ticker skip unchanged elements. `None` means "compare pixels". |
|
||||
| `live` | `False` places it as plain content (trimmed, never refreshed): separators, decoration. |
|
||||
| `refresh_hz` | For content that changes with **time** rather than data (an aircraft moving between position reports): the ticker calls `redraw_vegas_element()` about this often while the element is on or near the screen, capped by `vegas_scroll.live_max_hz` and at 1 Hz without the rebuilt rgbmatrix binding. |
|
||||
|
||||
**`get_vegas_elements() -> Optional[List[VegasElement]]`** — called on the
|
||||
ticker's background thread under the plugin's lock (never while `update()`
|
||||
runs), on a canvas of its own and told its render width, exactly like
|
||||
`get_vegas_content()`. It is called after every `update()` while any of the
|
||||
plugin's elements is on or ahead of the screen, so it must be cheap when
|
||||
nothing changed (cache images by version), idempotent, and must not fetch.
|
||||
Return `None` to use `get_vegas_content()`, which a plugin must keep working
|
||||
for older cores and for the paths that do not ask for elements (the ticker's
|
||||
first strip, multi-display sync, the `live_refresh` switch).
|
||||
|
||||
**`redraw_vegas_element(key, width, height, at) -> Optional[PIL.Image]`** —
|
||||
only for elements with `refresh_hz`. Called **without** the plugin's lock,
|
||||
possibly while `update()` runs, so read only state `update()` replaces in one
|
||||
assignment (an immutable snapshot), never state it mutates in place. `at` is
|
||||
the `time.monotonic()` the pixels are expected on the panel: draw the element
|
||||
as it should look then. Return exactly `width` x `height`, or `None` to skip
|
||||
the tick.
|
||||
|
||||
**`notify_vegas_data_changed()`** — data that arrives outside `update()` (a
|
||||
background thread, a push callback) calls this so the ticker redraws without
|
||||
waiting for the next `update()`. Safe from any thread.
|
||||
|
||||
Live elements are never trimmed to their ink: the ticker pads each with
|
||||
`content_padding` black columns either side, the margin trimming would have
|
||||
left. A single element wider than the plugin's width budget
|
||||
(`vegas_max_width_screens`, not counting that padding) is cropped like any
|
||||
other content and scrolls by as plain, no longer live. The user can turn them off per plugin with `vegas_live: false` (a
|
||||
core-owned property) or for the whole ticker with
|
||||
`display.vegas_scroll.live_refresh: false`; they are always off under
|
||||
multi-display sync.
|
||||
|
||||
`scripts/check_plugin.py` checks the contract for any plugin that implements
|
||||
the hook (unique keys, height, width stable with no new data, redraw size,
|
||||
slow calls) and prints a `vegas elements` row; the checks are in
|
||||
`src/plugin_system/testing/vegas.py`. `test/fixtures/plugins/vegas-live-stub`
|
||||
is a small working example.
|
||||
|
||||
#### Legacy: `get_vegas_content_type()` and `get_vegas_display_mode()`
|
||||
|
||||
Superseded by participation, and still read to derive it when neither the
|
||||
|
||||
@@ -47,6 +47,17 @@ is `CORE_PLUGIN_PROPERTIES` in `src/plugin_system/schema_manager.py`):
|
||||
`src/plugin_system/base_plugin.py`; see
|
||||
[PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md#vegas-participation)
|
||||
|
||||
6. **`vegas_live`** (boolean; no default, unset means on)
|
||||
- Description: for a plugin with live Vegas elements (it implements
|
||||
`get_vegas_elements()`), whether the ticker changes what is already
|
||||
scrolling when the plugin's data changes. `false` shows each card as it
|
||||
was when drawn, as before live elements existed
|
||||
- Ignored by plugins without live elements, and whenever live elements
|
||||
are off for the whole ticker (`display.vegas_scroll.live_refresh`)
|
||||
- Read by `PluginAdapter.is_live_capable()` in
|
||||
`src/vegas_mode/plugin_adapter.py`; see
|
||||
[PLUGIN_API_REFERENCE.md](PLUGIN_API_REFERENCE.md#live-vegas-elements)
|
||||
|
||||
`skin` and `skin_options` were core properties until the skin system was
|
||||
removed. A plugin config saved with them still loads and saves; the keys are
|
||||
dropped on the next save (see `RETIRED_PLUGIN_KEYS` in `schema_manager.py`).
|
||||
|
||||
Reference in New Issue
Block a user