mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-10 09:06:36 +00:00
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:
@@ -357,3 +357,57 @@ class TestFrameHoldLifetime:
|
||||
assert dm._frame_hold == 1
|
||||
finally:
|
||||
dm.set_scrolling_state(False)
|
||||
|
||||
|
||||
class TestSnapshotOffRenderThread:
|
||||
"""Mid-scroll, the preview PNG is encoded off the render thread.
|
||||
|
||||
At 512x64 the encode takes 12-14ms on a Pi 4 -- longer than a refresh --
|
||||
so doing it inline made the next swap miss its vsync five times a second
|
||||
whenever the web preview was open.
|
||||
"""
|
||||
|
||||
def _record_saves(self, dm, monkeypatch):
|
||||
import threading
|
||||
threads = []
|
||||
done = threading.Event()
|
||||
real = dm._save_snapshot
|
||||
|
||||
def recording(image):
|
||||
threads.append(threading.current_thread().name)
|
||||
real(image)
|
||||
done.set()
|
||||
|
||||
monkeypatch.setattr(dm, "_save_snapshot", recording)
|
||||
return threads, done
|
||||
|
||||
def _due(self, dm, tmp_path, colour):
|
||||
dm._snapshot_path = str(tmp_path / "snap.png")
|
||||
dm._last_snapshot_ts = 0.0
|
||||
dm._last_snapshot_touch_ts = 0.0
|
||||
dm._last_snapshot_digest = None
|
||||
dm.draw.rectangle([0, 0, 10, 4], fill=colour)
|
||||
|
||||
def test_scrolling_frames_are_encoded_on_the_writer_thread(
|
||||
self, dm, tmp_path, monkeypatch):
|
||||
import threading
|
||||
threads, done = self._record_saves(dm, monkeypatch)
|
||||
self._due(dm, tmp_path, (0, 255, 255))
|
||||
dm.set_scrolling_state(True)
|
||||
try:
|
||||
dm.update_display()
|
||||
assert done.wait(5), "the snapshot writer never wrote the frame"
|
||||
finally:
|
||||
dm.set_scrolling_state(False)
|
||||
assert threads == ["snapshot-writer"]
|
||||
assert threads[0] != threading.current_thread().name
|
||||
assert os.path.exists(dm._snapshot_path)
|
||||
|
||||
def test_static_frames_are_still_written_inline(
|
||||
self, dm, tmp_path, monkeypatch):
|
||||
import threading
|
||||
threads, _ = self._record_saves(dm, monkeypatch)
|
||||
self._due(dm, tmp_path, (255, 0, 255))
|
||||
dm.set_scrolling_state(False)
|
||||
dm.update_display()
|
||||
assert threads == [threading.current_thread().name]
|
||||
|
||||
Reference in New Issue
Block a user