From ff20f8ec41291aff48dc32e4c3921ffaaf0c73bf Mon Sep 17 00:00:00 2001 From: ded Date: Tue, 20 Oct 2026 03:34:33 +0300 Subject: [PATCH] G5: a playing preview prefetches past the frames it already cached The playback prefetch only looked ahead*3 frames (96 with 16 workers) for uncached frames. After a loop or a restart from the start those were all cached, so the workers stood idle until the playhead came within that window of the end of the cached run, then the preview stalled. A playing preview now looks as far ahead as the RAM preview holds (and the range spans) for the next uncached frames, still queuing at most ahead at a time; eviction already spares the frames the playhead reaches first (#595). Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/lib.rs | 14 +++++++-- crates/ui-egui/tests/ui_preview_align.rs | 39 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) 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();