mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 07:38:24 +00:00
Preview: the start with sound never waits for ever (#947)
The startup hold waited for the current frame to be cached before anything else, so a frame that can never be cached held the first frame for ever: a zero RAM preview budget, or a single frame larger than the budget (which the comp-size estimate misses for Extended Viewer frames). It now gives up at once when the budget can't hold even one comp-sized frame, and otherwise after STARTUP_WAIT (2 s from pressing play): the sound starts from the first frame, and the preview goes on as a stalled one does. frame_bytes() is factored out of frames_that_fit() for it. Tests (ui_audio_scrub): with a budget of 0 or half a frame the sound starts on the first step; with an Extended Viewer frame larger than the whole budget (the estimate says one fits) the first frame holds for about two seconds, then the sound starts. Both wait for ever on the previous commit. The Extended Viewer setup is now shared by its two tests.
This commit is contained in:
1 parent
e519f21f45
commit
c75ea46c86
2 files changed
+79
-9
No files matched your search
@@ -52,6 +52,12 @@ const SCREENSHOT_TIMEOUT_S: f64 = 4.0;
|
||||
/// [`EffectcraftApp::editing`]).
|
||||
const EDIT_QUIET: f64 = 0.3;
|
||||
|
||||
/// Longest a preview with sound holds its first frame waiting for frames (seconds, #947). A frame
|
||||
/// that never gets cached (larger than the RAM preview, which the size estimate can't always
|
||||
/// tell) mustn't hold it for ever: the sound starts from the first frame anyway, and the preview
|
||||
/// goes on as a stalled one does.
|
||||
const STARTUP_WAIT: f64 = 2.0;
|
||||
|
||||
/// How many frames from the first a preview with sound waits for before it starts (#947): the
|
||||
/// prefetch's `ahead`, but no more than its `window` requests from the current frame (offsets
|
||||
/// `0..window`), and no more than fit in the RAM preview `budget` (90 %, as for the prefetch) at
|
||||
@@ -234,6 +240,8 @@ pub struct Playback {
|
||||
/// while they rendered and the sound started at the later frame, so the opening was lost.
|
||||
/// A stall later on still falls back to [`Playback::audio_held`].
|
||||
pub starting: bool,
|
||||
/// When the preview started (seconds): [`Playback::starting`] gives up after [`STARTUP_WAIT`].
|
||||
pub starting_since: f64,
|
||||
/// When the last frames were shown (seconds, the last second's worth): the achieved rate.
|
||||
pub shown_at: std::collections::VecDeque<f64>,
|
||||
}
|
||||
@@ -1022,6 +1030,7 @@ impl EffectcraftApp {
|
||||
audio_frame: (!plan.video).then_some(plan.first),
|
||||
audio_held: false,
|
||||
starting: false,
|
||||
starting_since: now,
|
||||
shown_at: Default::default(),
|
||||
};
|
||||
if !self.playback.caching {
|
||||
@@ -1298,7 +1307,10 @@ impl EffectcraftApp {
|
||||
let [w, h] = img.size();
|
||||
startup_frames(ahead, window, self.frames.budget(), w.saturating_mul(h).saturating_mul(4))
|
||||
});
|
||||
if !need.is_some_and(|n| self.cached_ahead(&p, &series, cur, n)) {
|
||||
// Don't wait for frames the RAM preview can't hold (not even one frame fits), nor for
|
||||
// longer than STARTUP_WAIT (a frame that never gets cached).
|
||||
let give_up = (self.frames.budget() as f64) < self.frame_bytes(&c, scale) || now - self.playback.starting_since >= STARTUP_WAIT;
|
||||
if !give_up && !need.is_some_and(|n| self.cached_ahead(&p, &series, cur, n)) {
|
||||
self.playback.waiting = true;
|
||||
ctx.request_repaint();
|
||||
return;
|
||||
@@ -1467,9 +1479,13 @@ impl EffectcraftApp {
|
||||
}
|
||||
|
||||
fn frames_that_fit(&self, comp: &effectcraft_engine::project::Comp, scale: f64) -> i64 {
|
||||
((self.frames.budget() as f64 * 0.9 / self.frame_bytes(comp, scale)) as i64).max(1)
|
||||
}
|
||||
|
||||
/// Bytes of one comp-sized viewer frame at `scale` (Extended Viewer frames are larger).
|
||||
fn frame_bytes(&self, comp: &effectcraft_engine::project::Comp, scale: f64) -> f64 {
|
||||
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)
|
||||
side(comp.width) * side(comp.height) * 4.0
|
||||
}
|
||||
|
||||
fn frames_parallelism(&self) -> usize {
|
||||
|
||||
@@ -327,11 +327,10 @@ fn startup_finishes_at_any_ram_preview_budget() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Extended Viewer frames render larger than the comp: a budget that would hold `ahead` comp-sized
|
||||
/// frames holds fewer of them, and the start waits only for as many as really fit (#947).
|
||||
#[test]
|
||||
fn startup_finishes_when_extended_viewer_frames_are_larger() {
|
||||
let fake = Fake::default();
|
||||
/// An audible 160×120 comp with a 3D layer and Draft 3D, Extended Viewer on: its viewer frames
|
||||
/// render the pasteboard too, larger than the comp. Returns the harness once the first frame has
|
||||
/// rendered, the comp, and that frame's size.
|
||||
fn extended_viewer(fake: &Fake) -> (Harness<'static, EffectcraftApp>, ItemId, [usize; 2]) {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "E", "width": 160, "height": 120, "frameRate": 30, "duration": 8})).unwrap();
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
@@ -344,7 +343,6 @@ fn startup_finishes_when_extended_viewer_frames_are_larger() {
|
||||
let mut app = EffectcraftApp::new(s);
|
||||
let f = fake.clone();
|
||||
app.hooks.audio_device = Some(Box::new(move |_| Some(Box::new(f.clone()) as Box<dyn AudioDevice>)));
|
||||
let ahead = (app.frames.parallelism() * 2).max(4);
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(1.0 / 30.0).build_eframe(|_| app);
|
||||
// The viewer shows the pasteboard: frames render the extended region.
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
|
||||
@@ -358,6 +356,16 @@ fn startup_finishes_when_extended_viewer_frames_are_larger() {
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
};
|
||||
assert!(h.state().ui.viewer.extended.is_some(), "Extended Viewer is on");
|
||||
(h, cid, size)
|
||||
}
|
||||
|
||||
/// Extended Viewer frames render larger than the comp: a budget that would hold `ahead` comp-sized
|
||||
/// frames holds fewer of them, and the start waits only for as many as really fit (#947).
|
||||
#[test]
|
||||
fn startup_finishes_when_extended_viewer_frames_are_larger() {
|
||||
let fake = Fake::default();
|
||||
let (mut h, cid, size) = extended_viewer(&fake);
|
||||
let ahead = (h.state().frames.parallelism() * 2).max(4);
|
||||
let (comp_bytes, real_bytes) = (160 * 120 * 4, size[0] * size[1] * 4);
|
||||
// Room for `ahead` comp-sized frames (and a half), which is fewer real ones.
|
||||
let budget = ((ahead as f64 + 0.5) * comp_bytes as f64 / 0.9) as usize;
|
||||
@@ -450,3 +458,49 @@ fn a_started_preview_follows_its_sound() {
|
||||
assert!(h.state().audio.is_none() && h.state().playback.audio_held && h.state().playback.playing, "a stall stops the sound");
|
||||
*device.0.lock().unwrap() = true;
|
||||
}
|
||||
|
||||
/// A RAM preview that can't hold even one frame (none, or half of one): the start doesn't wait
|
||||
/// for frames that can never be cached; the sound starts at once from the first frame.
|
||||
#[test]
|
||||
fn startup_does_not_wait_when_the_ram_preview_holds_no_frame() {
|
||||
for budget in [0, 9216 / 2] {
|
||||
let fake = Fake::default();
|
||||
let (app, _) = setup_long(true, &fake, 8.0);
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(1.0 / 30.0).build_eframe(|_| app);
|
||||
h.run_steps(2);
|
||||
h.state().frames.set_budget(budget);
|
||||
h.state().frames.clear();
|
||||
let now = h.ctx.input(|i| i.time);
|
||||
h.state_mut().play(now);
|
||||
h.step();
|
||||
assert!(!h.state().playback.starting, "budget {budget}: no wait");
|
||||
assert!(fake.0.lock().unwrap().0.is_some(), "budget {budget}: the sound started");
|
||||
assert!(h.state().playback.playing);
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame that never gets cached although the estimate says one fits (an Extended Viewer frame
|
||||
/// larger than the whole budget): the start gives up after a short wait instead of holding the
|
||||
/// first frame for ever, and the sound starts from it.
|
||||
#[test]
|
||||
fn startup_gives_up_after_a_short_wait() {
|
||||
let fake = Fake::default();
|
||||
let (mut h, cid, size) = extended_viewer(&fake);
|
||||
let (comp_bytes, real_bytes) = (160 * 120 * 4, size[0] * size[1] * 4);
|
||||
assert!(real_bytes > comp_bytes * 2, "Extended Viewer frames ({size:?}) are larger than the comp");
|
||||
// Room for a comp-sized frame but not a real one: the current frame can never be cached.
|
||||
let budget = (comp_bytes + real_bytes) / 2;
|
||||
h.state().frames.set_budget(budget);
|
||||
h.state().frames.clear();
|
||||
let start = h.ctx.input(|i| i.time);
|
||||
h.state_mut().play(start);
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
|
||||
while h.state().playback.starting && std::time::Instant::now() < deadline {
|
||||
assert_eq!(frame(&h, cid), 0, "it holds the first frame while it waits");
|
||||
h.step();
|
||||
}
|
||||
let waited = h.ctx.input(|i| i.time) - start;
|
||||
assert!(!h.state().playback.starting, "it gave up waiting");
|
||||
assert!((1.9..4.0).contains(&waited), "after about two seconds ({waited:.2} s)");
|
||||
assert!(fake.0.lock().unwrap().0.is_some(), "and the sound started");
|
||||
}
|
||||
Reference in new issue
Block a user