mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 21:16:52 -06:00
Merge upstream/main into fix-1083-beat-marker-audio-pause
Upstream's #1167 fix and this branch both stop marker edits from invalidating cached frames. Kept this branch's shared Project::reads_markers/same_pixels_with/ pixel_form_with (viewer frames, disk cache, nested comps) and folded upstream's rules into them: any expression counts as reading markers, protected time markers with a duration still change the frames. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,9 @@
|
||||
//! project therefore never reads stale pixels: its keys simply change.
|
||||
//! * Payloads are LZ4-compressed (`lz4_flex`, MIT): frames as 8-bit premultiplied RGBA, layer
|
||||
//! buffers as `f32` RGBA with the bytes shuffled into planes (exact, so cached and fresh
|
||||
//! renders are bit-identical). Every file ends in a checksum; damaged files read as misses and
|
||||
//! renders are bit-identical), Roto Brush / Object Matte segmentations as
|
||||
//! `FrameSeg::to_bytes` (keyed by their content-keyed chain key and scale, so they are
|
||||
//! reused whenever the same frames, settings and prompt come back, also after a restart). Every file ends in a checksum; damaged files read as misses and
|
||||
//! are deleted.
|
||||
//! * Size limit with least-recently-used eviction. The in-memory index is rebuilt from the
|
||||
//! folder when the cache opens (file modification times order it), so the cache survives
|
||||
@@ -37,6 +39,8 @@ const QUEUE_LIMIT: usize = 64;
|
||||
pub enum Kind {
|
||||
Frame,
|
||||
Layer,
|
||||
/// A Roto Brush / Object Matte segmentation.
|
||||
Matte,
|
||||
}
|
||||
|
||||
impl Kind {
|
||||
@@ -45,12 +49,14 @@ impl Kind {
|
||||
match self {
|
||||
Kind::Frame => "frames",
|
||||
Kind::Layer => "layers",
|
||||
Kind::Matte => "mattes",
|
||||
}
|
||||
}
|
||||
fn tag(self) -> u8 {
|
||||
match self {
|
||||
Kind::Frame => 1,
|
||||
Kind::Layer => 2,
|
||||
Kind::Matte => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,7 +325,7 @@ impl DiskCache {
|
||||
/// Rebuild the index from the folder (oldest files first).
|
||||
pub fn rescan(&self) {
|
||||
let mut found: Vec<(std::time::SystemTime, Kind, u128, u64)> = vec![];
|
||||
for kind in [Kind::Frame, Kind::Layer] {
|
||||
for kind in [Kind::Frame, Kind::Layer, Kind::Matte] {
|
||||
let Ok(shards) = std::fs::read_dir(self.dir.join(kind.dir())) else { continue };
|
||||
for shard in shards.flatten() {
|
||||
let Ok(files) = std::fs::read_dir(shard.path()) else { continue };
|
||||
@@ -403,7 +409,7 @@ impl DiskCache {
|
||||
if let Ok(mut g) = self.index.lock() {
|
||||
g.clear();
|
||||
}
|
||||
for kind in [Kind::Frame, Kind::Layer] {
|
||||
for kind in [Kind::Frame, Kind::Layer, Kind::Matte] {
|
||||
let _ = std::fs::remove_dir_all(self.dir.join(kind.dir()));
|
||||
}
|
||||
}
|
||||
@@ -555,6 +561,22 @@ impl DiskCache {
|
||||
self.write_now(Kind::Layer, key, &encode_layer(buf));
|
||||
}
|
||||
|
||||
/// A segmentation (chain key, scale) written by [`DiskCache::put_seg`].
|
||||
pub fn get_seg(&self, key: u64, scale: f64) -> Option<effectcraft_effects::roto::FrameSeg> {
|
||||
let p = self.read(Kind::Matte, seg_key(key, scale))?;
|
||||
let raw = lz4_flex::decompress_size_prepended(&p).ok()?;
|
||||
effectcraft_effects::roto::FrameSeg::from_bytes(&raw)
|
||||
}
|
||||
|
||||
/// Queue a segmentation for writing.
|
||||
pub fn put_seg(&self, key: u64, scale: f64, seg: &effectcraft_effects::roto::FrameSeg) {
|
||||
let k = seg_key(key, scale);
|
||||
if self.contains(Kind::Matte, k) {
|
||||
return;
|
||||
}
|
||||
self.enqueue(Kind::Matte, k, lz4_flex::compress_prepend_size(&seg.to_bytes()));
|
||||
}
|
||||
|
||||
/// Write a frame synchronously (tests).
|
||||
pub fn put_frame_now(&self, key: u128, width: u32, height: u32, rgba: &[u8]) {
|
||||
self.write_now(Kind::Frame, key, &encode_frame(width, height, rgba));
|
||||
@@ -627,6 +649,24 @@ pub fn read_layer_entry(data: &[u8]) -> Option<Buf> {
|
||||
decode_layer(verify(data, Kind::Layer)?)
|
||||
}
|
||||
|
||||
/// The disk key of a segmentation: its chain key and scale.
|
||||
fn seg_key(key: u64, scale: f64) -> u128 {
|
||||
let mut h = Hash128::default();
|
||||
h.write(b"segmentation-v1");
|
||||
h.write_u64(key);
|
||||
h.write_u64(scale.to_bits());
|
||||
h.finish()
|
||||
}
|
||||
|
||||
impl effectcraft_effects::roto::SegStore for DiskCache {
|
||||
fn get_seg(&self, key: u64, scale: f64) -> Option<effectcraft_effects::roto::FrameSeg> {
|
||||
DiskCache::get_seg(self, key, scale)
|
||||
}
|
||||
fn put_seg(&self, key: u64, scale: f64, seg: &effectcraft_effects::roto::FrameSeg) {
|
||||
DiskCache::put_seg(self, key, scale, seg);
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::cache::LayerStore for DiskCache {
|
||||
fn get_layer(&self, key: u128) -> Option<Arc<Buf>> {
|
||||
DiskCache::get_layer(self, key).map(Arc::new)
|
||||
@@ -892,6 +932,38 @@ mod tests {
|
||||
assert_eq!(parse_entry_name(&format!("{key:032x}.tmp.1")), None);
|
||||
}
|
||||
|
||||
/// Segmentations round-trip by (chain key, scale), survive a reopen (rescan), are bounded by
|
||||
/// the size limit like every entry, and a damaged file is a miss.
|
||||
#[test]
|
||||
fn segmentations_round_trip_and_reopen() {
|
||||
use effectcraft_effects::roto::FrameSeg;
|
||||
let dir = tmpdir("seg");
|
||||
let dc = DiskCache::open(&dir, 1 << 30).unwrap();
|
||||
let seg = FrameSeg { w: 7, h: 5, matte: (0..35).map(|i| (i % 3 == 0) as u8).collect(), refine: vec![0; 35], ..Default::default() };
|
||||
assert!(dc.get_seg(42, 1.0).is_none());
|
||||
dc.put_seg(42, 1.0, &seg);
|
||||
dc.flush();
|
||||
assert_eq!(dc.get_seg(42, 1.0), Some(seg.clone()));
|
||||
assert!(dc.get_seg(42, 0.5).is_none() && dc.get_seg(43, 1.0).is_none());
|
||||
assert_eq!(dc.keys(Kind::Matte).len(), 1);
|
||||
drop(dc);
|
||||
let dc = DiskCache::open(&dir, 1 << 30).unwrap();
|
||||
assert_eq!(dc.get_seg(42, 1.0), Some(seg.clone()));
|
||||
// Damaged on disk: a miss, and the entry is forgotten.
|
||||
let path = dc.path(Kind::Matte, seg_key(42, 1.0));
|
||||
let mut d = std::fs::read(&path).unwrap();
|
||||
d[18] ^= 0x55;
|
||||
std::fs::write(&path, d).unwrap();
|
||||
assert!(dc.get_seg(42, 1.0).is_none());
|
||||
assert!(!dc.contains(Kind::Matte, seg_key(42, 1.0)));
|
||||
// The size limit evicts segmentations too.
|
||||
dc.put_seg(1, 1.0, &seg);
|
||||
dc.flush();
|
||||
dc.set_max_bytes(1);
|
||||
assert!(dc.keys(Kind::Matte).is_empty());
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hit_miss_and_exact_round_trip() {
|
||||
let dir = tmpdir("hit");
|
||||
|
||||
Reference in New Issue
Block a user