Merge #1920: Preview: keep rendering past the cached frames after a loop or restart (@ded-achtung)

This commit is contained in:
aiongg
2026-10-23 21:37:28 +00:00
2 changed files with 64 additions and 17 deletions
+25 -17
View File
@@ -1238,26 +1238,34 @@ 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);
for k in 0..window {
// preview shows first, or be evicted before it gets there (#595). The window starts at the
// first uncached frame ahead: after a loop or a restart the frames just ahead are cached,
// and a window from the playhead left the workers idle until it came near the end of the
// cached run. It stays ahead × 3 frames wide: workers spread over the whole preview render
// frames with different layers at once (on the GPU, an atlas each: #1812).
let fit_ahead = self.frames_that_fit(&c, scale) - 1;
let frame = |k: i64| match plan {
Some(p) => p.frame_after(cur, k),
None => {
let f = cur + k;
Some(if f > wb { wa + (f - wb - 1).rem_euclid((wb - wa + 1).max(1)) } else { f })
}
};
let cached = self.frames.cached_frames(&series);
let uncached = |f: i64| cached.binary_search(&f).is_err();
let first = match plan.filter(|_| self.playback.playing) {
Some(p) => (0..fit_ahead.min((p.end - p.start) / p.step.max(1) + 1))
.map_while(|k| frame(k).map(|f| (k, f)))
.find(|&(_, f)| uncached(f))
.map_or(0, |(k, _)| k),
None => 0,
};
for k in first..(first + ahead * 3).min(fit_ahead) {
if queued >= ahead as usize || !video {
break;
}
let f = match plan {
Some(p) => match p.frame_after(cur, k) {
Some(f) => f,
None => break,
},
None => {
let mut f = cur + k;
if f > wb {
f = wa + (f - wb - 1).rem_euclid((wb - wa + 1).max(1));
}
f
}
};
if !self.frames.is_cached(&FrameKey { frame: f, ..series }) {
let Some(f) = frame(k) else { break };
if uncached(f) {
self.request_frame(cid, f, scale);
queued += 1;
}
@@ -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::<Vec<_>>());
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();