Render: CC Particle World layers with expressions aren't layer cached (#1373)

The simulation checks its checkpoints against the Birth Rate history,
but a cached layer is returned before the effect runs. The layer key
sees the layer's keyframes and its current values, not what an
expression returned at earlier times: a Birth Rate expression reading
another layer could change the history while the current rate, 0, stayed
the same.

effects::reads_param_history names CC Particle World. The renderer
already declines to cache layers whose time effects (ec.time.*) meet
expressions. It now treats Particle World the same way, in the layer key,
the input key (only for the effects that input runs) and the dependency
walk.
This commit is contained in:
tylerjenningsw
2026-10-13 21:59:03 -04:00
parent f263a32ab7
commit aa300d7f2e
3 changed files with 31 additions and 11 deletions
+21 -8
View File
@@ -484,10 +484,11 @@ fn hash_values(h: &mut KeyHasher, ctx: &EvalCtx, layer: &Layer, g: &PropGroup) {
/// time, or `None` when the layer must not be cached (footage, precomps, cameras, adjustment
/// layers…).
pub fn layer_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool) -> Option<u64> {
// Time effects see property values at other times. Keyframes are part of the hashed
// structure (and the layer time is folded in), but expressions may read other layers at
// those times, which the key cannot see: such layers are not cached.
if reads_other_times(layer) && layer.props.children.iter().any(|c| matches!(c, Node::Group(g) if g.match_id != "transform" && has_expression(g))) {
// Time effects (and CC Particle World's Birth Rate history) see property values at other
// times. Keyframes are part of the hashed structure (and the layer time is folded in), but
// expressions may read other layers at those times, which the key cannot see: such layers
// are not cached.
if history_unkeyable(layer, None) {
return None;
}
key_with(ctx, layer, scale, draft, blur, false)
@@ -497,7 +498,7 @@ pub fn layer_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bo
/// effects (see `Renderer::layer_input`). Unlike [`layer_key`], footage layers are cached here
/// (keyed by item and source time), since Time effects read many neighbouring frames.
pub fn input_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, effects: usize) -> Option<u64> {
if layer.switches.adjustment {
if layer.switches.adjustment || history_unkeyable(layer, Some(effects)) {
return None;
}
let base = key_any(ctx, layer, scale, draft, blur, true, Some(effects))?;
@@ -506,14 +507,26 @@ pub fn input_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bo
Some(h.finish())
}
/// Does the layer run an effect that reads the layer at other times (Echo, Timewarp…)?
fn reads_other_times(layer: &Layer) -> bool {
/// Does the layer run (among its first `effects` effects, all when `None`) an effect that reads
/// the layer at other times (Echo, Timewarp…) or its own parameters' history (CC Particle
/// World's Birth Rate, #1373)?
fn reads_other_times(layer: &Layer, effects: Option<usize>) -> bool {
layer.switches.effects
&& layer.effects().is_some_and(|fx| {
fx.groups().any(|g| g.enabled && matches!(&g.kind, effectcraft_project::GroupKind::Effect { effect } if effect.starts_with("ec.time.")))
fx.groups().take(effects.unwrap_or(usize::MAX)).any(|g| {
g.enabled
&& matches!(&g.kind, effectcraft_project::GroupKind::Effect { effect }
if effect.starts_with("ec.time.") || effectcraft_effects::reads_param_history(effect))
})
})
}
/// The layer reads values at other times (see [`reads_other_times`]) and has expressions
/// (outside Transform), whose results at those times the key can't see.
fn history_unkeyable(layer: &Layer, effects: Option<usize>) -> bool {
reads_other_times(layer, effects) && layer.props.children.iter().any(|c| matches!(c, Node::Group(g) if g.match_id != "transform" && has_expression(g)))
}
fn has_expression(g: &PropGroup) -> bool {
g.children.iter().any(|c| match c {
Node::Prop(p) => p.has_expression(),