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>
This commit is contained in:
Chuck
2026-09-30 18:36:39 -04:00
co-authored by Claude Opus 5.5
parent 6efea8c19a
commit 1160eb5efe
5 changed files with 322 additions and 29 deletions
+14
View File
@@ -332,6 +332,20 @@ read any of them:
worker, which applies the latest one as soon as the lock frees, and before
the plugin's next update() at the latest. The plugin API is unchanged.
### Scrolling
- A Vegas strip extension costs the render thread about a third of what it
did. Appending the next group and trimming what has scrolled past each
rebuilt the strip's PIL image from its numpy array in full
(`Image.fromarray`: 1.7ms for an 8,000px strip, 3.8ms for 20,000px, on a
Pi 4 -- twice per extension), though every frame is cut from the array and
nothing on the frame path reads the image's pixels. `ScrollHelper` now
builds `cached_image` only when something reads it, which in Vegas means
only a multi-display sync push, and the strip is no longer held in memory
twice. Assigning `cached_image` still stores exactly what was assigned.
New `ScrollHelper.has_strip()` says whether there is a strip without
building its image; the frame path and Vegas use it.
### Tooling
- The frame-timing recorder says which render-thread work a late frame