@@ -238,6 +238,19 @@ impl Cache {
}
self . order . retain ( | k | ! gone . contains ( & k . content ) ) ;
}
/// Move the frames of identity `from` that `keep` accepts to identity `to` (in place in the
/// eviction order), unless `to` already has them.
fn carry ( & mut self , from : u64 , to : u64 , keep : impl Fn ( i64 ) -> bool ) {
for k in self . order . iter_mut ( ) . filter ( | k | k . content = = from & & keep ( k . frame ) ) {
let moved = FrameKey { content : to , .. * k } ;
if ! self . map . contains_key ( & moved )
& & let Some ( img ) = self . map . remove ( k )
{
self . map . insert ( moved , img ) ;
* k = moved ;
}
}
}
fn evict_to_budget ( & mut self ) {
while self . bytes > self . budget {
let Some ( old ) = self . victim ( ) . and_then ( | i | self . order . remove ( i ) ) else { break } ;
@@ -301,11 +314,12 @@ fn content_key(keys: &ContentKeys, project: &Project, revision: u64, comp: u64,
/// hands out), and by value the project settings and the footage, solids and proxies it uses.
/// With expressions in its comps (which can read anything) every item counts.
pub fn comp_content ( project : & Project , comp : ItemId ) -> u64 {
comp_content_with ( project , comp , | _ , c | Arc ::as_ptr ( c ) )
comp_content_with ( project , comp , project , | _ , c | c )
}
/// [`comp_content`] with each comp's address given by ` addr` .
fn comp_content_with ( project : & Project , comp : ItemId , addr : impl Fn ( ItemId , & Arc < Comp > ) -> * const Comp ) -> u64 {
/// [`comp_content`] with each comp's version selected for both its sources and its address .
/// Sources removed by undo are read from `base`.
fn comp_content_with < ' a > ( project : & ' a Project , comp : ItemId , base : & ' a Project , version : impl Fn ( ItemId , & ' a Arc < Comp > ) -> & ' a Arc < Comp > ) -> u64 {
use std ::hash ::{ Hash , Hasher } ;
fn has_expression ( g : & PropGroup ) -> bool {
g . children . iter ( ) . any ( | c | match c {
@@ -315,6 +329,7 @@ fn comp_content_with(project: &Project, comp: ItemId, addr: impl Fn(ItemId, &Arc
}
let mut h = std ::collections ::hash_map ::DefaultHasher ::new ( ) ;
format! ( " {:?} " , project . settings ) . hash ( & mut h ) ;
let item = | id | project . item ( id ) . or_else ( | | base . item ( id ) ) ;
let mut seen = BTreeSet ::new ( ) ;
let mut stack = vec! [ comp ] ;
let mut expressions = false ;
@@ -322,7 +337,8 @@ fn comp_content_with(project: &Project, comp: ItemId, addr: impl Fn(ItemId, &Arc
if ! seen . insert ( id ) {
continue ;
}
let Some ( ItemKind ::Comp ( c ) ) = project . item ( id ) . map ( | i | & i . kind ) else { continue } ;
let Some ( ItemKind ::Comp ( c ) ) = item ( id ) . map ( | i | & i . kind ) else { continue } ;
let c = version ( id , c ) ;
for l in & c . layers {
if let LayerSource ::Comp { item } | LayerSource ::Footage { item } | LayerSource ::Solid { item } = l . source {
stack . push ( item ) ;
@@ -332,14 +348,14 @@ fn comp_content_with(project: &Project, comp: ItemId, addr: impl Fn(ItemId, &Arc
}
let ids : Vec < ItemId > = if expressions { project . items . keys ( ) . copied ( ) . collect ( ) } else { seen . into_iter ( ) . collect ( ) } ;
for id in ids {
let Some ( it ) = project . item ( id ) else { continue } ;
let Some ( it ) = item ( id ) else { continue } ;
id . 0. hash ( & mut h ) ;
if expressions {
// (`comp("Name")`, `footage("Name")`)
it . name . hash ( & mut h ) ;
}
match & it . kind {
ItemKind ::Comp ( c ) = > ( addr ( id , c ) as usize ) . hash ( & mut h ) ,
ItemKind ::Comp ( c ) = > ( Arc ::as_ptr ( version ( id , c ) ) as usize ) . hash ( & mut h ) ,
k = > format! ( " {k:?} " ) . hash ( & mut h ) ,
}
format! ( " {:?} " , it . proxy ) . hash ( & mut h ) ;
@@ -733,13 +749,28 @@ impl Frames {
let markers = project . reads_markers ( ) ;
if let Some ( ( base_id , base ) ) = bases . get ( & comp . 0 )
& & * base_id ! = plain
& & comp_content_with ( project , comp , | id , c | match base . item ( id ) . map ( | i | & i . kind ) {
Some ( ItemKind ::Comp ( b ) ) if Arc ::ptr_eq ( b , c ) | | b . same_pixels_with ( c , markers ) = > Arc ::as_ptr ( b ) ,
_ = > Arc ::as_ptr ( c ) ,
& & comp_content_with ( project , comp , project , | id , c | match base . item ( id ) . map ( | i | & i . kind ) {
Some ( ItemKind ::Comp ( b ) ) if Arc ::ptr_eq ( b , c ) | | b . same_pixels_with ( c , markers ) = > b ,
_ = > c ,
} ) = = * base_id
{
return ( * base_id , Some ( base . clone ( ) ) ) ;
}
// Only layers of this comp changed: its frames outside their span (and a frame either
// side for rounding) keep drawing the same pixels, so a cut or trim recaches only the
// layers it touched (#1641).
if let Some ( ( base_id , base ) ) = bases . get ( & comp . 0 )
& & let ( Some ( old ) , Some ( new ) ) = ( base . comp ( comp ) , project . comp ( comp ) )
& & comp_content_with ( project , comp , base , | id , c | match base . item ( id ) . map ( | i | & i . kind ) {
Some ( ItemKind ::Comp ( b ) ) if id = = comp | | Arc ::ptr_eq ( b , c ) | | b . same_pixels_with ( c , markers ) = > b ,
_ = > c ,
} ) = = * base_id
& & let Some ( ( s , e ) ) = new . changed_span ( old )
& & let Ok ( mut cache ) = self . cache . lock ( )
{
let ( first , last ) = ( new . frame_rate . frame_at ( s ) . saturating_sub ( 1 ) , new . frame_rate . frame_at ( e ) . saturating_add ( 1 ) ) ;
cache . carry ( * base_id , plain , | f | f < first | | f > last ) ;
}
if bases . len ( ) > MAX_KEEPERS {
bases . clear ( ) ;
}
@@ -2168,6 +2199,156 @@ mod tests {
assert_ne! ( at ( & s ) , o1 ) ;
}
/// A cut or trim recaches only the frames of the layers it touched (#1641), and none are
/// kept when the change reaches other layers (here a child of the trimmed layer).
#[ test ]
fn an_edit_keeps_the_frames_outside_the_layers_it_changed ( ) {
use effectcraft_engine ::Session ;
use serde_json ::json ;
let mut s = Session ::default ( ) ;
let id = | v : serde_json ::Value , k : & str | v [ k ] . as_u64 ( ) . unwrap ( ) ;
let comp = ItemId ( id ( s . execute ( " comp.new " , json! ( { " name " : " Edit " , " width " : 8 , " height " : 8 , " duration " : 10 } ) ) . unwrap ( ) , " comp " ) ) ;
let a = id ( s . execute ( " layer.newSolid " , json! ( { " comp " : comp . 0 , " color " : " #ff0000 " } ) ) . unwrap ( ) , " layer " ) ;
let b = id ( s . execute ( " layer.newSolid " , json! ( { " comp " : comp . 0 , " color " : " #00ff00 " } ) ) . unwrap ( ) , " layer " ) ;
s . execute ( " layer.timing " , json! ( { " layers " : [ a ] , " in " : 0 , " out " : 4 } ) ) . unwrap ( ) ;
s . execute ( " layer.timing " , json! ( { " layers " : [ b ] , " in " : 4 , " out " : 10 } ) ) . unwrap ( ) ;
let ( fr , out ) = s . project . comp ( comp ) . map ( | c | ( c . frame_rate , c . layers . iter ( ) . find ( | l | l . id . 0 = = a ) . unwrap ( ) . out_point ) ) . unwrap ( ) ;
let frames : Vec < i64 > = ( 0 .. fr . frame_at ( Tick ::from_seconds_f64 ( 10.0 ) ) ) . collect ( ) ;
let f = Frames ::default ( ) ;
let cache_all = | s : & Session | {
let c = f . content_of ( & s . project , s . revision , comp ) ;
frames . iter ( ) . for_each ( | & n | insert ( & f , key ( c , n , 0 ) ) ) ;
c
} ;
let cached = | c : u64 | frames . iter ( ) . filter ( | & & n | f . is_cached ( & key ( c , n , 0 ) ) ) . copied ( ) . collect ::< Vec < _ > > ( ) ;
let before = cache_all ( & s ) ;
s . execute ( " time.set " , json! ( { " time " : 2.0 } ) ) . unwrap ( ) ;
s . execute ( " edit.splitLayer " , json! ( { " layers " : [ a ] } ) ) . unwrap ( ) ;
let after = f . content_of ( & s . project , s . revision , comp ) ;
assert_ne! ( after , before ) ;
let kept : Vec < i64 > = ( fr . frame_at ( out ) + 2 .. frames . len ( ) as i64 ) . collect ( ) ;
assert_eq! ( cached ( after ) , kept , " the cut layer's frames (and one either side) render again; the rest stay " ) ;
s . execute ( " layer.setParent " , json! ( { " layers " : [ b ] , " parent " : a } ) ) . unwrap ( ) ;
let parented = cache_all ( & s ) ;
s . execute ( " layer.timing " , json! ( { " layers " : [ a ] , " out " : 1 } ) ) . unwrap ( ) ;
assert_eq! ( cached ( f . content_of ( & s . project , s . revision , comp ) ) , Vec ::< i64 > ::new ( ) , " the trimmed layer's child moves everywhere " ) ;
assert_eq! ( cached ( parented ) . len ( ) , frames . len ( ) , " the earlier frames stay for undo " ) ;
}
/// Every frame an edit keeps renders as a fresh render of the edited project does: Solo
/// hides the other layers at every frame, and a camera reaches the frames whose shutter
/// samples it (#1641 review).
#[ test ]
fn the_frames_an_edit_keeps_match_a_fresh_render ( ) {
use effectcraft_engine ::Session ;
use effectcraft_engine ::render ::NoFootage ;
use serde_json ::json ;
// Cache every frame of `comp`, run `edit`, check the frames kept against fresh renders.
fn kept ( s : & mut Session , comp : ItemId , edit : serde_json ::Value ) -> Vec < i64 > {
let ( fr , n ) = s . project . comp ( comp ) . map ( | c | ( c . frame_rate , c . frame_rate . frame_at ( c . duration ) ) ) . unwrap ( ) ;
let f = Frames ::default ( ) ;
let before = f . content_of ( & s . project , s . revision , comp ) ;
( 0 .. n ) . for_each ( | i | insert ( & f , key ( before , i , 0 ) ) ) ;
let old = s . project . clone ( ) ;
s . execute ( edit [ " cmd " ] . as_str ( ) . unwrap ( ) , edit [ " params " ] . clone ( ) ) . unwrap ( ) ;
let after = f . content_of ( & s . project , s . revision , comp ) ;
let kept : Vec < i64 > = ( 0 .. n ) . filter ( | & i | f . is_cached ( & key ( after , i , 0 ) ) ) . collect ( ) ;
let render = | p , i | Renderer ::new ( p , & NoFootage , RenderOpts ::default ( ) ) . comp_frame ( comp , fr . tick_of ( i ) ) ;
for & i in & kept {
assert_eq! ( render ( & old , i ) , render ( & s . project , i ) , " kept frame {i} draws other pixels now " ) ;
}
kept
}
let id = | v : serde_json ::Value , k : & str | v [ k ] . as_u64 ( ) . unwrap ( ) ;
let mut s = Session ::default ( ) ;
// Solo on a 2– 3 s layer hides the 10 s underlay everywhere; turning it off shows it again.
let comp = ItemId ( id ( s . execute ( " comp.new " , json! ( { " name " : " Solo " , " width " : 8 , " height " : 8 , " duration " : 10 } ) ) . unwrap ( ) , " comp " ) ) ;
s . execute ( " layer.newSolid " , json! ( { " comp " : comp . 0 , " color " : " #ff0000 " } ) ) . unwrap ( ) ;
let short = id ( s . execute ( " layer.newSolid " , json! ( { " comp " : comp . 0 , " color " : " #00ff00 " } ) ) . unwrap ( ) , " layer " ) ;
s . execute ( " layer.timing " , json! ( { " layers " : [ short ] , " in " : 2 , " out " : 3 } ) ) . unwrap ( ) ;
let solo = | on : bool | json! ( { " cmd " : " layer.setSwitch " , " params " : { " layers " : [ short ] , " switch " : " solo " , " value " : on } } ) ;
assert_eq! ( kept ( & mut s , comp , solo ( true ) ) , Vec ::< i64 > ::new ( ) ) ;
assert_eq! ( kept ( & mut s , comp , solo ( false ) ) , Vec ::< i64 > ::new ( ) ) ;
// A 2– 3 s camera over a 4 s motion-blurred 3D layer, shutter 720° at phase 360°: a frame
// samples 1 to 3 frames ahead, so moving the camera reaches the 3 frames before it.
let comp = ItemId ( id (
s . execute ( " comp.new " , json! ( { " name " : " Cam " , " width " : 16 , " height " : 16 , " duration " : 4 , " frameRate " : 30 , " shutterAngle " : 720 , " shutterPhase " : 360 } ) )
. unwrap ( ) ,
" comp " ,
) ) ;
let plane = id ( s . execute ( " layer.newSolid " , json! ( { " comp " : comp . 0 , " color " : " #ff0000 " } ) ) . unwrap ( ) , " layer " ) ;
for sw in [ " threeD " , " motionBlur " ] {
s . execute ( " layer.setSwitch " , json! ( { " layers " : [ plane ] , " switch " : sw , " value " : true } ) ) . unwrap ( ) ;
}
let cam = id ( s . execute ( " layer.newCamera " , json! ( { " comp " : comp . 0 } ) ) . unwrap ( ) , " layer " ) ;
s . execute ( " layer.timing " , json! ( { " layers " : [ cam ] , " in " : 2 , " out " : 3 } ) ) . unwrap ( ) ;
let moved =
kept ( & mut s , comp , json! ( { " cmd " : " prop.set " , " params " : { " comp " : comp . 0 , " layer " : cam , " path " : " transform/position " , " value " : [ 40 , 8 , - 30 ] } } ) ) ;
assert! ( moved . contains ( & 0 ) & & moved . contains ( & 100 ) , " frames far from the camera stay: {moved:?} " ) ;
assert! ( ! ( 56 ..= 91 ) . any ( | f | moved . contains ( & f ) ) , " frames whose shutter samples the camera render again: {moved:?} " ) ;
}
#[ test ]
fn short_solid_additions_and_removals_keep_unaffected_frames ( ) {
use effectcraft_engine ::Session ;
use serde_json ::json ;
let mut s = Session ::default ( ) ;
let comp = ItemId ( s . execute ( " comp.new " , json! ( { " width " : 8 , " height " : 8 , " frameRate " : 30 , " duration " : 20 } ) ) . unwrap ( ) [ " comp " ] . as_u64 ( ) . unwrap ( ) ) ;
s . execute ( " layer.newSolid " , json! ( { " color " : " #ff0000 " } ) ) . unwrap ( ) ;
let f = Frames ::default ( ) ;
let cache_all = | s : & Session | {
let content = f . content_of ( & s . project , s . revision , comp ) ;
( 0 .. 600 ) . for_each ( | n | insert ( & f , key ( content , n , 0 ) ) ) ;
} ;
let cached = | s : & Session | f . cached_frames ( & key ( f . content_of ( & s . project , s . revision , comp ) , 0 , 0 ) ) ;
// 2– 3 s, a quarter frame of the default shutter and a frame of rounding either side.
let kept : Vec < i64 > = ( 0 ..= 57 ) . chain ( 92 .. 600 ) . collect ( ) ;
cache_all ( & s ) ;
let solid = s . execute ( " layer.newSolid " , json! ( { " color " : " #00ff00 " } ) ) . unwrap ( ) [ " layer " ] . as_u64 ( ) . unwrap ( ) ;
s . execute ( " layer.timing " , json! ( { " layers " : [ solid ] , " in " : 2 , " out " : 3 } ) ) . unwrap ( ) ;
assert_eq! ( cached ( & s ) , kept , " adding a source invalidates only frames 58..=91 " ) ;
cache_all ( & s ) ;
s . execute ( " edit.undo " , json! ( { } ) ) . unwrap ( ) ;
s . execute ( " edit.undo " , json! ( { } ) ) . unwrap ( ) ;
assert_eq! ( cached ( & s ) , ( 0 .. 600 ) . collect ::< Vec < _ > > ( ) , " undoing the addition restores every frame " ) ;
s . execute ( " edit.redo " , json! ( { } ) ) . unwrap ( ) ;
s . execute ( " edit.redo " , json! ( { } ) ) . unwrap ( ) ;
assert_eq! ( cached ( & s ) , ( 0 .. 600 ) . collect ::< Vec < _ > > ( ) , " redoing the addition restores every frame " ) ;
s . execute ( " edit.clear " , json! ( { " layers " : [ solid ] } ) ) . unwrap ( ) ;
assert_eq! ( cached ( & s ) , kept , " removing its layer invalidates the same span " ) ;
}
#[ test ]
fn adding_a_source_does_not_hide_changes_to_existing_dependencies ( ) {
use effectcraft_engine ::Session ;
use serde_json ::json ;
for ( command , params ) in [
( " layer.settings " , json! ( { " comp " : " Outer " , " layer " : " Underlay " , " color " : " #0000ff " } ) ) ,
( " prop.set " , json! ( { " comp " : " Inner " , " layer " : " Inner solid " , " path " : " transform/opacity " , " value " : 40 } ) ) ,
] {
let mut s = Session ::default ( ) ;
let comp = ItemId (
s . execute ( " comp.new " , json! ( { " name " : " Outer " , " width " : 8 , " height " : 8 , " frameRate " : 30 , " duration " : 20 } ) ) . unwrap ( ) [ " comp " ] . as_u64 ( ) . unwrap ( ) ,
) ;
s . execute ( " layer.newSolid " , json! ( { " name " : " Underlay " , " color " : " #ff0000 " } ) ) . unwrap ( ) ;
s . execute ( " comp.new " , json! ( { " name " : " Inner " , " width " : 8 , " height " : 8 , " duration " : 20 } ) ) . unwrap ( ) ;
s . execute ( " layer.newSolid " , json! ( { " name " : " Inner solid " , " color " : " #00ff00 " } ) ) . unwrap ( ) ;
s . execute ( " comp.open " , json! ( { " comp " : " Outer " } ) ) . unwrap ( ) ;
s . execute ( " layer.addItem " , json! ( { " item " : " Inner " } ) ) . unwrap ( ) ;
let f = Frames ::default ( ) ;
let before = f . content_of ( & s . project , s . revision , comp ) ;
( 0 .. 600 ) . for_each ( | n | insert ( & f , key ( before , n , 0 ) ) ) ;
s . execute ( command , params ) . unwrap ( ) ;
let solid = s . execute ( " layer.newSolid " , json! ( { " color " : " #ffffff " } ) ) . unwrap ( ) [ " layer " ] . as_u64 ( ) . unwrap ( ) ;
s . execute ( " layer.timing " , json! ( { " layers " : [ solid ] , " in " : 2 , " out " : 3 } ) ) . unwrap ( ) ;
let after = f . content_of ( & s . project , s . revision , comp ) ;
assert! ( f . cached_frames ( & key ( after , 0 , 0 ) ) . is_empty ( ) , " {command} changes pixels outside the added layer " ) ;
}
}
/// Adding, moving or deleting a marker (Num* while a preview plays) keeps the cached frames:
/// the sound would otherwise stop at the next frame that has to be rendered again (#1083).
/// Only expressions that read markers make them part of the pixels.