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).
This commit is contained in:
Skyler Lehmkuhl 2026-06-21 01:12:24 -04:00
parent 3d0a334014
commit 0883c77e2b
2 changed files with 41 additions and 0 deletions

View File

@ -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<Uuid> {
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<tiny_skia::Pixmap> {
let img = image::load_from_memory(data).ok()?;

View File

@ -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<std::path::PathBuf>,
/// Whether playback is active (gates image prefetch).
is_playing: bool,
/// Active layer for tool operations
active_layer_id: Option<uuid::Uuid>,
/// 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(),