mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
- Footage sources: the cancellable wrapper (#784) forwards frame_with (#752 float curves) and problem (#887); problem's default lives on the trait. - Media: a called-off decode (#784) is not a footage problem (#887). - Effect Controls rename (#953): Escape cancels (egui drops focus on Escape the same frame), the Rename item is translated; regression test. - i18n (#873): drop the /out-i18n/ ignore rule and the note about an uncommitted script. - Tests: text range selector caps (#926) re-borrow the layer; the RAM preview test (#793) sets the comp's own Resolution (#462). Co-Authored-By: Claude Opus 5.5 <[email protected]>
151 lines
7.6 KiB
Rust
151 lines
7.6 KiB
Rust
//! RAM preview frames are keyed by content: editing one comp keeps the cached frames of another,
|
|
//! and undo finds the frames of the state it returns to.
|
|
|
|
use effectcraft_engine::Session;
|
|
use effectcraft_engine::project::ItemId;
|
|
use effectcraft_ui_egui::EffectcraftApp;
|
|
use egui_kittest::Harness;
|
|
use serde_json::json;
|
|
|
|
fn settle(h: &mut Harness<'_, EffectcraftApp>) {
|
|
for _ in 0..600 {
|
|
h.step();
|
|
if h.state().frames.inflight() == 0 && h.state().frames.last_ms.lock().map(|v| *v > 0.0).unwrap_or(false) {
|
|
break;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
}
|
|
h.run_steps(2);
|
|
}
|
|
|
|
fn shown_cached(h: &Harness<'_, EffectcraftApp>, comp: u64) -> bool {
|
|
let app = h.state();
|
|
let key = effectcraft_ui_egui::frames::FrameKey { frame: 0, ..app.shown_series(ItemId(comp)) };
|
|
app.frames.is_cached(&key)
|
|
}
|
|
|
|
#[test]
|
|
fn an_edit_in_another_comp_keeps_the_cached_frames_and_undo_finds_them_again() {
|
|
let mut s = Session::default();
|
|
let b = s.execute("comp.new", json!({"name": "B", "width": 64, "height": 36, "duration": 1})).unwrap()["comp"].as_u64().unwrap();
|
|
let a = s.execute("comp.new", json!({"name": "A", "width": 64, "height": 36, "duration": 1})).unwrap()["comp"].as_u64().unwrap();
|
|
s.execute("layer.newSolid", json!({"color": "#3080ff"})).unwrap();
|
|
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(s));
|
|
settle(&mut h);
|
|
assert!(shown_cached(&h, a), "A's frame rendered");
|
|
// Edit B (open it, add a layer), then come back to A: its frame is still in RAM.
|
|
h.state_mut().session.execute("comp.open", json!({"comp": b})).unwrap();
|
|
h.state_mut().session.execute("layer.newSolid", json!({"color": "#ff8030"})).unwrap();
|
|
settle(&mut h);
|
|
let b_frame = h.state().shown_series(ItemId(b));
|
|
h.state_mut().session.execute("comp.open", json!({"comp": a})).unwrap();
|
|
assert!(shown_cached(&h, a), "A's frame survived the edit in B");
|
|
// Edit A, then undo: the frame of the state undo returns to is found again.
|
|
let before = h.state().shown_series(ItemId(a));
|
|
h.state_mut().session.execute("layer.newSolid", json!({"color": "#20c040"})).unwrap();
|
|
settle(&mut h);
|
|
assert_ne!(h.state().shown_series(ItemId(a)), before, "A's content changed");
|
|
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
|
assert_eq!(h.state().shown_series(ItemId(a)), before);
|
|
assert!(shown_cached(&h, a), "undo finds the earlier frame");
|
|
// B's frame is still there too.
|
|
assert!(h.state().frames.is_cached(&effectcraft_ui_egui::frames::FrameKey { frame: 0, ..b_frame }));
|
|
}
|
|
|
|
/// #284: the Timeline shows the cached frames as a green line under the work area bar, from
|
|
/// the first frame rendered on.
|
|
#[test]
|
|
fn the_timeline_shows_the_cached_frames_in_green() {
|
|
let mut s = Session::default();
|
|
s.execute("comp.new", json!({"name": "Cache", "width": 64, "height": 36, "duration": 2})).unwrap();
|
|
s.execute("layer.newSolid", json!({"color": "#3080ff"})).unwrap();
|
|
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(s));
|
|
settle(&mut h);
|
|
h.run_steps(2);
|
|
let green = h.state().tokens.cache_green;
|
|
let bar = h.state().auto.find("timeline.workArea.bar").unwrap().rect;
|
|
fn rects(s: &egui::Shape, out: &mut Vec<egui::epaint::RectShape>) {
|
|
match s {
|
|
egui::Shape::Rect(r) => out.push(r.clone()),
|
|
egui::Shape::Vec(v) => v.iter().for_each(|s| rects(s, out)),
|
|
_ => {}
|
|
}
|
|
}
|
|
let mut all = vec![];
|
|
for c in &h.output().shapes {
|
|
rects(&c.shape, &mut all);
|
|
}
|
|
let line: Vec<egui::Rect> = all.iter().filter(|r| r.fill == green).map(|r| r.rect).collect();
|
|
assert!(!line.is_empty(), "a green cache line is drawn");
|
|
// Under the work area bar, at its start (frame 0 is cached), a visible width.
|
|
let under = |r: &egui::Rect| r.min.y >= bar[1] + bar[3] - 1.0 && r.max.y <= bar[1] + bar[3] + 8.0;
|
|
assert!(line.iter().any(|r| under(r) && (r.min.x - bar[0]).abs() < 2.0 && r.width() >= 1.0), "{line:?} bar {bar:?}");
|
|
}
|
|
|
|
/// 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");
|
|
}
|
|
|
|
/// #595: at Third (and other resolutions that aren't a whole number of thousandths) the cache
|
|
/// bars find the rendered frames. The series they count was rebuilt from the frame key's scale
|
|
/// (333 → 0.333), whose render options hashed apart from the frames rendered at 0.3333….
|
|
#[test]
|
|
fn cached_frames_are_counted_at_one_third_resolution() {
|
|
use effectcraft_ui_egui::state::Resolution;
|
|
for res in [Resolution::Third, Resolution::Custom(7)] {
|
|
let mut s = Session::default();
|
|
let c = s.execute("comp.new", json!({"name": "Third", "width": 64, "height": 36, "duration": 1})).unwrap()["comp"].as_u64().unwrap();
|
|
s.execute("layer.newSolid", json!({"color": "#3080ff"})).unwrap();
|
|
// Each composition keeps its own Resolution (#417).
|
|
if let Some(comp) = std::sync::Arc::make_mut(&mut s.project).comp_mut(ItemId(c)) {
|
|
comp.resolution = res;
|
|
}
|
|
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(s));
|
|
settle(&mut h);
|
|
let series = h.state().shown_series(ItemId(c));
|
|
assert_eq!(series.scale, (res.scale(1.0, 1.0) * 1000.0).round() as u32, "{res:?}");
|
|
assert!(shown_cached(&h, c), "{res:?}: the shown frame counts as cached");
|
|
assert!(!h.state().frames.cached_frames(&series).is_empty(), "{res:?}");
|
|
}
|
|
}
|