mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Fix #65: the viewer keeps up while a layer is dragged
While paused, the app prefetched the frames around the current one as soon as the viewer's frame finished. During a drag every step changes the project, so those frames were stale at the next step, and they held the CPU and GPU the next viewer frame needed: on the demo project a drag step took 102 ms on average to show (up to 413 ms) though a frame renders in 20 ms. Paused prefetch now waits until edits pause for 0.3 s: 16 ms a step (11 ms render). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
75640044b8
commit
1b8780f6da
2 files changed
+66
-1
No files matched your search
@@ -39,6 +39,9 @@ use state::UiState;
|
||||
use theme::Tokens;
|
||||
|
||||
const SCREENSHOT_TIMEOUT_S: f64 = 4.0;
|
||||
/// Seconds without a project change before paused prefetch resumes (see
|
||||
/// [`EffectcraftApp::editing`]).
|
||||
const EDIT_QUIET: f64 = 0.3;
|
||||
|
||||
/// Modal dialogs.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -184,6 +187,8 @@ pub struct EffectcraftApp {
|
||||
/// 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),
|
||||
/// When the project last changed, and its revision then (see [`Self::editing`]).
|
||||
last_edit: (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)>,
|
||||
@@ -263,6 +268,7 @@ impl EffectcraftApp {
|
||||
next_token: 1,
|
||||
last_ui_time: 0.0,
|
||||
last_activity: (0.0, 0),
|
||||
last_edit: (0.0, 0),
|
||||
toast: None,
|
||||
viewer_tex: None,
|
||||
passive_tex: std::collections::HashMap::new(),
|
||||
@@ -797,11 +803,16 @@ impl EffectcraftApp {
|
||||
// Prefetch ahead.
|
||||
let cur = fr.frame_at(self.session.time());
|
||||
// While paused (scrubbing, editing) the viewer's frame comes first: prefetch only a few
|
||||
// frames, and only once it is done, so prefetch never delays what is on screen.
|
||||
// frames, only once it is done, and not while edits keep coming (a layer dragged in the
|
||||
// viewer): those frames would be stale at the next step and would hold the CPU and GPU
|
||||
// the next viewer frame needs.
|
||||
let ahead = if self.playback.playing {
|
||||
(self.frames_parallelism() * 2).max(4) as i64
|
||||
} else if self.frames.urgent_pending() {
|
||||
0
|
||||
} else if let Some(wait) = self.editing(now) {
|
||||
ctx.request_repaint_after(std::time::Duration::from_secs_f64(wait));
|
||||
0
|
||||
} else {
|
||||
(self.frames_parallelism() / 2).max(2) as i64
|
||||
};
|
||||
@@ -963,6 +974,16 @@ impl EffectcraftApp {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
|
||||
/// While the project keeps changing (an edit within [`EDIT_QUIET`] seconds), how long until
|
||||
/// it counts as settled.
|
||||
fn editing(&mut self, now: f64) -> Option<f64> {
|
||||
if self.last_edit.1 != self.session.revision {
|
||||
self.last_edit = (now, self.session.revision);
|
||||
}
|
||||
let left = EDIT_QUIET - (now - self.last_edit.0);
|
||||
(left > 0.0).then_some(left)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
|
||||
@@ -51,3 +51,47 @@ fn an_edit_in_another_comp_keeps_the_cached_frames_and_undo_finds_them_again() {
|
||||
// B's frame is still there too.
|
||||
assert!(h.state().frames.is_cached(&effectcraft_ui_egui::frames::FrameKey { frame: 0, ..b_frame }));
|
||||
}
|
||||
|
||||
/// Issue #65: while edits keep coming (a layer dragged in the viewer), only the viewer's frame
|
||||
/// renders; prefetching the frames around it waits until the edits pause (they would be stale at
|
||||
/// the next step, and would hold the CPU and GPU the next viewer frame needs), then resumes.
|
||||
#[test]
|
||||
fn prefetch_waits_while_edits_keep_coming() {
|
||||
let mut s = Session::default();
|
||||
let c = s.execute("comp.new", json!({"name": "Drag", "width": 64, "height": 36, "duration": 2})).unwrap()["comp"].as_u64().unwrap();
|
||||
let solid = s.execute("layer.newSolid", json!({"color": "#3080ff", "width": 16, "height": 16})).unwrap()["layer"].as_u64().unwrap();
|
||||
// Short steps: input time advances 50 ms per frame.
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(0.05).build_eframe(|_| EffectcraftApp::new(s));
|
||||
settle(&mut h);
|
||||
let frame = |h: &Harness<'_, EffectcraftApp>| {
|
||||
let app = h.state();
|
||||
let comp = app.session.project.comp(ItemId(c)).unwrap();
|
||||
comp.frame_rate.frame_at(app.session.time())
|
||||
};
|
||||
for i in 1..=8 {
|
||||
let v = json!([20.0 + 2.0 * i as f64, 18.0, 0.0]);
|
||||
h.state_mut().session.execute("prop.set", json!({"layer": solid, "path": "transform/position", "value": v, "merge": "drag"})).unwrap();
|
||||
h.step();
|
||||
// The viewer's frame renders...
|
||||
for _ in 0..500 {
|
||||
if !h.state().frames.urgent_pending() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
}
|
||||
h.step();
|
||||
// ...and nothing else is queued or cached for this state of the comp.
|
||||
let series = h.state().shown_series(ItemId(c));
|
||||
assert_eq!(h.state().frames.cached_frames(&series), vec![frame(&h)], "edit {i}");
|
||||
assert_eq!(h.state().frames.inflight(), 0, "edit {i}");
|
||||
}
|
||||
// The edits pause: the frames around the current one fill in.
|
||||
settle(&mut h);
|
||||
for _ in 0..20 {
|
||||
h.step();
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
}
|
||||
settle(&mut h);
|
||||
let series = h.state().shown_series(ItemId(c));
|
||||
assert!(h.state().frames.cached_frames(&series).len() > 1, "prefetch resumed");
|
||||
}
|
||||
Reference in new issue
Block a user