M4.7: Cache Frames When Idle renders the work area in the background

Composition ▸ Preview ▸ Cache Frames When Idle was only a saved checkbox. After a second
without input or edits it now renders the work area's frames (from the current time on, then
from the start) into the RAM preview, a few at a time behind the viewer's own frame, until the
work area is cached or the RAM preview budget is full.

Docs: preferences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-05 00:56:49 +07:00
parent 6214aecdb9
commit 5ce641c6b3
3 changed files with 74 additions and 1 deletions

View File

@ -179,6 +179,9 @@ pub struct EffectcraftApp {
pending_screenshots: Vec<(u64, Option<String>, Option<[f32; 4]>, Sender<serde_json::Value>, f64)>,
next_token: u64,
pub(crate) last_ui_time: f64,
/// When the user last did something (input or an edit): Cache Frames When Idle waits for
/// a second of quiet. (time, project revision then)
last_activity: (f64, u64),
pub(crate) toast: Option<(String, f64)>,
/// Texture of the last CPU frame shown in the viewer and the key it came from.
pub(crate) viewer_tex: Option<(egui::TextureHandle, FrameKey)>,
@ -253,6 +256,7 @@ impl EffectcraftApp {
pending_screenshots: vec![],
next_token: 1,
last_ui_time: 0.0,
last_activity: (0.0, 0),
toast: None,
viewer_tex: None,
viewer_native: None,
@ -842,6 +846,7 @@ impl EffectcraftApp {
ctx.request_repaint();
}
if !self.playback.playing {
self.cache_when_idle(ctx, &c, &series, now, (wa, wb));
return;
}
let snap = |f: i64| wa + (f - wa).div_euclid(step) * step;
@ -919,6 +924,46 @@ impl EffectcraftApp {
ctx.request_repaint();
}
/// Composition ▸ Preview ▸ Cache Frames When Idle: after a second without input or edits,
/// render the work area's frames (from the current time on, then from its start) in the
/// background, a few at a time, until it is cached or the RAM preview budget is full.
fn cache_when_idle(&mut self, ctx: &egui::Context, comp: &effectcraft_engine::project::Comp, series: &FrameKey, now: f64, (wa, wb): (i64, i64)) {
let active = ctx.input(|i| !i.events.is_empty() || i.pointer.any_down() || i.pointer.is_moving());
if active || self.last_activity.1 != self.session.revision {
self.last_activity = (now, self.session.revision);
}
if !self.ui.cache_when_idle || self.frames.urgent_pending() {
return;
}
let quiet = now - self.last_activity.0;
if quiet < 1.0 {
ctx.request_repaint_after(std::time::Duration::from_secs_f64(1.0 - quiet));
return;
}
let cur = comp.frame_rate.frame_at(self.session.time()).clamp(wa, wb);
let n = wb - wa + 1;
let fit = self.frames_that_fit(comp, series.scale as f64 / 1000.0).min(n);
let slots = (self.frames_parallelism() / 2).max(1);
let mut queued = self.frames.inflight();
let scale = series.scale as f64 / 1000.0;
let mut pending = false;
for k in 0..fit {
let f = wa + (cur - wa + k).rem_euclid(n);
if self.frames.is_cached(&FrameKey { frame: f, ..*series }) {
continue;
}
pending = true;
if queued >= slots {
break;
}
self.request_frame(ItemId(series.comp), f, scale);
queued += 1;
}
if pending {
ctx.request_repaint_after(std::time::Duration::from_millis(100));
}
}
/// How many viewer frames of `comp` at `scale` the RAM preview budget holds (90 % of it,
/// at least one).
fn frames_that_fit(&self, comp: &effectcraft_engine::project::Comp, scale: f64) -> i64 {

View File

@ -231,3 +231,29 @@ fn achieved_frame_rate_and_real_time() {
assert_eq!(slow.real_time(), Some(false));
assert_eq!(slow.shown_at.len(), 13, "only the last second counts");
}
#[test]
fn cache_frames_when_idle_fills_the_work_area() {
let mut h = harness();
let cid = h.state().session.active_comp_id().unwrap();
let cached = |h: &Harness<'_, EffectcraftApp>| h.state().frames.cached_frames(&h.state().shown_series(cid)).len();
// Off: only a few frames ahead of the current time are prefetched (the work area is the
// whole 240 frames).
for _ in 0..120 {
h.step();
std::thread::sleep(std::time::Duration::from_millis(2));
}
let before = cached(&h);
assert!(before < 240, "{before}");
invoke(&mut h, "playback.cacheWhenIdle", json!({"value": true}));
let mut n = 0;
for _ in 0..4000 {
h.step();
n = cached(&h);
if n >= 240 {
break;
}
std::thread::sleep(std::time::Duration::from_millis(2));
}
assert_eq!(n, 240, "the work area is cached while idle");
}

View File

@ -47,7 +47,9 @@ Commands (CLI, MCP, control channel):
- `autoSave.saveOnRenderStart`: Save When Starting Render Queue
- `composition.showRenderingProgress`: Show Rendering Progress in Info Panel and Flowchart
- `previews.adaptiveResolutionLimit`: Adaptive Resolution Limit
- `previews.cacheFramesWhenIdle`: Cache Frames When Idle
- `previews.cacheFramesWhenIdle`: Cache Frames When Idle (Composition ▸ Preview): after a second
without input or edits, the viewer renders the work area into the RAM preview in the
background, from the current time on, until it is cached or the budget is full
- `previews.fastPreviews`: Fast Previews (Draft 3D, Faster Effects)
- `appearance.theme`: Theme
- `appearance.brightness`: Brightness