mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 05:58:19 -06:00
Merge pull request #701 from vbasky/fix/687-adjustment-footprint-key
Cache/GPU: adjustment footprint ignores the effect stack; upload starvation is reported (#687)
This commit is contained in:
+49
-25
@@ -527,7 +527,7 @@ pub fn input_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bo
|
||||
if layer.switches.adjustment || history_unkeyable(layer, Some(effects)) {
|
||||
return None;
|
||||
}
|
||||
let base = key_any(ctx, layer, scale, draft, blur, true, Some(effects))?;
|
||||
let base = key_any(ctx, layer, scale, draft, blur, true, EffectScope::First(effects))?;
|
||||
let mut h = KeyHasher(base ^ 0x5bd1_e995_7a3c_11d3);
|
||||
effects.hash(&mut h);
|
||||
Some(h.finish())
|
||||
@@ -588,25 +588,39 @@ fn has_expression(g: &PropGroup) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// How far the effect stack reaches into a cache key. [`EffectScope::Footprint`] keys an
|
||||
/// adjustment layer's footprint — its source through its masks, without any effects — so the
|
||||
/// stack must not reach the key at all (#687).
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum EffectScope {
|
||||
/// The whole stack (the processed layer).
|
||||
All,
|
||||
/// Only the first `n` effects (a layer's input, see [`input_key`]).
|
||||
First(usize),
|
||||
/// None of it (an adjustment layer's footprint).
|
||||
Footprint,
|
||||
}
|
||||
|
||||
/// Cache key for an adjustment layer's footprint (its source through its masks, see
|
||||
/// `Renderer::adjustment_footprint`): the GPU compositor then uploads it once, not every frame.
|
||||
pub fn footprint_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool) -> Option<u64> {
|
||||
// The footprint is the source through the masks, without the effects: a time-dependent
|
||||
// effect (Noise on a finishing adjustment layer) must not re-key it every frame (on the GPU,
|
||||
// a full-frame re-upload per frame).
|
||||
Some(derive(key_any(ctx, layer, scale, draft, blur, false, Some(0))?, 0xf007_9417))
|
||||
// a full-frame re-upload per frame). Shutter, paint, the comp scene and effect dependencies
|
||||
// stay out of this key too; a mask still does.
|
||||
Some(derive(key_any(ctx, layer, scale, draft, blur, false, EffectScope::Footprint)?, 0xf007_9417))
|
||||
}
|
||||
|
||||
fn key_with(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, footage: bool) -> Option<u64> {
|
||||
if layer.switches.adjustment {
|
||||
return None;
|
||||
}
|
||||
key_any(ctx, layer, scale, draft, blur, footage, None)
|
||||
key_any(ctx, layer, scale, draft, blur, footage, EffectScope::All)
|
||||
}
|
||||
|
||||
/// `blur`: the layer is motion blurred in this render (`Renderer::mb_on`). `effects`: key only
|
||||
/// the first that many effects (a layer's input, see [`input_key`]).
|
||||
fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, footage: bool, effects: Option<usize>) -> Option<u64> {
|
||||
/// `blur`: the layer is motion blurred in this render (`Renderer::mb_on`). `effects`: how far
|
||||
/// the effect stack reaches into the key (see [`EffectScope`]).
|
||||
fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, footage: bool, effects: EffectScope) -> Option<u64> {
|
||||
let mut h = KeyHasher(0xcbf2_9ce4_8422_2325);
|
||||
ctx.comp.frame_rate.as_f64().to_bits().hash(&mut h);
|
||||
ctx.comp.enable_frame_blending.hash(&mut h);
|
||||
@@ -651,8 +665,10 @@ fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, fo
|
||||
// (Audio, Lock and Shy don't change pixels.)
|
||||
hash_debug(&mut h, &layer.switches.pixels());
|
||||
// Mask motion blur and effects that read the shutter depend on whether the layer is motion
|
||||
// blurred in this render and on the comp's shutter.
|
||||
if layer.masks().is_some_and(|m| !m.children.is_empty()) || layer.effects().is_some_and(|fx| !fx.children.is_empty()) {
|
||||
// blurred in this render and on the comp's shutter. A footprint has no effects, so only its
|
||||
// masks put the shutter in the key.
|
||||
if layer.masks().is_some_and(|m| !m.children.is_empty()) || (effects != EffectScope::Footprint && layer.effects().is_some_and(|fx| !fx.children.is_empty()))
|
||||
{
|
||||
blur.hash(&mut h);
|
||||
ctx.comp.enable_motion_blur.hash(&mut h);
|
||||
ctx.comp.shutter_angle.to_bits().hash(&mut h);
|
||||
@@ -666,23 +682,29 @@ fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, fo
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// A footprint is its source through its masks: the effect stack is not in it at all.
|
||||
// A layer's input runs only its first effects: the later ones (a Puppet whose pins are
|
||||
// dragged, #212) don't change it, so it stays cached.
|
||||
if let (Some(n), Node::Group(g)) = (effects, c)
|
||||
if let Node::Group(g) = c
|
||||
&& g.match_id == "effects"
|
||||
{
|
||||
let mut first = g.clone();
|
||||
let mut seen = 0;
|
||||
first.children.retain(|e| match e {
|
||||
Node::Prop(_) => true,
|
||||
Node::Group(_) => {
|
||||
seen += 1;
|
||||
seen <= n
|
||||
}
|
||||
});
|
||||
hash_debug(&mut h, &first);
|
||||
hash_values(&mut h, ctx, layer, &first);
|
||||
continue;
|
||||
if effects == EffectScope::Footprint {
|
||||
continue;
|
||||
}
|
||||
if let EffectScope::First(n) = effects {
|
||||
let mut first = g.clone();
|
||||
let mut seen = 0;
|
||||
first.children.retain(|e| match e {
|
||||
Node::Prop(_) => true,
|
||||
Node::Group(_) => {
|
||||
seen += 1;
|
||||
seen <= n
|
||||
}
|
||||
});
|
||||
hash_debug(&mut h, &first);
|
||||
hash_values(&mut h, ctx, layer, &first);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Static structure: values, keyframes, expressions, enabled flags, effect ids, modes.
|
||||
hash_debug(&mut h, c);
|
||||
@@ -691,7 +713,9 @@ fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, fo
|
||||
}
|
||||
}
|
||||
// Paint strokes appear and disappear with their Duration spans (not property values).
|
||||
if layer.switches.effects
|
||||
// A footprint does not include them.
|
||||
if effects != EffectScope::Footprint
|
||||
&& layer.switches.effects
|
||||
&& let Some(fx) = layer.effects()
|
||||
{
|
||||
let lt = layer.layer_time(ctx.time).seconds();
|
||||
@@ -704,14 +728,14 @@ fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, fo
|
||||
if mask_motion_blur(ctx, layer, blur) {
|
||||
layer.layer_time(ctx.time).0.hash(&mut h);
|
||||
}
|
||||
if time_dependent(layer, effects != Some(0)) {
|
||||
if time_dependent(layer, effects != EffectScope::Footprint) {
|
||||
layer.layer_time(ctx.time).0.hash(&mut h);
|
||||
// Effects also see the layer's time mapping.
|
||||
layer.start_time.0.hash(&mut h);
|
||||
layer.stretch.to_bits().hash(&mut h);
|
||||
}
|
||||
// Effects that see the comp camera or lights (#397) change when they or the layer move.
|
||||
if reads_comp_scene(layer) {
|
||||
if effects != EffectScope::Footprint && reads_comp_scene(layer) {
|
||||
hash_debug(&mut h, &crate::comp_scene(ctx, layer));
|
||||
}
|
||||
Some(h.finish())
|
||||
|
||||
+8
-2
@@ -6,9 +6,15 @@ use crate::EvalCtx;
|
||||
use effectcraft_project::{ItemId, ItemKind, Layer, LayerId, LayerSource, LightKind, Node, PropGroup};
|
||||
use std::hash::Hash;
|
||||
|
||||
pub(super) fn hash(h: &mut KeyHasher, ctx: &EvalCtx, layer: &Layer, effects: Option<usize>) -> Option<()> {
|
||||
pub(super) fn hash(h: &mut KeyHasher, ctx: &EvalCtx, layer: &Layer, effects: super::EffectScope) -> Option<()> {
|
||||
// A footprint runs no effects: nothing they read is in its pixels or its key.
|
||||
let limit = match effects {
|
||||
super::EffectScope::Footprint => return Some(()),
|
||||
super::EffectScope::All => None,
|
||||
super::EffectScope::First(n) => Some(n),
|
||||
};
|
||||
let mut deps = Dependencies { h, path: vec![(ctx.comp_id, layer.id)], remaining: 1024, other_times: super::reads_other_times(layer, None) };
|
||||
deps.effects(ctx, layer, effects)
|
||||
deps.effects(ctx, layer, limit)
|
||||
}
|
||||
|
||||
struct Dependencies<'a> {
|
||||
|
||||
@@ -929,6 +929,54 @@ fn default_effect_points_sit_at_the_centre_of_a_shape_layer() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #687: an adjustment layer's footprint (its source through its masks) must not be keyed by
|
||||
/// its effect stack. Noise on a finishing adjustment changes every frame, and letting that
|
||||
/// reach the key re-rendered and re-uploaded a full-frame buffer every frame. A moving mask
|
||||
/// must still re-key it, and the cached buffer must follow the mask.
|
||||
#[test]
|
||||
fn adjustment_footprint_key_ignores_the_effect_stack() {
|
||||
let (mut p, cid, comp) = setup();
|
||||
let mut adj = solid(&mut p, &comp, [1.0, 1.0, 1.0], 200, 100);
|
||||
adj.switches.adjustment = true;
|
||||
add_effect_200(&mut p, &mut adj, "ec.noise.noise", &[("amount", Value::Scalar(50.0))]);
|
||||
let key = |p: &Project, t: f64| {
|
||||
let c = p.comp(cid).unwrap();
|
||||
crate::cache::footprint_key(&crate::EvalCtx::new(p, cid, c, Tick::from_seconds_f64(t)), &c.layers[0], 1.0, false, false).unwrap()
|
||||
};
|
||||
let mut next = p.next_id;
|
||||
let mut m = build::mask(&mut Ids(&mut next), "Mask 1", ShapePath::rect([50.0, 50.0], 40.0, 40.0), MaskMode::Add, [255, 255, 0]);
|
||||
p.next_id = next;
|
||||
m.prop_mut("path").unwrap().keys = vec![
|
||||
Keyframe::new(Tick::ZERO, Value::Path(ShapePath::rect([50.0, 50.0], 40.0, 40.0))),
|
||||
Keyframe::new(Tick::from_seconds_f64(1.0), Value::Path(ShapePath::rect([150.0, 50.0], 40.0, 40.0))),
|
||||
];
|
||||
// Effects alone must not put the shutter in the footprint key. Masks, added below, may.
|
||||
p.comp_mut(cid).unwrap().layers.push(adj.clone());
|
||||
let bare = key(&p, 0.0);
|
||||
p.comp_mut(cid).unwrap().shutter_angle = 90.0;
|
||||
assert_eq!(bare, key(&p, 0.0), "the comp shutter must not re-key a footprint");
|
||||
p.comp_mut(cid).unwrap().shutter_angle = 180.0;
|
||||
p.comp_mut(cid).unwrap().layers[0].props.sub_mut("masks").unwrap().children.push(m.into());
|
||||
let foot = |p: &Project, t: f64| {
|
||||
let c = p.comp(cid).unwrap();
|
||||
let mut r = crate::Renderer::new(p, &crate::NoFootage, crate::RenderOpts::default());
|
||||
let cache = crate::LayerCache::default();
|
||||
r.cache = Some(&cache);
|
||||
let layer = c.layers[0].clone();
|
||||
let b = r.adjustment_footprint(&crate::EvalCtx::new(p, cid, c, Tick::from_seconds_f64(t)), &layer).unwrap();
|
||||
b.img.get(50, 50)
|
||||
};
|
||||
assert_ne!(key(&p, 0.0), key(&p, 0.5), "an animated mask path must re-key the footprint");
|
||||
assert_ne!(key(&p, 0.0), key(&p, 1.0));
|
||||
assert_ne!(foot(&p, 0.0), foot(&p, 1.0), "the moved mask must reach the cached footprint");
|
||||
assert_eq!(key(&p, 1.0), key(&p, 1.5), "a per-frame effect on the adjustment re-keyed its footprint");
|
||||
assert_eq!(key(&p, 1.0), key(&p, 3.5), "the footprint key followed the clock past the mask");
|
||||
assert_eq!(foot(&p, 1.0), foot(&p, 3.5));
|
||||
let before = key(&p, 0.0);
|
||||
p.comp_mut(cid).unwrap().layers[0].props.prop_mut("effects/#1/amount").unwrap().value = Value::Scalar(90.0);
|
||||
assert_eq!(before, key(&p, 0.0), "editing an effect parameter must not re-key the footprint");
|
||||
}
|
||||
|
||||
/// #659: a tiling effect on a text layer tiles across the layer's whole region. The glyphs
|
||||
/// were cut off at their own bounding box: the effect computed over the comp-sized layer rect
|
||||
/// but wrote into the painted (glyph-bounds) buffer alone. Tiling shrinks each copy, so the
|
||||
|
||||
Reference in New Issue
Block a user