Markers: the disk cache key ignores them too unless an expression reads them (#1083)

Project::reads_markers and Comp::pixel_form_with are shared by the RAM and disk frame caches.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-10-12 20:52:29 +02:00
co-authored by Claude Sonnet 5.5
parent 6a8166491c
commit cdd6d83abb
4 changed files with 58 additions and 16 deletions
+20
View File
@@ -136,3 +136,23 @@ fn footage_changes_change_the_salt() {
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");
}
+32 -1
View File
@@ -881,14 +881,30 @@ impl Comp {
/// The comp with those switches and the Resolution at their defaults (borrowed when they
/// already are), for keys that hash the whole comp.
pub fn pixel_form(&self) -> std::borrow::Cow<'_, Comp> {
if !self.hide_shy && self.resolution.is_auto() && self.layers.iter().all(|l| l.switches == l.switches.pixels()) {
self.pixel_form_with(true)
}
/// [`Self::pixel_form`], also without the comp's and its layers' markers when not
/// `with_markers` (see [`Self::same_pixels_with`] and [`Project::reads_markers`]).
pub fn pixel_form_with(&self, with_markers: bool) -> std::borrow::Cow<'_, Comp> {
if !self.hide_shy
&& self.resolution.is_auto()
&& self.layers.iter().all(|l| l.switches == l.switches.pixels())
&& (with_markers || (self.markers.is_empty() && self.layers.iter().all(|l| l.markers.is_empty())))
{
return std::borrow::Cow::Borrowed(self);
}
let mut c = self.clone();
c.hide_shy = false;
c.resolution = Resolution::Auto;
if !with_markers {
c.markers.clear();
}
for l in &mut c.layers {
l.switches = l.switches.pixels();
if !with_markers {
l.markers.clear();
}
}
std::borrow::Cow::Owned(c)
}
@@ -1339,6 +1355,21 @@ impl Default for Project {
}
impl Project {
/// Some expression mentions markers (`marker.key(1)`, `numKeys`, `nearestKey`...). Only then
/// do the markers of comps and layers affect what is rendered, so frame caches ignore them
/// otherwise.
pub fn reads_markers(&self) -> bool {
let mut found = false;
for (_, c) in self.comps() {
for l in &c.layers {
if !found {
l.props.walk("", &mut |_, p| found |= p.expr.as_ref().is_some_and(|e| e.text.to_ascii_lowercase().contains("marker")));
}
}
}
found
}
/// Allocate an id (items, layers and property uids share one counter).
pub fn alloc(&mut self) -> u64 {
let id = self.next_id;
+5 -2
View File
@@ -780,7 +780,8 @@ pub fn footage_salt(project: &effectcraft_project::Project) -> u64 {
/// Content key of a composition's pixels: project render settings, the comp and everything it
/// uses (nested comps, footage, solids, their proxies and Use Proxy switches, footage and proxy
/// files' size and time). With expressions in play (which can read any comp) the whole project
/// counts. Switches that don't change pixels (Audio, Lock, Shy, Hide Shy Layers) don't count.
/// counts. Switches that don't change pixels (Audio, Lock, Shy, Hide Shy Layers) don't count, and
/// neither do markers unless an expression mentions them.
pub fn comp_content_key(project: &effectcraft_project::Project, comp: effectcraft_project::ItemId) -> u128 {
use effectcraft_project::{ItemKind, LayerSource};
let mut h = Hash128::default();
@@ -805,13 +806,15 @@ pub fn comp_content_key(project: &effectcraft_project::Project, comp: effectcraf
}
}
}
// Markers are pixels only to the expressions that read them (#1083).
let markers = project.reads_markers();
let ids: Vec<effectcraft_project::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 };
h.write_u64(id.0);
h.write(it.name.as_bytes());
match &it.kind {
ItemKind::Comp(c) => hash_debug(&mut h, &c.pixel_form()),
ItemKind::Comp(c) => hash_debug(&mut h, &c.pixel_form_with(markers)),
k => hash_debug(&mut h, k),
}
if let ItemKind::Footage(f) = &it.kind {
+1 -13
View File
@@ -291,18 +291,6 @@ pub fn comp_content(project: &Project, comp: ItemId) -> u64 {
comp_content_with(project, comp, |_, c| Arc::as_ptr(c))
}
/// Some expression in the project mentions markers (`marker.key(1)`, `numKeys`, `nearestKey`…).
/// Only then do the markers of its comps and layers affect what is rendered.
fn reads_markers(project: &Project) -> bool {
fn group(g: &PropGroup) -> bool {
g.children.iter().any(|c| match c {
Node::Prop(p) => p.expr.as_ref().is_some_and(|e| e.text.to_ascii_lowercase().contains("marker")),
Node::Group(sub) => group(sub),
})
}
project.comps().any(|(_, c)| c.layers.iter().any(|l| group(&l.props)))
}
/// [`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 {
use std::hash::{Hash, Hasher};
@@ -716,7 +704,7 @@ impl Frames {
let plain = comp_content(project, comp);
let Ok(mut bases) = self.bases.lock() else { return (plain, None) };
// Markers are pixels only to the expressions that read them (#1083).
let markers = reads_markers(project);
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) {