mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 21:16:52 -06:00
198 lines
9.7 KiB
Rust
198 lines
9.7 KiB
Rust
//! The persistent disk cache (PRV-2) through the session: settings, layer cache persistence
|
|
//! across restarts, invalidation on edits, purging.
|
|
|
|
use serde_json::json;
|
|
|
|
use crate::Session;
|
|
use crate::render::RenderOpts;
|
|
|
|
fn folder(name: &str) -> String {
|
|
let d = std::env::temp_dir().join(format!("effectcraft-session-disk-{name}-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&d);
|
|
d.to_string_lossy().to_string()
|
|
}
|
|
|
|
fn session(dir: &str) -> Session {
|
|
let mut s = Session::default();
|
|
s.execute_checked("prefs.set", json!({"values": {"disk.diskCacheFolder": dir, "disk.diskCacheEnabled": true}})).unwrap();
|
|
// Write every layer buffer, however quick to render.
|
|
s.disk_cache.as_ref().unwrap().set_min_layer_ms(0);
|
|
s
|
|
}
|
|
|
|
fn project(s: &mut Session) -> effectcraft_project::ItemId {
|
|
s.execute("comp.new", json!({"name": "D", "width": 160, "height": 90, "frameRate": 30, "duration": 1})).unwrap();
|
|
s.execute("layer.newSolid", json!({"color": "#3080ff", "width": 80, "height": 40})).unwrap();
|
|
s.execute("effect.apply", json!({"layer": "#1", "effect": "Gaussian Blur"})).unwrap();
|
|
s.execute("prop.set", json!({"layer": "#1", "path": "effects/#1/blurriness", "value": 12})).unwrap();
|
|
s.active_comp_id().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn disk_cache_settings_persistence_and_invalidation() {
|
|
crate::tests::roto::hold_roto_cache_test_lock();
|
|
let dir = folder("persist");
|
|
let mut s = session(&dir);
|
|
let st = s.execute_checked("cache.diskStats", json!({})).unwrap();
|
|
assert_eq!(st["enabled"], json!(true));
|
|
assert_eq!(st["maxBytes"], json!(20u64 << 30));
|
|
let cid = project(&mut s);
|
|
let t = s.time();
|
|
let first = s.render(cid, t, RenderOpts::default());
|
|
let dc = s.disk_cache.clone().unwrap();
|
|
dc.flush();
|
|
let written = dc.stats().entries;
|
|
assert!(written > 0, "layer buffers written");
|
|
// "Restart": a new session on the same folder and project renders from disk.
|
|
let saved = s.project.clone();
|
|
drop(s);
|
|
let mut s2 = session(&dir);
|
|
s2.project = saved;
|
|
s2.bump();
|
|
s2.open_comp(cid);
|
|
let dc2 = s2.disk_cache.clone().unwrap();
|
|
assert_eq!(dc2.stats().entries, written);
|
|
let again = s2.render(cid, t, RenderOpts::default());
|
|
assert!(dc2.stats().hits > 0, "{:?}", dc2.stats());
|
|
assert_eq!(again.data, first.data, "disk-cached pixels are exact");
|
|
// An edit changes the keys: a miss, and new entries.
|
|
let hits = dc2.stats().hits;
|
|
s2.execute("prop.set", json!({"layer": "#1", "path": "effects/#1/blurriness", "value": 20})).unwrap();
|
|
let edited = s2.render(cid, t, RenderOpts::default());
|
|
assert_ne!(edited.data, first.data);
|
|
assert_eq!(dc2.stats().hits, hits);
|
|
dc2.flush();
|
|
assert!(dc2.stats().entries > written);
|
|
// Purge ▸ All Disk Cache empties it; memory purge leaves the disk alone.
|
|
let r = s2.execute_checked("edit.purge", json!({"what": "memory"})).unwrap();
|
|
assert_eq!(r["diskEntries"], json!(0));
|
|
assert!(dc2.stats().entries > 0);
|
|
let r = s2.execute_checked("edit.purge", json!({"what": "disk"})).unwrap();
|
|
assert!(r["diskEntries"].as_u64().unwrap() > 0);
|
|
assert_eq!(dc2.stats().entries, 0);
|
|
// Disabling detaches it.
|
|
s2.execute_checked("prefs.set", json!({"key": "disk.diskCacheEnabled", "value": false})).unwrap();
|
|
assert!(s2.disk_cache.is_none());
|
|
assert!(s2.layer_cache.disk().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn footage_changes_change_the_salt() {
|
|
let dir = folder("salt");
|
|
let mut s = session(&dir);
|
|
let foot = std::env::temp_dir().join(format!("effectcraft-salt-{}.png", std::process::id()));
|
|
std::fs::write(&foot, b"one").unwrap();
|
|
s.edit("add", None, |p, _| {
|
|
let f = effectcraft_project::Footage {
|
|
path: foot.to_string_lossy().to_string(),
|
|
kind: effectcraft_project::FootageKind::Still,
|
|
width: 1,
|
|
height: 1,
|
|
pixel_aspect: 1.0,
|
|
frame_rate: effectcraft_time::FrameRate::FPS_30,
|
|
native_rate: None,
|
|
duration: effectcraft_time::Tick::ZERO,
|
|
has_video: true,
|
|
has_audio: false,
|
|
alpha: effectcraft_project::AlphaMode::Ignore,
|
|
premul_color: [0.0; 3],
|
|
loop_count: 1,
|
|
codec: "PNG".into(),
|
|
missing: false,
|
|
sequence: vec![],
|
|
color_profile: None,
|
|
layer: None,
|
|
..Default::default()
|
|
};
|
|
p.add_item("f.png", effectcraft_color::Label::Lavender, None, effectcraft_project::ItemKind::Footage(f));
|
|
Ok(())
|
|
})
|
|
.unwrap();
|
|
let a = effectcraft_render::disk_cache::footage_salt(&s.project);
|
|
std::fs::write(&foot, b"two, longer").unwrap();
|
|
let b = effectcraft_render::disk_cache::footage_salt(&s.project);
|
|
assert_ne!(a, b);
|
|
let cid = project(&mut s);
|
|
let k1 = effectcraft_render::disk_cache::comp_content_key(&s.project, cid);
|
|
s.execute("prop.set", json!({"layer": "#1", "path": "transform/opacity", "value": 50})).unwrap();
|
|
let k2 = effectcraft_render::disk_cache::comp_content_key(&s.project, cid);
|
|
assert_ne!(k1, k2);
|
|
// Switches that don't change pixels (Audio, Lock, Shy, Hide Shy Layers) don't change it (#103).
|
|
for sw in ["audio", "lock", "shy"] {
|
|
s.execute("layer.setSwitch", json!({"layers": ["#1"], "switch": sw, "value": sw != "audio"})).unwrap();
|
|
}
|
|
s.execute("comp.setSwitch", json!({"switch": "hideShy", "value": true})).unwrap();
|
|
assert_eq!(effectcraft_render::disk_cache::comp_content_key(&s.project, cid), k2);
|
|
// Unrelated comps do not change it.
|
|
s.execute("comp.new", json!({"name": "Other", "width": 10, "height": 10, "frameRate": 30, "duration": 1})).unwrap();
|
|
assert_eq!(effectcraft_render::disk_cache::comp_content_key(&s.project, cid), k2);
|
|
// A proxy, and its Use Proxy switch, change the pixels.
|
|
let set_proxy = |s: &mut Session, enabled: bool| {
|
|
let it = std::sync::Arc::make_mut(&mut s.project).items.get_mut(&cid).unwrap();
|
|
it.proxy = Some(Box::new(effectcraft_project::Proxy { footage: effectcraft_project::Footage::default(), enabled }));
|
|
effectcraft_render::disk_cache::comp_content_key(&s.project, cid)
|
|
};
|
|
let k3 = set_proxy(&mut s, true);
|
|
assert_ne!(k3, k2);
|
|
assert_ne!(set_proxy(&mut s, false), k3);
|
|
}
|
|
|
|
/// Marker edits keep the disk key (the sound of a playing preview must not wait for frames to
|
|
/// render again, #1083); an expression that reads markers makes them count.
|
|
#[test]
|
|
fn markers_keep_the_content_key_unless_an_expression_reads_them() {
|
|
let mut s = Session::default();
|
|
s.execute("comp.new", json!({"name": "M", "width": 8, "height": 8, "frameRate": 30, "duration": 2})).unwrap();
|
|
let cid = s.active_comp_id().unwrap();
|
|
let l = s.execute("layer.newSolid", json!({"color": "#ff0000"})).unwrap()["layer"].as_u64().unwrap();
|
|
let key = |s: &Session| effectcraft_render::disk_cache::comp_content_key(&s.project, cid);
|
|
let k0 = key(&s);
|
|
s.execute("comp.addMarker", json!({"time": 0.5, "comment": "beat"})).unwrap();
|
|
s.execute("markers.set", json!({"layer": l, "new": true, "time": 0.25})).unwrap();
|
|
assert_eq!(key(&s), k0, "markers");
|
|
s.execute("prop.setExpression", json!({"layer": l, "path": "transform/opacity", "expression": "thisComp.marker.numKeys"})).unwrap();
|
|
let k1 = key(&s);
|
|
assert_ne!(k1, k0);
|
|
s.execute("comp.addMarker", json!({"time": 1.0})).unwrap();
|
|
assert_ne!(key(&s), k1, "a marker read by an expression");
|
|
}
|
|
|
|
/// A comp marker alone (no layer marker beside it) keeps the key too.
|
|
#[test]
|
|
fn a_lone_comp_marker_keeps_the_content_key() {
|
|
let mut s = Session::default();
|
|
s.execute("comp.new", json!({"name": "M", "width": 8, "height": 8, "frameRate": 30, "duration": 2})).unwrap();
|
|
let cid = s.active_comp_id().unwrap();
|
|
s.execute("layer.newSolid", json!({"color": "#ff0000"})).unwrap();
|
|
let key = |s: &Session| effectcraft_render::disk_cache::comp_content_key(&s.project, cid);
|
|
let k0 = key(&s);
|
|
s.execute("comp.addMarker", json!({"time": 0.5, "comment": "beat"})).unwrap();
|
|
assert_eq!(key(&s), k0);
|
|
}
|
|
|
|
/// A protected marker with a duration retimes a stretched precomp layer (Responsive Design), so
|
|
/// changing it changes the key of the comp that nests it; a plain marker beside it does not.
|
|
#[test]
|
|
fn protected_markers_with_a_duration_change_the_content_key() {
|
|
let mut s = Session::default();
|
|
s.execute("comp.new", json!({"name": "Inner", "width": 8, "height": 8, "frameRate": 30, "duration": 4})).unwrap();
|
|
let inner = s.active_comp_id().unwrap();
|
|
s.execute("layer.newSolid", json!({"color": "#ff0000"})).unwrap();
|
|
s.execute("comp.new", json!({"name": "Outer", "width": 8, "height": 8, "frameRate": 30, "duration": 10})).unwrap();
|
|
let outer = s.active_comp_id().unwrap();
|
|
let l = s.execute("layer.addItem", json!({"item": inner.0})).unwrap()["layer"].as_u64().unwrap();
|
|
s.execute("layer.select", json!({"layers": [l]})).unwrap();
|
|
s.execute("layer.timeStretch", json!({"percent": 200})).unwrap();
|
|
let key = |s: &Session| effectcraft_render::disk_cache::comp_content_key(&s.project, outer);
|
|
let k0 = key(&s);
|
|
s.execute("comp.addMarker", json!({"comp": inner.0, "time": 2.0, "comment": "beat"})).unwrap();
|
|
assert_eq!(key(&s), k0, "a plain marker");
|
|
s.execute("comp.addMarker", json!({"comp": inner.0, "time": 1.0})).unwrap();
|
|
let index = s.project.comp(inner).unwrap().markers.iter().position(|m| m.time.seconds() == 1.0).unwrap();
|
|
s.execute("markers.set", json!({"comp": inner.0, "index": index, "duration": 1.0, "protected": true})).unwrap();
|
|
let k1 = key(&s);
|
|
assert_ne!(k1, k0, "a protected marker with a duration");
|
|
s.execute("markers.set", json!({"comp": inner.0, "index": index, "duration": 2.0})).unwrap();
|
|
assert_ne!(key(&s), k1, "its duration");
|
|
}
|