Merge pull request #784 from vangel76/fix-329-seek-latency

#329: scrubbing a long movie: superseded viewer frames call off their decodes

# Conflicts:
#	crates/media/tests/footage.rs
This commit is contained in:
Brandon Thomas
2026-10-12 03:22:50 -04:00
6 changed files with 260 additions and 13 deletions
+88 -7
View File
@@ -23,6 +23,7 @@ pub mod text;
pub mod three_d;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
pub use auto::{AutoKey, AutoPick};
pub use cache::{CacheStats, LayerCache, LayerStore, PrefetchStore};
@@ -110,6 +111,80 @@ pub trait FootageSource: Send + Sync {
fn vector_frame(&self, _item: ItemId, _footage: &Footage, _scale: f64) -> Option<Arc<Image>> {
None
}
/// Run `f`, which reads frames from this source, as work that can be called off: once
/// `cancel` is set, long decodes it starts (a movie seeking back to a keyframe and decoding
/// forward) stop early and their frames come back `None`. Sources that can't stop a decode
/// just run `f`. See [`CancellableFootage`].
fn cancellable(&self, _cancel: &Arc<AtomicBool>, f: &mut dyn FnMut()) {
f()
}
/// Reads through this source have been called off: frames may have come back `None` that
/// exist, so what was made from them must not be kept.
fn called_off(&self) -> bool {
false
}
}
/// A render's view of a [`FootageSource`] whose frame reads can be called off with `cancel`
/// (a viewer frame nobody waits for any more, #329). Every read goes through
/// [`FootageSource::cancellable`] on its own, so reads made for this render on another thread
/// (a parallel loop) answer to this render's flag, and reads for other renders never do.
pub struct CancellableFootage<'a> {
pub inner: &'a dyn FootageSource,
pub cancel: Arc<AtomicBool>,
}
impl CancellableFootage<'_> {
fn read<R>(&self, f: impl FnOnce() -> Option<R>) -> Option<R> {
if self.cancel.load(Ordering::Relaxed) {
return None;
}
let (mut f, mut out) = (Some(f), None);
self.inner.cancellable(&self.cancel, &mut || out = f.take().and_then(|f| f()));
out
}
}
impl FootageSource for CancellableFootage<'_> {
fn frame(&self, item: ItemId, footage: &Footage, t: Tick) -> Option<Arc<Image>> {
self.read(|| self.inner.frame(item, footage, t))
}
fn frame_at_size(&self, item: ItemId, footage: &Footage, t: Tick, width: u32, height: u32) -> Option<Arc<Image>> {
self.read(|| self.inner.frame_at_size(item, footage, t, width, height))
}
fn audio(&self, item: ItemId, footage: &Footage, t: Tick, frames: usize, rate: u32) -> Option<Vec<f32>> {
self.inner.audio(item, footage, t, frames, rate)
}
fn aux(&self, item: ItemId, footage: &Footage, t: Tick) -> Option<Arc<effectcraft_raster::AuxChannels>> {
self.inner.aux(item, footage, t)
}
fn model(&self, item: ItemId, footage: &Footage) -> Option<Arc<effectcraft_model::Model>> {
self.inner.model(item, footage)
}
fn set_cache_budget(&self, bytes: usize) {
self.inner.set_cache_budget(bytes)
}
fn purge(&self) {
self.inner.purge()
}
fn forget(&self, path: &str) {
self.inner.forget(path)
}
fn set_conform_folder(&self, folder: Option<std::path::PathBuf>) {
self.inner.set_conform_folder(folder)
}
fn cache_budget(&self) -> Option<usize> {
self.inner.cache_budget()
}
fn vector_frame(&self, item: ItemId, footage: &Footage, scale: f64) -> Option<Arc<Image>> {
self.inner.vector_frame(item, footage, scale)
}
fn cancellable(&self, cancel: &Arc<AtomicBool>, f: &mut dyn FnMut()) {
self.inner.cancellable(cancel, f)
}
fn called_off(&self) -> bool {
self.cancel.load(Ordering::Relaxed) || self.inner.called_off()
}
}
/// Footage that can be rasterised at any scale (SVG).
@@ -604,6 +679,12 @@ impl<'a> Renderer<'a> {
}
}
/// The layer cache to keep this render's buffers in: none once its footage reads were
/// called off, as they may have come back empty ([`FootageSource::called_off`], #329).
fn cache_to_keep(&self) -> Option<&'a LayerCache> {
self.cache.filter(|_| !self.footage.called_off())
}
/// A layer's Quality switch, limited by the precomp layers above it when
/// [`RenderOpts::nested_switches`] is on.
pub fn quality(&self, layer: &Layer) -> Quality {
@@ -1279,7 +1360,7 @@ impl<'a> Renderer<'a> {
let b = self.footage.frame_with(item, f, f.frame_rate.tick_of(i + 1), None, float);
if let (Some(a), Some(b)) = (a, b) {
let img = blend_frames(mode, &a, &b, w as f32);
if let (Some(c), Some(k)) = (self.cache, key) {
if let (Some(c), Some(k)) = (self.cache_to_keep(), key) {
c.insert(k, Arc::new(Buf { img: img.clone(), offset: [0.0; 2], scale: 1.0 }));
}
return Some(Arc::new(img));
@@ -1327,7 +1408,7 @@ impl<'a> Renderer<'a> {
let mut nb = (*b).clone();
color::convert(&mut nb.img, &c);
let nb = Arc::new(nb);
if let (Some(cache), Some(k)) = (self.cache, key) {
if let (Some(cache), Some(k)) = (self.cache_to_keep(), key) {
cache.insert(k, nb.clone());
}
nb
@@ -1345,7 +1426,7 @@ impl<'a> Renderer<'a> {
return Some((b, key));
}
let flat = Arc::new(styles::Styled { passes: st.passes.iter().map(|(b, m)| ((**b).clone(), *m)).collect(), body: (*st.body).clone() }.flatten());
if let (Some(c), Some(k)) = (self.cache, key) {
if let (Some(c), Some(k)) = (self.cache_to_keep(), key) {
c.insert(k, flat.clone());
}
Some((flat, key))
@@ -1376,7 +1457,7 @@ impl<'a> Renderer<'a> {
}
let buf = self.masked_source(ctx, layer)?;
let buf = Arc::new(self.apply_effects_timed(ctx, layer, buf, false, usize::MAX, timing.map(|t| &mut t.effects)));
if let (Some(c), Some(k)) = (self.cache, key) {
if let (Some(c), Some(k)) = (self.cache_to_keep(), key) {
c.insert(k, buf.clone());
}
Some((buf, key))
@@ -1401,7 +1482,7 @@ impl<'a> Renderer<'a> {
buf = self.effect_space_stack(ctx, layer, buf, false, effects, None, origin);
}
let buf = Arc::new(buf);
if let (Some(c), Some(k)) = (self.cache, key) {
if let (Some(c), Some(k)) = (self.cache_to_keep(), key) {
c.insert(k, buf.clone());
}
Some(buf)
@@ -1433,7 +1514,7 @@ impl<'a> Renderer<'a> {
}
let body = Arc::new(st.body);
let passes: Vec<_> = st.passes.into_iter().map(|(b, m)| (Arc::new(b), m)).collect();
if let (Some(c), Some(k)) = (self.cache, key) {
if let (Some(c), Some(k)) = (self.cache_to_keep(), key) {
c.insert(k, body.clone());
for (i, (b, _)) in passes.iter().enumerate() {
c.insert(cache::derive(k, i as u64 + 1), b.clone());
@@ -2057,7 +2138,7 @@ impl<'a> Renderer<'a> {
let mut foot = self.source(ctx, layer)?;
let _ = masks::apply(ctx, layer, &mut foot, self.mask_blur(ctx, layer));
let foot = Arc::new(foot);
if let (Some(c), Some(k)) = (self.cache, key) {
if let (Some(c), Some(k)) = (self.cache_to_keep(), key) {
c.insert(k, foot.clone());
}
Some(foot)
+17
View File
@@ -268,6 +268,23 @@ fn render_cached(p: &Project, cid: ItemId, t: Tick, cache: Option<&crate::LayerC
r.comp_frame(cid, t)
}
/// #329: a render whose footage reads were called off keeps nothing in the layer cache: a layer
/// made from a frame that came back empty (an effect's layer parameter) would be reused by the
/// next render of that time.
#[test]
fn a_called_off_render_keeps_nothing_in_the_layer_cache() {
let (p, cid) = cache_scene();
let t = Tick::from_seconds_f64(0.5);
let cache = crate::LayerCache::default();
let off = crate::CancellableFootage { inner: &crate::NoFootage, cancel: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)) };
let mut r = crate::Renderer::new(&p, &off, crate::RenderOpts::default());
r.cache = Some(&cache);
r.comp_frame(cid, t);
assert_eq!(cache.stats().entries, 0, "a called-off render kept layers");
render_cached(&p, cid, t, Some(&cache));
assert!(cache.stats().entries > 0, "a render that runs to the end keeps its layers");
}
/// [`add_effect`] for a 200×100 layer.
fn add_effect_200(p: &mut Project, l: &mut effectcraft_project::Layer, id: &str, vals: &[(&str, Value)]) {
add_effect(p, l, id, [200.0, 100.0], vals);