diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 72016cc5..541081af 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1238,8 +1238,16 @@ impl EffectcraftApp { } let step = plan.map_or(1, |p| p.step); // Never further ahead than the RAM preview holds: those frames would evict the ones the - // preview shows first, or be evicted before it gets there (#595). - let window = (ahead * 3).min(self.frames_that_fit(&c, scale) - 1); + // preview shows first, or be evicted before it gets there (#595). A playing preview + // looks that far for the next uncached frames: after a loop the frames just ahead are + // cached, and a shorter window left the workers idle until the playhead came near the + // end of the cached run. + let fit_ahead = self.frames_that_fit(&c, scale) - 1; + let window = match plan.filter(|_| self.playback.playing) { + Some(p) => fit_ahead.min((p.end - p.start) / p.step.max(1) + 1), + None => (ahead * 3).min(fit_ahead), + }; + let cached = self.frames.cached_frames(&series); for k in 0..window { if queued >= ahead as usize || !video { break; @@ -1257,7 +1265,7 @@ impl EffectcraftApp { f } }; - if !self.frames.is_cached(&FrameKey { frame: f, ..series }) { + if cached.binary_search(&f).is_err() { self.request_frame(cid, f, scale); queued += 1; } diff --git a/crates/ui-egui/tests/ui_preview_align.rs b/crates/ui-egui/tests/ui_preview_align.rs index e9b8d6c4..2b2ba1b4 100644 --- a/crates/ui-egui/tests/ui_preview_align.rs +++ b/crates/ui-egui/tests/ui_preview_align.rs @@ -259,6 +259,45 @@ fn cache_frames_when_idle_fills_the_work_area() { assert_eq!(n, 240, "the work area is cached while idle"); } +/// A preview that plays into frames it already cached (after a loop) keeps rendering the next +/// uncached ones: the prefetch window used to end within the cached run, and the workers stood +/// idle until the playhead came near its end. +#[test] +fn playback_prefetches_past_the_cached_frames_ahead() { + let mut h = harness(); + let cid = h.state().session.active_comp_id().unwrap(); + let series = h.state().shown_series(cid); + let scale = series.scale as f64 / 1000.0; + // The first 120 frames (5 s) are cached: more than the widest old window (16 workers × 6). + for f in 0..120 { + h.state().request_frame(cid, f, scale); + } + for _ in 0..4000 { + if h.state().frames.cached_frames(&series).len() >= 120 && h.state().frames.inflight() == 0 { + break; + } + std::thread::sleep(std::time::Duration::from_millis(2)); + } + assert_eq!(h.state().frames.cached_frames(&series), (0..120).collect::>()); + h.state_mut().session.set_time(Tick::ZERO); + invoke(&mut h, "playback.toggle", json!({})); + h.run_steps(2); + assert!(h.state().playback.playing); + let cur = h.state().session.time(); + assert!(cur < Tick::from_seconds_f64(1.0), "{cur:?}"); + for _ in 0..4000 { + if h.state().frames.cached_frames(&series).iter().any(|&f| f >= 120) { + break; + } + h.step(); + std::thread::sleep(std::time::Duration::from_millis(1)); + if h.state().session.time() >= Tick::from_seconds_f64(1.0) { + break; + } + } + assert!(h.state().frames.cached_frames(&series).iter().any(|&f| f >= 120), "frames past the cached run render while it plays the first second"); +} + #[test] fn cache_frames_when_idle_survives_an_inverted_work_area() { let mut h = harness();