M4.8: Edit ▸ Purge clears what it names: disk and snapshot purges keep the RAM preview, memory purges drop decoded footage

- Every purge emptied the viewers' RAM preview; Purge ▸ All Disk Cache and Snapshot now leave
  it (Image Cache Memory, All Memory, All Memory & Disk Cache, All and All 3D Cache still
  clear it).
- The memory purges also drop the decoded footage frames (`FootageSource::purge`, the media
  pool's `clear_frames`; decoders stay open), so footage reads decode again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-05 01:03:47 +07:00
parent 5ce641c6b3
commit fa27d621e0
4 changed files with 45 additions and 4 deletions

View File

@ -569,6 +569,7 @@ fn purge_caches(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
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}))
}

View File

@ -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<std::sync::Arc<crate::render::Image>> {
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"));
}

View File

@ -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<usize> {
Some(self.budget())
}

View File

@ -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<std::path::PathBuf>) {}