fix(vegas): lock the scroll to the panel refresh; encode the preview off the render thread

Vegas advanced by elapsed time, blended neighbouring columns every frame,
and paced itself with a sleep to target_fps. On hdpi (4x128x64 on one
chain, a 120Hz cap the chain cannot reach, ~95-100Hz real) that ran at
73fps with target 90 and ~89fps with target 125: the sleep drifted
against the refresh and missed a vsync every few frames, and the blend
read as shimmer on the panel (and as "anti-aliased" text in the preview).

smooth_scroll now means the crisp pacing the plugin tickers already use:
a whole number of pixels per presented frame, each held for frame_hold
refreshes, with SwapOnVSync as the clock. The speed is solved against the
panel's measured refresh, timed from our own swaps once scrolling starts,
because the configured limit is only a cap -- at "120Hz" 90px/s solves to
3px every 4 refreshes, at the real ~97Hz to 1px every refresh. The old
blend stays available as sub_pixel_blend (default off).

With the web preview open, the render thread also PNG-encoded the whole
512x64 frame five times a second, 12-14ms each -- longer than a refresh.
Mid-scroll that encode now runs on a single-slot writer thread (Pillow
releases the GIL while compressing); static frames still write inline.

Measured on hdpi, 3-minute soak with the preview open: 3 of 17,280
frames held an extra refresh (0.02%), down from ~6-20%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-09-23 21:41:15 -04:00
co-authored by Claude Opus 5.5
parent f3894916a9
commit 0a2ce58026
10 changed files with 447 additions and 69 deletions
+16 -5
View File
@@ -58,13 +58,22 @@ class VegasModeConfig:
# switched off at the start of every cycle.
lead_in_width: int = 0
# Blend between neighbouring pixel positions so motion happens at the frame
# rate rather than the scroll speed. With integer positioning the number of
# distinct frames per second equals scroll_speed, so at 50px/s the motion is
# 50 discrete 1px steps however fast the loop runs. The trade is a slight
# horizontal softening of text, since each frame is a blend of two positions.
# Lock motion to the panel: a whole number of pixels per presented frame,
# each frame held for a whole number of refreshes, with SwapOnVSync as the
# clock (see src/common/scroll_config.py). scroll_speed is snapped to the
# nearest speed the panel can show that way. Off falls back to advancing by
# elapsed time, which drifts against the refresh and judders.
smooth_scroll: bool = True
# The older way of smoothing: advance by elapsed time and blend the two
# neighbouring pixel positions each frame. It looks anti-aliased in the web
# preview, but on the panel the blended columns shimmer (the library's
# brightness curve makes a 50% blend far dimmer than half), text softens,
# and the loop is not tied to the refresh, so it still misses frames.
# Measured on a 512x64 chain at 95Hz: 73-89fps, p99 20-28ms. Takes
# precedence over smooth_scroll's whole-pixel pacing when on.
sub_pixel_blend: bool = False
# Keep one continuous strip, extending it with the next group of plugins as
# the scroll approaches the end, instead of composing a fresh strip and
# swapping it in. A swap stops the motion, substitutes every pixel at once
@@ -192,6 +201,7 @@ class VegasModeConfig:
vegas_config.get('min_content_separation', 24)),
min_cut_gap=int(vegas_config.get('min_cut_gap', 6)),
smooth_scroll=vegas_config.get('smooth_scroll', True),
sub_pixel_blend=bool(vegas_config.get('sub_pixel_blend', False)),
continuous_scroll=vegas_config.get('continuous_scroll', True),
extend_threshold_screens=float(
vegas_config.get('extend_threshold_screens', 2.0)),
@@ -232,6 +242,7 @@ class VegasModeConfig:
'min_content_separation': self.min_content_separation,
'min_cut_gap': self.min_cut_gap,
'smooth_scroll': self.smooth_scroll,
'sub_pixel_blend': self.sub_pixel_blend,
'continuous_scroll': self.continuous_scroll,
'extend_threshold_screens': self.extend_threshold_screens,
'auto_trim': self.auto_trim,