diff --git a/crates/engine/src/commands/edit.rs b/crates/engine/src/commands/edit.rs index 10051b42..bcc6a910 100644 --- a/crates/engine/src/commands/edit.rs +++ b/crates/engine/src/commands/edit.rs @@ -569,6 +569,7 @@ fn purge_caches(s: &mut Session, p: &Value) -> Result { let disk = matches!(what.as_str(), "all" | "memoryAndDisk" | "disk"); if memory { s.layer_cache.clear(); + s.footage.purge(); } let mut disk_entries = 0; if disk && let Some(dc) = &s.disk_cache { @@ -581,7 +582,11 @@ fn purge_caches(s: &mut Session, p: &Value) -> Result { if matches!(what.as_str(), "all" | "image" | "memory" | "memoryAndDisk") { effectcraft_effects::roto::purge(); } - s.events.push(crate::Event::PurgeCaches); + // The viewers' RAM preview goes with the memory purges (and the 3D one, whose renders it + // holds); purging the disk cache or the snapshot leaves it. + if memory || what == "3d" { + s.events.push(crate::Event::PurgeCaches); + } s.toast(format!("Purged {what} cache")); Ok(json!({"purged": what, "diskEntries": disk_entries})) } diff --git a/crates/engine/src/tests_menu_cmds.rs b/crates/engine/src/tests_menu_cmds.rs index 85e12674..ec6e09b6 100644 --- a/crates/engine/src/tests_menu_cmds.rs +++ b/crates/engine/src/tests_menu_cmds.rs @@ -263,9 +263,35 @@ fn label_group_purge_and_edit_original() { s.execute("layer.select", json!({"layers": [a]})).unwrap(); s.execute("edit.selectLabelGroup", json!({})).unwrap(); assert_eq!(s.state.selected_layers.len(), 2); + // A footage source that counts purges (the media pool drops its decoded frames). + struct Counting(std::sync::atomic::AtomicUsize); + impl crate::render::FootageSource for Counting { + fn frame( + &self, + _: effectcraft_project::ItemId, + _: &effectcraft_project::Footage, + _: effectcraft_time::Tick, + ) -> Option> { + None + } + fn purge(&self) { + self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + } + } + let src = std::sync::Arc::new(Counting(Default::default())); + s.footage = src.clone(); s.drain_events(); s.execute("edit.purge", json!({"what": "memory"})).unwrap(); assert!(s.drain_events().contains(&Event::PurgeCaches)); + assert_eq!(src.0.load(std::sync::atomic::Ordering::SeqCst), 1, "decoded footage frames are dropped"); + // The disk cache and the snapshot purge alone: the RAM preview stays. + for what in ["disk", "snapshot"] { + s.execute("edit.purge", json!({"what": what})).unwrap(); + assert!(!s.drain_events().contains(&Event::PurgeCaches), "{what}"); + } + assert_eq!(src.0.load(std::sync::atomic::Ordering::SeqCst), 1); + s.execute("edit.purge", json!({"what": "3d"})).unwrap(); + assert!(s.drain_events().contains(&Event::PurgeCaches)); assert!(!s.is_enabled("edit.editOriginal")); } diff --git a/crates/media/src/pool.rs b/crates/media/src/pool.rs index 3aa7fca7..e5be90a6 100644 --- a/crates/media/src/pool.rs +++ b/crates/media/src/pool.rs @@ -233,12 +233,16 @@ impl MediaPool { } } - /// Drop every cached frame and decoder. - pub fn clear(&self) { + /// Drop every cached frame (decoders stay open). + pub fn clear_frames(&self) { let mut c = lock(&self.inner.cache); c.lru = Lru::default(); c.last.clear(); - drop(c); + } + + /// Drop every cached frame and decoder. + pub fn clear(&self) { + self.clear_frames(); lock(&self.inner.sources).clear(); } @@ -602,6 +606,9 @@ impl FootageSource for MediaPool { fn set_cache_budget(&self, bytes: usize) { self.set_budget(bytes); } + fn purge(&self) { + self.clear_frames(); + } fn cache_budget(&self) -> Option { Some(self.budget()) } diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 4b6c7f3e..13d8fc4b 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -56,6 +56,9 @@ pub trait FootageSource: Send + Sync { /// Set the decoded-frame cache budget in bytes (Settings ▸ Memory & CPU); sources without /// a cache ignore it. fn set_cache_budget(&self, _bytes: usize) {} + /// Drop the decoded frames it keeps (Edit ▸ Purge ▸ All Memory / Image Cache Memory): the + /// next reads decode again. Sources without a cache ignore it. + fn purge(&self) {} /// Settings ▸ Disk ▸ Conformed Audio Folder: where decoded audio is kept between reads /// (`None` = off); sources that don't decode audio ignore it. fn set_conform_folder(&self, _folder: Option) {}