mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 19:18:14 -06:00
Preview: an edit keeps the cached frames outside the layers it changes (#1641)
The RAM preview keyed every frame by one identity of the whole comp, so any edit (a cut, a trim, a nudge) made every cached frame a miss and the whole comp recached. When only some of a comp's layers change, frames outside their In-Out span now move to the new identity. Settings changes, reorders, expressions, and edits that reach other layers (parents, track mattes, effect layer pickers) still recache everything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7c733f4cc9
commit
17edbbffff
@@ -839,7 +839,12 @@ impl Comp {
|
||||
/// Draws the same pixels as `o`: equal but for its layers' Audio, Lock and Shy switches, Hide
|
||||
/// Shy Layers and the viewer's Resolution.
|
||||
pub fn same_pixels(&self, o: &Comp) -> bool {
|
||||
// Every field (no `..`: a new field must be considered here); the layers last.
|
||||
self.same_settings(o) && self.layers.len() == o.layers.len() && self.layers.iter().zip(&o.layers).all(|(a, b)| a.same_pixels(b))
|
||||
}
|
||||
|
||||
/// [`Self::same_pixels`] but for the layers.
|
||||
fn same_settings(&self, o: &Comp) -> bool {
|
||||
// Every field (no `..`: a new field must be considered here).
|
||||
let Comp {
|
||||
width,
|
||||
height,
|
||||
@@ -849,7 +854,7 @@ impl Comp {
|
||||
display_start,
|
||||
background,
|
||||
work_area,
|
||||
layers,
|
||||
layers: _,
|
||||
markers,
|
||||
shutter_angle,
|
||||
shutter_phase,
|
||||
@@ -891,8 +896,50 @@ impl Comp {
|
||||
&& *global_light == o.global_light
|
||||
&& *guides == o.guides
|
||||
&& *essential == o.essential
|
||||
&& layers.len() == o.layers.len()
|
||||
&& layers.iter().zip(&o.layers).all(|(a, b)| a.same_pixels(b))
|
||||
}
|
||||
|
||||
/// The comp-time span (`in_point..out_point` of every layer changed, added or removed)
|
||||
/// outside which `self` draws the same pixels as `o`, so cached frames there stay valid
|
||||
/// (#1641). `None` when the edit can reach any frame: a comp setting, a layer reorder, an
|
||||
/// expression (it reads anything), or a changed layer another layer parents to, mattes
|
||||
/// with or picks in an effect.
|
||||
pub fn changed_span(&self, o: &Comp) -> Option<(Tick, Tick)> {
|
||||
fn reads(g: &PropGroup, changed: &[LayerId]) -> bool {
|
||||
g.children.iter().any(|c| match c {
|
||||
Node::Prop(p) => {
|
||||
p.expr.is_some()
|
||||
|| std::iter::once(&p.value)
|
||||
.chain(p.keys.iter().map(|k| &k.value))
|
||||
.any(|v| matches!(v, Value::Layer(Some(id)) if changed.contains(&LayerId(*id))))
|
||||
}
|
||||
Node::Group(g) => reads(g, changed),
|
||||
})
|
||||
}
|
||||
if !self.same_settings(o) {
|
||||
return None;
|
||||
}
|
||||
let common = |a: &Comp, b: &Comp| a.layers.iter().map(|l| l.id).filter(|id| b.layer(*id).is_some()).collect::<Vec<_>>();
|
||||
if common(self, o) != common(o, self) {
|
||||
return None;
|
||||
}
|
||||
let mut changed = Vec::new();
|
||||
let mut span: Option<(Tick, Tick)> = None;
|
||||
// Each changed layer comes up from both sides: its old and its new span.
|
||||
for (l, other) in self.layers.iter().map(|l| (l, o.layer(l.id))).chain(o.layers.iter().map(|l| (l, self.layer(l.id)))) {
|
||||
if other.is_some_and(|b| l.same_pixels(b)) {
|
||||
continue;
|
||||
}
|
||||
changed.push(l.id);
|
||||
let (s, e) = (l.in_point.min(l.out_point), l.in_point.max(l.out_point));
|
||||
span = Some(span.map_or((s, e), |(a, b)| (a.min(s), b.max(e))));
|
||||
}
|
||||
let reaches = |l: &Layer| {
|
||||
l.parent.is_some_and(|p| changed.contains(&p)) || l.track_matte.as_ref().is_some_and(|m| changed.contains(&m.layer)) || reads(&l.props, &changed)
|
||||
};
|
||||
if self.layers.iter().chain(&o.layers).any(reaches) {
|
||||
return None;
|
||||
}
|
||||
span
|
||||
}
|
||||
|
||||
/// Draws the same pixels as `o` once plain markers are ignored: they change no pixels. Protected
|
||||
|
||||
@@ -227,6 +227,19 @@ impl Cache {
|
||||
}
|
||||
self.order.retain(|k| !gone.contains(&k.content));
|
||||
}
|
||||
/// Move the frames of identity `from` that `keep` accepts to identity `to` (in place in the
|
||||
/// eviction order), unless `to` already has them.
|
||||
fn carry(&mut self, from: u64, to: u64, keep: impl Fn(i64) -> bool) {
|
||||
for k in self.order.iter_mut().filter(|k| k.content == from && keep(k.frame)) {
|
||||
let moved = FrameKey { content: to, ..*k };
|
||||
if !self.map.contains_key(&moved)
|
||||
&& let Some(img) = self.map.remove(k)
|
||||
{
|
||||
self.map.insert(moved, img);
|
||||
*k = moved;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn evict_to_budget(&mut self) {
|
||||
while self.bytes > self.budget {
|
||||
let Some(old) = self.victim().and_then(|i| self.order.remove(i)) else { break };
|
||||
@@ -731,6 +744,21 @@ impl Frames {
|
||||
{
|
||||
return (*base_id, Some(base.clone()));
|
||||
}
|
||||
// Only layers of this comp changed: its frames outside their span (a frame either side
|
||||
// for motion blur) keep drawing the same pixels, so a cut or trim recaches only the
|
||||
// layers it touched (#1641).
|
||||
if let Some((base_id, base)) = bases.get(&comp.0)
|
||||
&& let (Some(old), Some(new)) = (base.comp(comp), project.comp(comp))
|
||||
&& comp_content_with(project, comp, |id, c| match base.item(id).map(|i| &i.kind) {
|
||||
Some(ItemKind::Comp(b)) if id == comp || Arc::ptr_eq(b, c) || same_pixels(b, c, markers_count) => Arc::as_ptr(b),
|
||||
_ => Arc::as_ptr(c),
|
||||
}) == *base_id
|
||||
&& let Some((s, e)) = new.changed_span(old)
|
||||
&& let Ok(mut cache) = self.cache.lock()
|
||||
{
|
||||
let (first, last) = (new.frame_rate.frame_at(s).saturating_sub(1), new.frame_rate.frame_at(e).saturating_add(1));
|
||||
cache.carry(*base_id, plain, |f| f < first || f > last);
|
||||
}
|
||||
if bases.len() > MAX_KEEPERS {
|
||||
bases.clear();
|
||||
}
|
||||
@@ -1990,6 +2018,43 @@ mod tests {
|
||||
assert_ne!(at(&s), o1);
|
||||
}
|
||||
|
||||
/// A cut or trim recaches only the frames of the layers it touched (#1641), and none are
|
||||
/// kept when the change reaches other layers (here a child of the trimmed layer).
|
||||
#[test]
|
||||
fn an_edit_keeps_the_frames_outside_the_layers_it_changed() {
|
||||
use effectcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
let mut s = Session::default();
|
||||
let id = |v: serde_json::Value, k: &str| v[k].as_u64().unwrap();
|
||||
let comp = ItemId(id(s.execute("comp.new", json!({"name": "Edit", "width": 8, "height": 8, "duration": 10})).unwrap(), "comp"));
|
||||
let a = id(s.execute("layer.newSolid", json!({"comp": comp.0, "color": "#ff0000"})).unwrap(), "layer");
|
||||
let b = id(s.execute("layer.newSolid", json!({"comp": comp.0, "color": "#00ff00"})).unwrap(), "layer");
|
||||
s.execute("layer.timing", json!({"layers": [a], "in": 0, "out": 4})).unwrap();
|
||||
s.execute("layer.timing", json!({"layers": [b], "in": 4, "out": 10})).unwrap();
|
||||
let (fr, out) = s.project.comp(comp).map(|c| (c.frame_rate, c.layers.iter().find(|l| l.id.0 == a).unwrap().out_point)).unwrap();
|
||||
let frames: Vec<i64> = (0..fr.frame_at(Tick::from_seconds_f64(10.0))).collect();
|
||||
let f = Frames::default();
|
||||
let cache_all = |s: &Session| {
|
||||
let c = f.content_of(&s.project, s.revision, comp);
|
||||
frames.iter().for_each(|&n| insert(&f, key(c, n, 0)));
|
||||
c
|
||||
};
|
||||
let cached = |c: u64| frames.iter().filter(|&&n| f.is_cached(&key(c, n, 0))).copied().collect::<Vec<_>>();
|
||||
let before = cache_all(&s);
|
||||
s.execute("time.set", json!({"time": 2.0})).unwrap();
|
||||
s.execute("edit.splitLayer", json!({"layers": [a]})).unwrap();
|
||||
let after = f.content_of(&s.project, s.revision, comp);
|
||||
assert_ne!(after, before);
|
||||
let kept: Vec<i64> = (fr.frame_at(out) + 2..frames.len() as i64).collect();
|
||||
assert_eq!(cached(after), kept, "the cut layer's frames (and one either side) render again; the rest stay");
|
||||
|
||||
s.execute("layer.setParent", json!({"layers": [b], "parent": a})).unwrap();
|
||||
let parented = cache_all(&s);
|
||||
s.execute("layer.timing", json!({"layers": [a], "out": 1})).unwrap();
|
||||
assert_eq!(cached(f.content_of(&s.project, s.revision, comp)), Vec::<i64>::new(), "the trimmed layer's child moves everywhere");
|
||||
assert_eq!(cached(parented).len(), frames.len(), "the earlier frames stay for undo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markers_keep_the_frames() {
|
||||
use effectcraft_engine::Session;
|
||||
|
||||
Reference in New Issue
Block a user