On a wide panel Vegas mode spent much of its time showing black. At 50px/s
on a 512px display, one display width of blank is 10.2 seconds, which makes
several long-standing behaviours expensive:
- ScrollHelper prepended a full display width of black as an "initial gap",
charged once per cycle — 10.2s of black at the start of every rotation.
- Plugins without get_vegas_content() are captured off a full-display canvas,
so their blank margins entered the ticker too. Measured: of-the-day drew
35px of "No Data" on a 512px canvas (92% blank), youtube-stats 142px of
content with 185px of black either side. Only the scroll_helper path had
any trimming.
- Cycle transitions deliberately pushed a blank frame and then recomposed
synchronously: 84ms at best, 4.8s at worst, every millisecond of it black.
- buffer_ahead doubled as the cycle size, so a 21-plugin install showed 3
plugins per cycle and took ~7 cycles to come around.
- separator_width was applied between every image rather than at plugin
boundaries, so a per-row ticker like the F1 scoreboard (116 images, which
it renders 4px apart internally) got a 32px chasm between each row — and
the width budget didn't count those gaps, so the plugin quietly occupied
far more of the panel than intended.
Changes:
- src/vegas_mode/geometry.py: numpy column-ink primitives shared by the
trimmer and the audit tool, so the number reported is the number acted on.
A Python per-column loop over a 17,000px strip is far too slow for the
render path.
- PluginAdapter trims every content path, not just scroll_helper. Only outer
edges are cropped: interior blank columns are the plugin's own layout
(logo left, score right) and closing them would corrupt the design. A
plugin on a non-black background is inherently unaffected.
- ScrollHelper.create_scrolling_image takes an explicit lead_gap, still
defaulting to display_width so the many standalone-ticker callers are
unchanged. Vegas passes lead_in_width (default 0).
- Cycle end holds the last rendered frame instead of blanking, turning the
recompose into a brief freeze rather than the panel switching off.
- plugins_per_cycle (default 6) is split from buffer_ahead, which goes back
to being only a prefetch low-water mark.
- max_plugin_width_ratio (default 3x display width) caps one plugin's share
of a cycle. Overflow is deferred, not discarded: a rotation offset advances
each fetch so later rows appear on subsequent cycles. Single oversized
images are cropped at a blank column so the cut misses glyphs.
- Composition groups images by plugin: rows are joined by intra_plugin_gap
(default 8) and separator_width applies only between plugins. The width
budget now counts those gaps.
- Plugin data updates no longer run on the Vegas render path.
All new settings are user-configurable in Display -> Vegas Scroll, including
min/max cycle duration and dynamic duration, which previously existed in code
but were reachable only by hand-editing config.json.
Measured with scripts/dev/vegas_audit.py on a 512x64 panel:
mean ink coverage 42.7% -> 69.4%
fully blank 5.9% -> 0%
reads as empty 13.6% -> 0%
worst blank stretch 4.8s -> 0s
full rotation 414s -> 123s
plugins per cycle 3 -> 6
Note the metric choice: a "fully blank" scan (>=95% black viewport) reported
only 0.4% and badly understated the problem, because two full-width segments
with mid-canvas content never fully blank the viewport — they hold it at ~28%.
window_coverage_stats grades every viewport position by how much ink it
carries, which is what tracks perceived dead time.
Known remaining: cycle transitions still freeze ~3.5s while the next cycle is
fetched. Fixing that needs background prefetch, which is deferred because the
fallback-capture path mutates the shared display_manager.image and racing it
against the render loop risks torn frames.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ