mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 19:47:12 -06:00
M4.6: previews: Auto resolution keeps its scale while playing, Cache Before Playback plays what fits, achieved frame rate in the Info panel
- Viewer Resolution Auto rendered at half its scale while a preview played, so frames cached while scrubbing were not played and frames cached by the preview were re-rendered once it stopped. Auto now follows the zoom alike playing and paused (Fast Previews still lowers it while dragging). - Cache Before Playback waited for every frame of the range: a range larger than the RAM preview budget evicted its first frames and never started. The range is now cut to the frames that fit (90 % of Settings ▸ Memory & CPU's budget) with a message, then cached and played. - The Info panel shows the frame rate a playing preview achieves and whether it is real time (`Playback::achieved_fps` / `real_time`, automation id `info.previewFps`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5648fdb1c2
commit
6214aecdb9
@@ -127,6 +127,33 @@ pub struct Playback {
|
||||
pub restore_max: Option<Option<PanelKind>>,
|
||||
/// Audio-only previews: the frame under the audible sample (the current time stays put).
|
||||
pub audio_frame: Option<i64>,
|
||||
/// When the last frames were shown (seconds, the last second's worth): the achieved rate.
|
||||
pub shown_at: std::collections::VecDeque<f64>,
|
||||
}
|
||||
|
||||
impl Playback {
|
||||
/// A new frame went on screen at `now`.
|
||||
pub fn frame_shown(&mut self, now: f64) {
|
||||
self.shown += 1;
|
||||
self.shown_at.push_back(now);
|
||||
while self.shown_at.front().is_some_and(|t| now - t > 1.0) {
|
||||
self.shown_at.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
/// Frames per second shown over the last second while the preview plays (`None` before two
|
||||
/// frames were shown, while caching, or stopped).
|
||||
pub fn achieved_fps(&self) -> Option<f64> {
|
||||
let (a, b) = (self.shown_at.front()?, self.shown_at.back()?);
|
||||
(self.playing && !self.caching && self.shown_at.len() > 1 && b > a).then(|| (self.shown_at.len() - 1) as f64 / (b - a))
|
||||
}
|
||||
|
||||
/// The preview keeps up with its frame rate (within 5 %; previews that skip frames count
|
||||
/// the frames they mean to show).
|
||||
pub fn real_time(&self) -> Option<bool> {
|
||||
let step = self.plan.map_or(1, |p| p.step).max(1) as f64;
|
||||
self.achieved_fps().map(|f| f * step >= self.fps * 0.95)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EffectcraftApp {
|
||||
@@ -462,12 +489,8 @@ impl EffectcraftApp {
|
||||
};
|
||||
return r.scale(zoom, ppp);
|
||||
}
|
||||
if self.playback.playing && self.ui.viewer.res == state::Resolution::Auto {
|
||||
// Keep previews snappy: half the displayed size, but no lower than Settings ▸
|
||||
// Previews ▸ Adaptive Resolution Limit.
|
||||
let full = self.ui.viewer.res.scale(zoom, ppp);
|
||||
return full.min((full * 0.5).max(self.session.prefs.adaptive_limit()));
|
||||
}
|
||||
// Auto follows the zoom alike while playing and paused, so frames cached while scrubbing
|
||||
// play back, and frames cached by a preview stay on screen when it stops.
|
||||
let full = self.ui.viewer.res.scale(zoom, ppp);
|
||||
// Fast Previews ▸ Adaptive Resolution / Fast Draft: lower resolution while dragging.
|
||||
let (_, k) = self.session.state.viewer.fast_previews.render(self.ui.viewer.interacting);
|
||||
@@ -611,6 +634,7 @@ impl EffectcraftApp {
|
||||
layer_controls: preset.include_layer_controls,
|
||||
restore_max,
|
||||
audio_frame: (!plan.video).then_some(plan.first),
|
||||
shown_at: Default::default(),
|
||||
};
|
||||
if !self.playback.caching {
|
||||
self.start_audio(fr.tick_of(plan.first));
|
||||
@@ -751,9 +775,20 @@ impl EffectcraftApp {
|
||||
if self.playback.caching
|
||||
&& let Some(p) = plan
|
||||
{
|
||||
// Cache Before Playback: the whole range in play order, then start the clock.
|
||||
// Cache Before Playback: the range in play order, then start the clock. A range
|
||||
// larger than the RAM preview budget is cut to the frames that fit (caching more
|
||||
// would evict the first ones and never finish).
|
||||
let mut all = true;
|
||||
let n = (p.end - p.start) / p.step + 1;
|
||||
let fit = self.frames_that_fit(&c, scale);
|
||||
if n > fit {
|
||||
let mut cut = p;
|
||||
(cut.start, cut.end) = (p.first, (p.first + (fit - 1) * p.step).min(p.end));
|
||||
self.playback.plan = Some(cut);
|
||||
self.toast = Some((format!("Preview cut to {fit} of {n} frames: no more fit in the RAM preview (Settings ▸ Memory & CPU)"), now));
|
||||
ctx.request_repaint();
|
||||
return;
|
||||
}
|
||||
let order: Vec<i64> = (0..n).filter_map(|k| p.frame_after(p.first, k)).collect();
|
||||
for f in order {
|
||||
if self.frames.is_cached(&FrameKey { frame: f, ..series }) {
|
||||
@@ -829,7 +864,7 @@ impl EffectcraftApp {
|
||||
self.playback.waiting = !self.frames.is_cached(&FrameKey { frame: target, ..series });
|
||||
if target != cur {
|
||||
self.session.set_time(fr.tick_of(target));
|
||||
self.playback.shown += 1;
|
||||
self.playback.frame_shown(now);
|
||||
}
|
||||
}
|
||||
ctx.request_repaint();
|
||||
@@ -869,7 +904,7 @@ impl EffectcraftApp {
|
||||
self.playback.waiting = false;
|
||||
if target != cur {
|
||||
self.session.set_time(fr.tick_of(target));
|
||||
self.playback.shown += 1;
|
||||
self.playback.frame_shown(now);
|
||||
}
|
||||
} else {
|
||||
// Not cached yet: hold the clock at the current frame (cache first, then play).
|
||||
@@ -884,6 +919,14 @@ impl EffectcraftApp {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let side = |n: u32| (n as f64 * scale).ceil().max(1.0);
|
||||
let frame = side(comp.width) * side(comp.height) * 4.0;
|
||||
((self.frames.budget() as f64 * 0.9 / frame) as i64).max(1)
|
||||
}
|
||||
|
||||
fn frames_parallelism(&self) -> usize {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
// frame workers render one frame each; without them frames render one at a time
|
||||
|
||||
@@ -87,6 +87,13 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
ly += 18.0;
|
||||
}
|
||||
}
|
||||
// While a preview plays: the frame rate it achieves and whether that is real time.
|
||||
if let (Some(fps), Some(rt)) = (app.playback.achieved_fps(), app.playback.real_time()) {
|
||||
let note = if rt { "real-time" } else { "not real-time" };
|
||||
p.text(pos2(x0, ly), Align2::LEFT_CENTER, format!("{fps:.2} fps ({note})"), Tokens::ui(11.5), if rt { t.text_dim } else { t.warning });
|
||||
app.auto.add("info.previewFps", Rect::from_min_size(pos2(x0, ly - 8.0), vec2(200.0, 16.0)), &format!("{fps:.2} fps ({note})"));
|
||||
ly += 18.0;
|
||||
}
|
||||
// Settings ▸ Composition ▸ Show Rendering Progress in Info Panel.
|
||||
if app.session.prefs.composition.show_rendering_progress {
|
||||
let ms = app.frames.last_ms.lock().map(|v| *v).unwrap_or(0.0);
|
||||
|
||||
@@ -184,3 +184,50 @@ fn align_panel_runs_align_and_distribute() {
|
||||
click(&mut h, "distribute.hcenter");
|
||||
assert_eq!(pos(&h), p);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_before_playback_plays_what_fits_in_ram() {
|
||||
let mut h = harness();
|
||||
set(&mut h, json!({"shortcut": "spacebar", "cacheBeforePlayback": true, "range": "entireDuration", "playFrom": "rangeStart"}));
|
||||
h.run_steps(2);
|
||||
// Room for about 12 full-size frames: the 240-frame range can't all be cached.
|
||||
h.state().frames.set_budget(320 * 180 * 4 * 12);
|
||||
invoke(&mut h, "playback.toggle", json!({}));
|
||||
assert!(h.state().playback.caching);
|
||||
for _ in 0..600 {
|
||||
h.step();
|
||||
if !h.state().playback.caching {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
let st = &h.state().playback;
|
||||
assert!(st.playing && !st.caching, "caching finished and the preview plays");
|
||||
let pl = st.plan.unwrap();
|
||||
assert!(pl.start == 0 && pl.end < 239 && pl.end >= 9, "cut to what fits: {}..{}", pl.start, pl.end);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_resolution_is_the_same_playing_and_paused() {
|
||||
let mut h = harness();
|
||||
let paused = h.state().viewer_scale(0.5, 2.0);
|
||||
h.state_mut().playback.playing = true;
|
||||
assert_eq!(h.state().viewer_scale(0.5, 2.0), paused);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn achieved_frame_rate_and_real_time() {
|
||||
let mut p = effectcraft_ui_egui::Playback { playing: true, fps: 24.0, ..Default::default() };
|
||||
assert_eq!(p.achieved_fps(), None);
|
||||
for i in 0..25 {
|
||||
p.frame_shown(i as f64 / 24.0);
|
||||
}
|
||||
assert!((p.achieved_fps().unwrap() - 24.0).abs() < 1e-9);
|
||||
assert_eq!(p.real_time(), Some(true));
|
||||
let mut slow = effectcraft_ui_egui::Playback { playing: true, fps: 24.0, ..Default::default() };
|
||||
for i in 0..13 {
|
||||
slow.frame_shown(i as f64 / 12.0);
|
||||
}
|
||||
assert_eq!(slow.real_time(), Some(false));
|
||||
assert_eq!(slow.shown_at.len(), 13, "only the last second counts");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user