From 0883c77e2b2a59103fe0f27fb9d0e0546707e98a Mon Sep 17 00:00:00 2001 From: Skyler Lehmkuhl Date: Sun, 21 Jun 2026 01:12:24 -0400 Subject: [PATCH] Phase 4: prefetch upcoming images during playback `assets_needed_at(document, time)` (core) enumerates the image asset ids referenced by the visible vector layers' active keyframes at a time (top-level + group children). During playback the stage decodes the images needed ~0.5s ahead into the bounded ImageCache, so a keyframe that swaps image fills doesn't hitch when the playhead reaches it. Gated on is_playing; nested clip-instance recursion + background decode are refinements. Completes Phase 4 (image asset paging: Tier 2 decoded-cache LRU, Tier 1 lazy bytes, playback prefetch). --- .../lightningbeam-core/src/renderer.rs | 22 +++++++++++++++++++ .../lightningbeam-editor/src/panes/stage.rs | 19 ++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/lightningbeam-ui/lightningbeam-core/src/renderer.rs b/lightningbeam-ui/lightningbeam-core/src/renderer.rs index 9b851f7..a13203c 100644 --- a/lightningbeam-ui/lightningbeam-core/src/renderer.rs +++ b/lightningbeam-ui/lightningbeam-core/src/renderer.rs @@ -158,6 +158,28 @@ impl Default for ImageCache { } } +/// Image asset ids referenced by the visible vector layers' active keyframes at `time` +/// (top-level + group children). Used to prefetch/decode images ahead during playback. +/// (Recursing into nested clip instances is a refinement.) +pub fn assets_needed_at(document: &Document, time: f64) -> Vec { + let mut ids = Vec::new(); + for layer in document.all_layers() { + if let crate::layer::AnyLayer::Vector(vl) = layer { + if !vl.layer.visible { + continue; + } + if let Some(kf) = vl.keyframe_at(time) { + for fill in &kf.graph.fills { + if let Some(id) = fill.image_fill { + ids.push(id); + } + } + } + } + } + ids +} + /// Decode image bytes to a premultiplied tiny-skia Pixmap (CPU render path). fn decode_image_to_pixmap(data: &[u8]) -> Option { let img = image::load_from_memory(data).ok()?; diff --git a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs index adac5b7..88bd8c4 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/panes/stage.rs @@ -439,6 +439,8 @@ struct VelloRenderContext { onion: crate::panes::OnionSkinSettings, /// `.beam` container path for lazily paging image-asset bytes in the ImageCache. container_path: Option, + /// Whether playback is active (gates image prefetch). + is_playing: bool, /// Active layer for tool operations active_layer_id: Option, /// Delta for drag preview (world space) @@ -1047,6 +1049,22 @@ impl egui_wgpu::CallbackTrait for VelloCallback { } } + // Prefetch: during playback, decode images the upcoming frames will need + // (a short lookahead) into the bounded cache, so a keyframe that swaps image + // fills doesn't hitch when the playhead reaches it. + if self.ctx.is_playing { + const PREFETCH_LOOKAHEAD: f64 = 0.5; + let ahead = lightningbeam_core::renderer::assets_needed_at( + &self.ctx.document, + self.ctx.playback_time + PREFETCH_LOOKAHEAD, + ); + for id in ahead { + if let Some(asset) = self.ctx.document.get_image_asset(&id) { + let _ = image_cache.get_or_decode(asset); + } + } + } + drop(image_cache); let _t_after_scene_build = std::time::Instant::now(); @@ -12078,6 +12096,7 @@ impl PaneRenderer for StagePane { tool_state: shared.tool_state.clone(), onion: shared.onion, container_path: shared.container_path.clone(), + is_playing: *shared.is_playing, active_layer_id: *shared.active_layer_id, drag_delta, selection: shared.selection.clone(),