#2104: bound audio comparisons across reused precomps

This commit is contained in:
FredApps committed 2026-11-10 13:10:10 +01:00
1 parent 9d504f6722
commit 31da676415
2 files changed
+40 -6

No files matched your search

+12 -6
View File
@@ -18,7 +18,7 @@
//! **Scrubbing:** Ctrl/Cmd-dragging the current-time indicator plays a short snippet of the mix
//! at each new frame ([`AudioScrub`], `playback.scrubAudio`), as in After Effects.
use std::collections::VecDeque;
use std::collections::{HashSet, VecDeque};
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::JoinHandle;
@@ -338,11 +338,17 @@ impl Drop for AudioPlayback {
/// Effects switch, Audio Levels or effects, or the same for a nested comp. Other edits (video
/// properties, names, labels) do not change the mix unless an audio expression can read them.
fn audio_changed(old: &Project, new: &Project, comp: ItemId) -> bool {
fn comps_differ(old: &Project, new: &Project, comp: ItemId, depth: usize) -> bool {
let (Some(a), Some(b)) = (old.comp(comp), new.comp(comp)) else { return old.comp(comp).is_some() != new.comp(comp).is_some() };
if depth >= 16 {
audio_changed_inner(old, new, comp, &mut HashSet::new())
}
/// Each shared precomp is compared once per depth. A depth limit alone still visits an
/// exponentially large number of paths in a small graph of reused precomps, blocking the UI.
fn audio_changed_inner(old: &Project, new: &Project, comp: ItemId, seen: &mut HashSet<(ItemId, usize)>) -> bool {
fn comps_differ(old: &Project, new: &Project, comp: ItemId, depth: usize, seen: &mut HashSet<(ItemId, usize)>) -> bool {
if depth >= 16 || !seen.insert((comp, depth)) {
return false;
}
let (Some(a), Some(b)) = (old.comp(comp), new.comp(comp)) else { return old.comp(comp).is_some() != new.comp(comp).is_some() };
a.layers.len() != b.layers.len()
|| a.layers.iter().zip(&b.layers).any(|(x, y)| {
x.id != y.id
@@ -360,7 +366,7 @@ fn audio_changed(old: &Project, new: &Project, comp: ItemId) -> bool {
|| audio_has_expression(y)
|| match &x.source {
LayerSource::Footage { item } => footage_differs(old, new, *item),
LayerSource::Comp { item } => comps_differ(old, new, *item, depth + 1),
LayerSource::Comp { item } => comps_differ(old, new, *item, depth + 1, seen),
_ => false,
}
})
@@ -386,7 +392,7 @@ fn audio_changed(old: &Project, new: &Project, comp: ItemId) -> bool {
};
f(old) != f(new)
}
comps_differ(old, new, comp, 0)
comps_differ(old, new, comp, 0, seen)
}
/// Mix one block at the feeder's cursor into the feed and move the cursor on (wrapping or
+28
View File
@@ -179,3 +179,31 @@ fn video_edits_keep_the_queue_unless_audio_expressions_can_read_them() {
}
}
}
#[test]
fn shared_nested_comps_are_compared_once_per_depth() {
let mut project = Project::default();
let mut child = None;
for _ in 0..10 {
let mut comp = Comp::new(64, 36, FrameRate::new(30, 1), Tick::from_seconds_f64(1.0));
if let Some(item) = child {
for _ in 0..10 {
let mut layer = build::layer(&mut project, &comp, "Nested", LayerSource::Comp { item }, (64, 36), None);
layer.switches.audio = false;
comp.layers.push(layer);
}
}
child = Some(project.add_item("Nested", Label::Sandstone, None, ItemKind::Comp(comp.into())));
}
let root = child.unwrap();
let mut edited = project.clone();
edited.comp_mut(root).unwrap().layers[0].name = "Renamed".into();
let mut seen = std::collections::HashSet::new();
assert!(!super::audio_changed_inner(&project, &edited, root, &mut seen));
assert_eq!(seen.len(), 10, "compare the ten comps, not a billion paths through them");
let leaf = *seen.iter().find(|(_, depth)| *depth == 9).map(|(item, _)| item).unwrap();
let mut layer = project.comp(root).unwrap().layers[0].clone();
layer.source = LayerSource::Null;
edited.comp_mut(leaf).unwrap().layers.push(layer);
assert!(super::audio_changed(&project, &edited, root), "memoization must still find a nested edit");
}