Preserve frozen Object Mattes with pre-0.23 footage keys (#3430)

This commit is contained in:
lisn0
2026-11-11 00:37:34 +00:00
parent c30fb9aeed
commit 3694f07600
3 changed files with 67 additions and 2 deletions
+12 -2
View File
@@ -219,7 +219,15 @@ fn fnv(s: &str) -> u64 {
/// What the effect's input frames are made of: the layer's source and timing, its masks and the
/// effects above the Roto Brush.
pub fn input_key(p: &Project, comp: ItemId, layer: &Layer, uid: Uid) -> String {
let mut s = crate::warp::signature(p, comp, layer);
input_key_with_signature(crate::warp::signature(p, comp, layer), layer, uid)
}
pub(crate) fn input_key_before_placed_page(p: &Project, comp: ItemId, layer: &Layer, uid: Uid) -> Option<String> {
let signature = crate::warp::signature_before_placed_page(p, comp, layer)?;
Some(input_key_with_signature(signature, layer, uid))
}
fn input_key_with_signature(mut s: String, layer: &Layer, uid: Uid) -> String {
if let Some(m) = layer.masks() {
s.push_str(&format!("{m:?}"));
}
@@ -247,7 +255,9 @@ pub(crate) fn sync(before: &Project, after: &mut Project) -> Vec<(ItemId, LayerI
for g in fxg.groups().filter(|g| is_roto(g) || is_object_matte(g)) {
let key = input_key(after, *cid, l, g.uid);
let old = before.comp(*cid).and_then(|bc| bc.layer(l.id)).and_then(|ol| ol.props.find_group(g.uid));
if str_of(g, INPUT_KEY) != key {
let saved_key = str_of(g, INPUT_KEY);
// Keep a matching old key: the frozen cache itself also hashes that key.
if saved_key != key && input_key_before_placed_page(after, *cid, l, g.uid).as_deref() != Some(saved_key.as_str()) {
set.push((*cid, l.id, g.uid, key));
todo.push((*cid, l.id, g.uid));
} else if old.is_none_or(|o| guard_of(o) != guard_of(g)) {
+14
View File
@@ -119,6 +119,20 @@ pub fn signature(p: &Project, comp: ItemId, layer: &Layer) -> String {
},
_ => String::new(),
};
signature_with_source(p, comp, layer, &src)
}
/// Before `Footage::placed_page` was added in 0.23, the persisted signature hashed the
/// same Debug representation without that last field. Only ordinary footage can match it.
pub(crate) fn signature_before_placed_page(p: &Project, comp: ItemId, layer: &Layer) -> Option<String> {
let LayerSource::Footage { item } = &layer.source else { return None };
let ItemKind::Footage(footage) = &p.item(*item)?.kind else { return None };
let src = format!("{footage:?}");
let old = src.strip_suffix(", placed_page: false }")?;
Some(signature_with_source(p, comp, layer, &format!("{old} }}")))
}
fn signature_with_source(p: &Project, comp: ItemId, layer: &Layer, src: &str) -> String {
let rate = p.comp(comp).map(|c| format!("{:?}", c.frame_rate)).unwrap_or_default();
let s = format!(
"{:?}|{src}|{:?}|{:?}|{:?}|{}|{:?}|{rate}",
+41
View File
@@ -603,6 +603,47 @@ fn object_matte_background_job_cancel_and_freeze() {
assert_eq!(status(&mut s)["frozen"], true);
}
/// A queue edit preserves a frozen matte made before the footage signature gained placed_page.
#[test]
fn render_queue_keeps_frozen_object_matte_with_legacy_footage_key() {
use effectcraft_effects::{object_matte, roto::{FROZEN, FrozenCache, INPUT_KEY}};
use effectcraft_keyframe::Value::Str;
use std::sync::Arc;
hold_roto_cache_test_lock();
let (mut s, clip, _) = setup(|f| draw(center(f)));
let cid = s.active_comp_id().unwrap();
let r = s.execute_checked("objectMatte.apply", json!({"layer": clip.0, "point": center(0), "before": 0, "after": 2})).unwrap();
let uid = r["effect"].as_u64().unwrap();
s.execute_checked("objectMatte.freeze", json!({"layer": clip.0, "wait": true})).unwrap();
let layer = s.project.comp(cid).unwrap().layer(clip).unwrap();
let legacy = crate::roto::input_key_before_placed_page(&s.project, cid, layer, uid).unwrap();
assert_ne!(legacy, crate::roto::input_key(&s.project, cid, layer, uid));
let fps = s.project.comp(cid).unwrap().frame_rate.as_f64();
// Recreate the two keys serialized by 0.22, leaving the real frozen alpha data alone.
let p = Arc::make_mut(&mut s.project);
let g = p.comp_mut(cid).unwrap().layer_mut(clip).unwrap().props.find_group_mut(uid).unwrap();
g.get_mut(INPUT_KEY).unwrap().value = Str(legacy.clone());
let params = crate::roto::params_static(g);
let Some(Str(value)) = params.get(FROZEN) else { panic!("missing frozen matte") };
let mut frozen: FrozenCache = serde_json::from_str(value).unwrap();
assert_eq!(frozen.frames.len(), 3);
frozen.key = object_matte::frozen_key(&params, [f64::from(W), f64::from(H)], fps, None);
let saved = Str(serde_json::to_string(&frozen).unwrap());
g.get_mut(FROZEN).unwrap().value = saved.clone();
s.execute_checked("renderQueue.add", json!({"comp": cid.0, "format": "png", "output": "matte.png"})).unwrap();
let params = |s: &Session| {
crate::roto::params_static(s.project.comp(cid).unwrap().layer(clip).unwrap().props.find_group(uid).unwrap())
};
assert_eq!(params(&s).get(INPUT_KEY), Some(&Str(legacy)));
assert_eq!(params(&s).get(FROZEN), Some(&saved));
s.execute_checked("layer.timing", json!({"layer": clip.0, "out": 0.5})).unwrap();
assert_eq!(params(&s).get(FROZEN), Some(&Str(String::new())));
}
/// Computed Object Matte segmentations go to the persistent disk cache: a second session on the
/// same folder and project gets them back without segmenting (its frames cannot even be read
/// here), and another prompt (other keys) finds nothing.