mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
Vegas mode: reclaim dead space and pace the rotation
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
This commit is contained in:
@@ -918,7 +918,12 @@ def save_main_config():
|
||||
|
||||
# Handle Vegas scroll mode settings
|
||||
vegas_fields = ['vegas_scroll_enabled', 'vegas_scroll_speed', 'vegas_separator_width',
|
||||
'vegas_target_fps', 'vegas_buffer_ahead', 'vegas_plugin_order', 'vegas_excluded_plugins']
|
||||
'vegas_target_fps', 'vegas_buffer_ahead', 'vegas_plugin_order', 'vegas_excluded_plugins',
|
||||
'vegas_auto_trim', 'vegas_trim_threshold', 'vegas_content_padding',
|
||||
'vegas_min_plugin_width', 'vegas_lead_in_width', 'vegas_plugins_per_cycle',
|
||||
'vegas_max_plugin_width_ratio', 'vegas_dynamic_duration_enabled',
|
||||
'vegas_min_cycle_duration', 'vegas_max_cycle_duration',
|
||||
'vegas_intra_plugin_gap']
|
||||
|
||||
if any(k in data for k in vegas_fields):
|
||||
if 'display' not in current_config:
|
||||
@@ -933,13 +938,43 @@ def save_main_config():
|
||||
# was submitted (any vegas field present) but enabled key is missing,
|
||||
# the checkbox was unchecked and we should set enabled=False
|
||||
vegas_config['enabled'] = _coerce_to_bool(data.get('vegas_scroll_enabled'))
|
||||
vegas_config['auto_trim'] = _coerce_to_bool(data.get('vegas_auto_trim'))
|
||||
vegas_config['dynamic_duration_enabled'] = _coerce_to_bool(
|
||||
data.get('vegas_dynamic_duration_enabled'))
|
||||
|
||||
# max_plugin_width_ratio is the one fractional setting, so it is
|
||||
# handled outside the integer loop below.
|
||||
if data.get('vegas_max_plugin_width_ratio') not in ('', None):
|
||||
try:
|
||||
ratio = float(data['vegas_max_plugin_width_ratio'])
|
||||
except (ValueError, TypeError):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': "Invalid value for vegas_max_plugin_width_ratio: "
|
||||
"must be a number"
|
||||
}), 400
|
||||
if not (0 <= ratio <= 20):
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': "Invalid value for vegas_max_plugin_width_ratio: "
|
||||
"must be between 0 and 20 (0 disables the cap)"
|
||||
}), 400
|
||||
vegas_config['max_plugin_width_ratio'] = ratio
|
||||
|
||||
# Handle numeric settings with validation
|
||||
numeric_fields = {
|
||||
'vegas_scroll_speed': ('scroll_speed', 1, 100),
|
||||
'vegas_separator_width': ('separator_width', 0, 500),
|
||||
'vegas_intra_plugin_gap': ('intra_plugin_gap', 0, 128),
|
||||
'vegas_target_fps': ('target_fps', 1, 200),
|
||||
'vegas_buffer_ahead': ('buffer_ahead', 1, 20),
|
||||
'vegas_trim_threshold': ('trim_threshold', 0, 254),
|
||||
'vegas_content_padding': ('content_padding', 0, 128),
|
||||
'vegas_min_plugin_width': ('min_plugin_width', 0, 512),
|
||||
'vegas_lead_in_width': ('lead_in_width', 0, 2048),
|
||||
'vegas_plugins_per_cycle': ('plugins_per_cycle', 1, 50),
|
||||
'vegas_min_cycle_duration': ('min_cycle_duration', 5, 3600),
|
||||
'vegas_max_cycle_duration': ('max_cycle_duration', 10, 3600),
|
||||
}
|
||||
for field_name, (config_key, min_val, max_val) in numeric_fields.items():
|
||||
if field_name in data:
|
||||
|
||||
@@ -425,7 +425,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-vegas_separator_width" data-setting-key="display.vegas_scroll.separator_width">
|
||||
<label for="vegas_separator_width" class="block text-sm font-medium text-gray-700">Separator Width (pixels){{ ui.help_tip('Blank gap inserted between each plugin block in the ticker (0–128 px).\nDefault: 32. Larger values make the boundary between plugins clearer.', 'Separator Width') }}</label>
|
||||
<label for="vegas_separator_width" class="block text-sm font-medium text-gray-700">Separator Width (pixels){{ ui.help_tip('Blank gap where one plugin hands off to the next (0–128 px).\nDefault: 32. Larger values make the boundary between plugins clearer. This does not apply between rows of the same plugin — see Row Gap for that.', 'Separator Width') }}</label>
|
||||
<input type="number"
|
||||
id="vegas_separator_width"
|
||||
name="vegas_separator_width"
|
||||
@@ -436,6 +436,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group" id="setting-display-vegas_intra_plugin_gap" data-setting-key="display.vegas_scroll.intra_plugin_gap">
|
||||
<label for="vegas_intra_plugin_gap" class="block text-sm font-medium text-gray-700">Row Gap (pixels){{ ui.help_tip('Gap between rows contributed by the same plugin (0–128 px).\nDefault: 8. Multi-row plugins such as sports scoreboards, news feeds and the F1 standings return one image per row; this keeps those rows close together while Separator Width still marks the jump to the next plugin. Set 0 to butt rows directly together.', 'Row Gap') }}</label>
|
||||
<input type="number"
|
||||
id="vegas_intra_plugin_gap"
|
||||
name="vegas_intra_plugin_gap"
|
||||
value="{{ main_config.display.get('vegas_scroll', {}).get('intra_plugin_gap', 8) }}"
|
||||
min="0"
|
||||
max="128"
|
||||
class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group" id="setting-display-vegas_target_fps" data-setting-key="display.vegas_scroll.target_fps">
|
||||
<label for="vegas_target_fps" class="block text-sm font-medium text-gray-700">Target FPS{{ ui.help_tip('Frames per second the Vegas ticker aims to render.\nHigher = smoother scrolling but more CPU. Default: 125 (smoothest). Drop to 60/90 if the Pi runs hot.', 'Target FPS') }}</label>
|
||||
@@ -456,6 +469,137 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cycle Pacing -->
|
||||
<div class="mt-4 pt-4 border-t border-gray-200">
|
||||
<h4 class="text-sm font-medium text-gray-900 mb-3">Cycle Pacing</h4>
|
||||
<p class="text-sm text-gray-600 mb-3">How long one pass through the ticker lasts, and how many plugins it covers.</p>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group" id="setting-display-vegas_plugins_per_cycle" data-setting-key="display.vegas_scroll.plugins_per_cycle">
|
||||
<label for="vegas_plugins_per_cycle" class="block text-sm font-medium text-gray-700">Plugins Per Cycle{{ ui.help_tip('How many plugins are composed into one pass of the ticker (1–50).\nDefault: 6. Higher means more variety before the ticker restarts, and fewer recompose pauses. Lower means each plugin comes around sooner.', 'Plugins Per Cycle') }}</label>
|
||||
<input type="number"
|
||||
id="vegas_plugins_per_cycle"
|
||||
name="vegas_plugins_per_cycle"
|
||||
value="{{ main_config.display.get('vegas_scroll', {}).get('plugins_per_cycle', 6) }}"
|
||||
min="1"
|
||||
max="50"
|
||||
class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-vegas_max_plugin_width_ratio" data-setting-key="display.vegas_scroll.max_plugin_width_ratio">
|
||||
<label for="vegas_max_plugin_width_ratio" class="block text-sm font-medium text-gray-700">Max Plugin Width (screens){{ ui.help_tip('Caps how much of one cycle a single plugin may occupy, measured in screen widths (0–20).\nDefault: 3. A long ticker such as a news feed or leaderboard is trimmed to this and the remainder shown on later cycles, so one plugin cannot hold the display for minutes. Set 0 for no limit.', 'Max Plugin Width') }}</label>
|
||||
<input type="number"
|
||||
id="vegas_max_plugin_width_ratio"
|
||||
name="vegas_max_plugin_width_ratio"
|
||||
value="{{ main_config.display.get('vegas_scroll', {}).get('max_plugin_width_ratio', 3.0) }}"
|
||||
min="0"
|
||||
max="20"
|
||||
step="0.5"
|
||||
class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group mt-4" id="setting-display-vegas_dynamic_duration_enabled" data-setting-key="display.vegas_scroll.dynamic_duration_enabled">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
id="vegas_dynamic_duration_enabled"
|
||||
name="vegas_dynamic_duration_enabled"
|
||||
{% if main_config.display.get('vegas_scroll', {}).get('dynamic_duration_enabled', True) %}checked{% endif %}
|
||||
class="form-checkbox">
|
||||
<span class="ml-2 text-sm text-gray-700">Size cycle time to the content{{ ui.help_tip('When on, each cycle runs just long enough to scroll all its content past, clamped to the min and max below.\nWhen off, the max is always used. Default: on.', 'Dynamic Cycle Duration') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-2">
|
||||
<div class="form-group" id="setting-display-vegas_min_cycle_duration" data-setting-key="display.vegas_scroll.min_cycle_duration">
|
||||
<label for="vegas_min_cycle_duration" class="block text-sm font-medium text-gray-700">Min Cycle Time (seconds){{ ui.help_tip('Shortest a single ticker pass may last (5–3600 s).\nDefault: 60.', 'Min Cycle Time') }}</label>
|
||||
<input type="number"
|
||||
id="vegas_min_cycle_duration"
|
||||
name="vegas_min_cycle_duration"
|
||||
value="{{ main_config.display.get('vegas_scroll', {}).get('min_cycle_duration', 60) }}"
|
||||
min="5"
|
||||
max="3600"
|
||||
class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-vegas_max_cycle_duration" data-setting-key="display.vegas_scroll.max_cycle_duration">
|
||||
<label for="vegas_max_cycle_duration" class="block text-sm font-medium text-gray-700">Max Cycle Time (seconds){{ ui.help_tip('Longest a single ticker pass may last before it restarts with fresh content (10–3600 s).\nThis is the setting that caps total Vegas scroll time. Default: 240. Lower it if the ticker feels like it takes too long to come back around.', 'Max Cycle Time') }}</label>
|
||||
<input type="number"
|
||||
id="vegas_max_cycle_duration"
|
||||
name="vegas_max_cycle_duration"
|
||||
value="{{ main_config.display.get('vegas_scroll', {}).get('max_cycle_duration', 240) }}"
|
||||
min="10"
|
||||
max="3600"
|
||||
class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dead Space -->
|
||||
<div class="mt-4 pt-4 border-t border-gray-200">
|
||||
<h4 class="text-sm font-medium text-gray-900 mb-3">Dead Space</h4>
|
||||
<p class="text-sm text-gray-600 mb-3">Plugins that draw onto a full-screen canvas contribute all the empty space around their content. Trimming reclaims it so the ticker stays full.</p>
|
||||
|
||||
<div class="form-group mb-4" id="setting-display-vegas_auto_trim" data-setting-key="display.vegas_scroll.auto_trim">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox"
|
||||
id="vegas_auto_trim"
|
||||
name="vegas_auto_trim"
|
||||
{% if main_config.display.get('vegas_scroll', {}).get('auto_trim', True) %}checked{% endif %}
|
||||
class="form-checkbox">
|
||||
<span class="ml-2 text-sm text-gray-700">Trim empty edges from plugin content{{ ui.help_tip('Crops blank columns from the left and right of each plugin block before it enters the ticker. Space between two pieces of content inside a block is left alone, so layouts are not altered. Default: on.', 'Auto Trim') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="form-group" id="setting-display-vegas_content_padding" data-setting-key="display.vegas_scroll.content_padding">
|
||||
<label for="vegas_content_padding" class="block text-sm font-medium text-gray-700">Content Padding (pixels){{ ui.help_tip('Blank columns kept either side of trimmed content, so it does not butt against the separator (0–128 px).\nDefault: 8.', 'Content Padding') }}</label>
|
||||
<input type="number"
|
||||
id="vegas_content_padding"
|
||||
name="vegas_content_padding"
|
||||
value="{{ main_config.display.get('vegas_scroll', {}).get('content_padding', 8) }}"
|
||||
min="0"
|
||||
max="128"
|
||||
class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-vegas_lead_in_width" data-setting-key="display.vegas_scroll.lead_in_width">
|
||||
<label for="vegas_lead_in_width" class="block text-sm font-medium text-gray-700">Lead-In Gap (pixels){{ ui.help_tip('Blank space before the first plugin of each cycle (0–2048 px).\nDefault: 0. Anything approaching your screen width reads as the display switching off at the start of every cycle.', 'Lead-In Gap') }}</label>
|
||||
<input type="number"
|
||||
id="vegas_lead_in_width"
|
||||
name="vegas_lead_in_width"
|
||||
value="{{ main_config.display.get('vegas_scroll', {}).get('lead_in_width', 0) }}"
|
||||
min="0"
|
||||
max="2048"
|
||||
class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
|
||||
<div class="form-group" id="setting-display-vegas_trim_threshold" data-setting-key="display.vegas_scroll.trim_threshold">
|
||||
<label for="vegas_trim_threshold" class="block text-sm font-medium text-gray-700">Trim Threshold{{ ui.help_tip('How bright a pixel must be to count as content rather than empty space (0–254).\nDefault: 10, which ignores the near-black noise left by image compression. Raise it if very dark artwork is being kept; lower it if dark detail is being cropped.', 'Trim Threshold') }}</label>
|
||||
<input type="number"
|
||||
id="vegas_trim_threshold"
|
||||
name="vegas_trim_threshold"
|
||||
value="{{ main_config.display.get('vegas_scroll', {}).get('trim_threshold', 10) }}"
|
||||
min="0"
|
||||
max="254"
|
||||
class="form-control">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="setting-display-vegas_min_plugin_width" data-setting-key="display.vegas_scroll.min_plugin_width">
|
||||
<label for="vegas_min_plugin_width" class="block text-sm font-medium text-gray-700">Min Plugin Width (pixels){{ ui.help_tip('Plugin blocks narrower than this after trimming are skipped for that cycle (0–512 px).\nDefault: 8. Raise it to hide plugins showing only a tiny placeholder such as "No Data" until they have real content.', 'Min Plugin Width') }}</label>
|
||||
<input type="number"
|
||||
id="vegas_min_plugin_width"
|
||||
name="vegas_min_plugin_width"
|
||||
value="{{ main_config.display.get('vegas_scroll', {}).get('min_plugin_width', 8) }}"
|
||||
min="0"
|
||||
max="512"
|
||||
class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Plugin Order Section -->
|
||||
<div class="mt-4 pt-4 border-t border-gray-200">
|
||||
<h4 class="text-sm font-medium text-gray-900 mb-3">Plugin Order</h4>
|
||||
|
||||
Reference in New Issue
Block a user