diff --git a/crates/engine/src/commands/file.rs b/crates/engine/src/commands/file.rs index 74ec861e..71f23b98 100644 --- a/crates/engine/src/commands/file.rs +++ b/crates/engine/src/commands/file.rs @@ -445,6 +445,11 @@ fn add_footage(s: &mut Session, pending: PendingImport, probed: Vec = vec![]; let made: Vec = comps.iter().map(|c| ItemId(*c)).collect(); if !found.is_empty() { + // The file may have been overwritten since an earlier import or project decoded it (the + // media layer keys frames by path): a fresh import reads it again (#1070). + for f in &found { + s.forget_decoded(&f.footage); + } s.edit("Import", None, |proj, st| { let mut selection = made; for f in found { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index a6243e44..845082fe 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1016,6 +1016,15 @@ impl Session { Ok((img.width, img.height, img.to_rgba8())) } + /// Make the media layer and the layer cache let go of what they decoded from `footage`'s + /// files (it keys frames by path, so an overwritten file would keep showing its old pixels). + pub(crate) fn forget_decoded(&self, footage: &effectcraft_project::Footage) { + for p in std::iter::once(&footage.path).chain(&footage.sequence) { + self.footage.forget(p); + } + self.layer_cache.clear(); + } + /// Replace the whole project (open/new), resetting history and state. pub fn replace_project(&mut self, mut p: Project, path: Option) { if self.stop_render() @@ -1042,6 +1051,14 @@ impl Session { self.script_ui.project_epoch = self.script_ui.project_epoch.wrapping_add(1); p.fix_next_id(); upgrade_effects(&mut p); + // The media layer keys decoded frames by path: files of the new project may have changed + // since an earlier project decoded them (#1070). + for it in p.items.values() { + if let effectcraft_project::ItemKind::Footage(f) = &it.kind { + self.forget_decoded(f); + } + } + self.layer_cache.clear(); self.project = Arc::new(p); self.history = History::default(); self.state = EditorState { snapping: true, view_layout: 1, ..Default::default() }; diff --git a/crates/host/tests/footage_reload.rs b/crates/host/tests/footage_reload.rs index 384f86a0..6834fc54 100644 --- a/crates/host/tests/footage_reload.rs +++ b/crates/host/tests/footage_reload.rs @@ -194,3 +194,22 @@ fn image_sequences_reload_by_themselves_only_with_all_footage() { png(&b, 128); assert_eq!(s.execute_checked("footage.reloadChanged", json!({})).unwrap()["files"], json!(1)); } + +/// #1070: a file decoded in one project and overwritten with one of the same size is read again +/// when a new project imports it (neither File ▸ Reload Footage nor the change watcher knows it). +#[test] +fn a_new_project_importing_an_overwritten_file_shows_the_new_pixels() { + let path = tmp("overwritten.psd"); + std::fs::write(&path, flat(RED, false)).unwrap(); + let (mut s, _item, cid) = flat_in_comp(&path); + assert!(near(pixel(&s, cid, 32, 24), RED)); + // Another app overwrites the file with the same number of bytes. + save(&path, &flat(BLUE, false)); + s.execute_checked("file.newProject", json!({})).unwrap(); + let r = s.execute_checked("file.import", json!({"paths": [path.clone()]})).unwrap(); + let item = r["items"][0].as_u64().unwrap(); + s.execute_checked("comp.new", json!({"name": "C", "width": 64, "height": 48, "frameRate": 30, "duration": 1})).unwrap(); + s.execute_checked("layer.addItem", json!({"item": item})).unwrap(); + let cid = s.active_comp_id().unwrap(); + assert!(near(pixel(&s, cid, 32, 24), BLUE), "stale pixels from the earlier project: {:?}", pixel(&s, cid, 32, 24)); +}