mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 19:18:14 -06:00
The cache bars count the frames of `shown_series`, which rebuilds the series from the frame key's scale in thousandths (333 -> 0.333). The key's `opts` hashed the render options with the f64 scale, and the frames had been rendered at 0.3333..., so the series never matched and the bars stayed empty at Third (and any resolution that isn't a whole number of thousandths, such as Custom 1/7). `opts_hash` now hashes the scale rounded to the key's thousandths, which the key carries anyway. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
148 lines
7.4 KiB
Rust
148 lines
7.4 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();
|
|
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(s));
|
|
h.state_mut().ui.viewer.res = res;
|
|
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:?}");
|
|
}
|
|
}
|