mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-10 20:41:25 +00:00
Preview: the pace measures its lead over no more frames than the RAM preview holds
The pace counted cached frames among the next ahead x 2, but the prefetch never requests more than the RAM preview holds (offsets 0..fit-1 from the current frame). With room for only a few frames the lead could never fill, so a preview that rendered every frame at once was slowed to a fraction of its frame rate, with Lower Resolution When Playback Can't Keep Up off (the default). The lead is now counted over at most the frames the prefetch keeps after the current one, and with none of those (room for at most two frames) the clock runs in real time as before pacing.
This commit is contained in:
1 parent
e356e4adf5
commit
114fbaed63
2 files changed
+83
-6
No files matched your search
@@ -54,6 +54,17 @@ fn pace_for(prev: f64, lead: i64, target: i64, fps: f64, dt: f64) -> f64 {
|
||||
prev + (raw - prev) * k
|
||||
}
|
||||
|
||||
/// How many frames after the current one the pace measures its lead over: twice the prefetch
|
||||
/// depth `ahead`, but only frames the RAM preview can keep cached together with the current one.
|
||||
/// `prefetch_playback` requests offsets `0..fit_ahead` from the current frame (the current frame
|
||||
/// and `fit_ahead - 1` after it), so a lead counted over more frames than that could never fill:
|
||||
/// a preview that renders every frame at once would still be slowed down to a fraction of its
|
||||
/// frame rate. 0 when not even one frame after the current one is prefetched: the pace can't
|
||||
/// measure anything, and the clock runs in real time (holding on frames that aren't cached).
|
||||
fn pace_horizon(ahead: i64, fit_ahead: i64) -> i64 {
|
||||
(ahead * 2).max(1).min(fit_ahead - 1).max(0)
|
||||
}
|
||||
|
||||
/// Cached frames (`cached`, sorted) among the next `max` frames `after(1..=max)` plays, and how
|
||||
/// many frames it plays (`after` is `None` past the end). Counted, not the consecutive run: one
|
||||
/// late frame (a GPU warm-up frame, a heavy one) with frames cached beyond it holds the clock
|
||||
@@ -145,7 +156,7 @@ impl EffectcraftApp {
|
||||
if self.advance_audio_clock(&pass, step) {
|
||||
return;
|
||||
}
|
||||
self.advance_frame_clock(&pass, step);
|
||||
self.advance_frame_clock(&pass, step, fit_ahead);
|
||||
}
|
||||
|
||||
/// While paused (scrubbing, editing) the viewer's frame comes first: prefetch only a few
|
||||
@@ -428,7 +439,7 @@ impl EffectcraftApp {
|
||||
/// frame (or holds when it is not cached yet). While frames arrive slower than the frame
|
||||
/// rate it runs at their pace ([`pace_for`]): every frame shows, evenly, as After Effects
|
||||
/// plays a preview it hasn't cached yet; real time again once the frames ahead are cached.
|
||||
fn advance_frame_clock(&mut self, pass: &PlaybackPass<'_>, step: i64) {
|
||||
fn advance_frame_clock(&mut self, pass: &PlaybackPass<'_>, step: i64, fit_ahead: i64) {
|
||||
let PlaybackPass { ctx, now, plan, video, frame_rate: fr, cur, series, .. } = *pass;
|
||||
let FrameWindow { last: wb, .. } = pass.window;
|
||||
// The plan carries the rate the preview was started with (and `play_with` copies it to
|
||||
@@ -439,10 +450,16 @@ impl EffectcraftApp {
|
||||
self.playback.paced_at = Some(now);
|
||||
if video {
|
||||
// The lead counts out of twice the prefetch depth: the prefetch keeps that many
|
||||
// frames coming (or the frames left before the end), so a full lead means rendering keeps up.
|
||||
let (lead, target) = self.cached_lead(pass, (pass.ahead * 2).max(1));
|
||||
// frames coming (or the frames left before the end), so a full lead means rendering
|
||||
// keeps up. No more than the RAM preview holds ([`pace_horizon`]).
|
||||
let horizon = pace_horizon(pass.ahead, fit_ahead);
|
||||
let prev = if self.playback.pace > 0.0 { self.playback.pace } else { fps };
|
||||
let pace = pace_for(prev, lead, target, fps, dt);
|
||||
let pace = if horizon > 0 {
|
||||
let (lead, target) = self.cached_lead(pass, horizon);
|
||||
pace_for(prev, lead, target, fps, dt)
|
||||
} else {
|
||||
fps
|
||||
};
|
||||
self.playback.pace = pace;
|
||||
if pace < fps * 0.8 {
|
||||
self.step_down(now, pass.scale);
|
||||
@@ -662,7 +679,26 @@ impl EffectcraftApp {
|
||||
|
||||
#[cfg(test)]
|
||||
mod pace_tests {
|
||||
use super::{lead_of, pace_for};
|
||||
use super::{lead_of, pace_for, pace_horizon};
|
||||
|
||||
/// The lead is measured over no more frames than the RAM preview keeps ahead of the current
|
||||
/// one, so a full cache plays in real time. (It counted out of twice the prefetch depth: with
|
||||
/// room for 4 frames, at most 2 after the current one were ever cached, and a preview that
|
||||
/// rendered at once played at a quarter of its frame rate.)
|
||||
#[test]
|
||||
fn the_pace_horizon_fits_in_the_ram_preview() {
|
||||
// A roomy budget: twice the prefetch depth.
|
||||
assert_eq!(pace_horizon(6, 400), 12);
|
||||
// Room for 4 frames: the prefetch requests offsets 0..3, so 2 after the current one.
|
||||
assert_eq!(pace_horizon(6, 3), 2);
|
||||
let (lead, target) = lead_of(|k| Some(10 + k), &[10, 11, 12], pace_horizon(6, 3));
|
||||
assert!((pace_for(30.0, lead, target, 30.0, 10.0) - 30.0).abs() < 1e-6, "a full cache plays in real time");
|
||||
// Room for the current frame only (or nothing): no lead to measure.
|
||||
assert_eq!(pace_horizon(6, 1), 0);
|
||||
assert_eq!(pace_horizon(6, 0), 0);
|
||||
// No prefetch depth: still the next frame.
|
||||
assert_eq!(pace_horizon(0, 400), 1);
|
||||
}
|
||||
|
||||
/// The pace follows the cached lead: real time with a full lead, a third of it with a third,
|
||||
/// never under an eighth, unchanged with nothing ahead, and smoothed over time.
|
||||
|
||||
@@ -395,3 +395,44 @@ fn playback_that_cannot_keep_up_steps_down_a_resolution() {
|
||||
stall(&mut h, 2.5);
|
||||
assert_eq!(h.state().viewer_scale(1.0, 1.0), 1.0);
|
||||
}
|
||||
|
||||
/// A preview that renders every frame at once plays in real time even when the RAM preview holds
|
||||
/// only a few frames. (The pace measured its lead over twice the prefetch depth, more frames than
|
||||
/// a small RAM preview keeps ahead, so such a preview was slowed to a fraction of its frame rate.)
|
||||
#[test]
|
||||
fn a_preview_that_keeps_up_plays_in_real_time_with_a_small_ram_preview() {
|
||||
let mut h = harness();
|
||||
let cid = h.state().session.active_comp_id().unwrap();
|
||||
let series = h.state().shown_series(cid);
|
||||
let s = series.scale as f64 / 1000.0;
|
||||
let frame = (320.0 * s).ceil() * (180.0 * s).ceil() * 4.0;
|
||||
// Room for 4 frames (90 % of 5): the prefetch keeps the current frame and 2 after it.
|
||||
h.state().frames.set_budget((frame * 5.0) as usize);
|
||||
h.state_mut().use_manual_clock();
|
||||
h.state_mut().session.set_time(Tick::ZERO);
|
||||
invoke(&mut h, "playback.toggle", json!({}));
|
||||
let fr = h.state().session.active_comp().unwrap().frame_rate;
|
||||
// Rendering keeps up: the frames ahead are rendered before the clock moves on.
|
||||
let settle = |h: &mut Harness<'_, EffectcraftApp>| {
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
|
||||
while std::time::Instant::now() < deadline {
|
||||
h.step();
|
||||
let cur = fr.frame_at(h.state().session.time());
|
||||
let cached = h.state().frames.cached_frames(&series);
|
||||
if (1..=2).all(|k| cached.contains(&((cur + k) % 240))) {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
panic!("frames ahead never cached");
|
||||
};
|
||||
for _ in 0..48 {
|
||||
settle(&mut h);
|
||||
h.state_mut().clock_mut().advance(1.0 / 24.0);
|
||||
h.step();
|
||||
}
|
||||
let pb = &h.state().playback;
|
||||
assert!(pb.playing);
|
||||
assert!(pb.pace >= 24.0 * 0.98, "plays at {} fps of 24", pb.pace);
|
||||
assert!(fr.frame_at(h.state().session.time()) >= 40, "the clock kept real time: {:?}", h.state().session.time());
|
||||
}
|
||||
Reference in new issue
Block a user