mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
GPU: a layer's effect stack runs resident on the GPU and is placed directly, instead of a readback and re-upload around every GPU effect run (#687)
This commit is contained in:
1 parent
d8ff1189dc
commit
bc6a145203
3 files changed
+119
No files matched your search
@@ -979,3 +979,68 @@ fn upload_budget_bounds_the_layer_textures_kept() {
|
||||
assert!(up > 0, "over budget, the footprints must upload again");
|
||||
assert!(a == b && b == c, "the upload budget changed the frame");
|
||||
}
|
||||
|
||||
/// Layers with effect stacks run them resident on the GPU (`walk::draw_resident_fx`): GPU
|
||||
/// effects, a CPU-only effect in the middle (read back, run, upload), opacity and a blend mode,
|
||||
/// a shape layer whose effects run in effect space from its bounds' corner; 8 and 32 bpc, full
|
||||
/// and half resolution; all match the CPU.
|
||||
#[test]
|
||||
fn effect_stacks_render_resident() {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.footage(97, 61);
|
||||
s.push(bg);
|
||||
let mut a = s.footage(70, 44);
|
||||
s.effect(&mut a, "ec.blur.gaussian", &[("blurriness", n(4.0))]);
|
||||
s.effect(&mut a, "ec.generate.fractal", &[]);
|
||||
s.effect(&mut a, "ec.color.levels", &[("gamma", n(1.4))]);
|
||||
set(&mut a, "transform/rotation", Value::Scalar(20.0));
|
||||
set(&mut a, "transform/opacity", Value::Scalar(70.0));
|
||||
a.blend_mode = BlendMode::Overlay;
|
||||
s.push(a);
|
||||
let mut l = build::layer(&mut s.p, &s.comp, "Shape", LayerSource::Shape, (97, 61), None);
|
||||
let mut next = s.p.next_id;
|
||||
let mut ids = Ids(&mut next);
|
||||
let rect = build::shape_rect(&mut ids, [50.0, 30.0], [5.0, -4.0], 0.0);
|
||||
let fill = build::shape_fill(&mut ids, [0.2, 0.8, 0.3, 1.0]);
|
||||
let g = build::shape_group(&mut ids, "Rectangle 1", vec![rect, fill]);
|
||||
s.p.next_id = next;
|
||||
l.props.sub_mut("contents").expect("a shape layer has contents").children.push(g.into());
|
||||
s.effect(&mut l, "ec.blur.gaussian", &[("blurriness", n(3.0))]);
|
||||
s.effect(&mut l, "ec.stylize.mosaic", &[]);
|
||||
s.push(l);
|
||||
let t = Tick::from_seconds_f64(0.2);
|
||||
check(&format!("effect stacks {depth:?}"), compare_at(&s, opts(), t), 0.0);
|
||||
check(&format!("effect stacks half {depth:?}"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, t), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer whose effects give the same result as last frame draws from the layer cache (its
|
||||
/// processed buffer uploads once) instead of re-running the stack on the GPU every frame.
|
||||
#[test]
|
||||
fn static_effect_layers_draw_from_the_cache() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let mut s = Scene::new(BitDepth::Bpc8);
|
||||
let mut l = s.solid([0.6, 0.4, 0.2], 80, 50);
|
||||
s.effect(&mut l, "ec.blur.gaussian", &[("blurriness", n(3.0))]);
|
||||
s.push(l);
|
||||
let cache = effectcraft_render::cache::LayerCache::new(64 << 20);
|
||||
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
|
||||
r.accel = Some(g);
|
||||
r.cache = Some(&cache);
|
||||
let comp = s.p.comp(s.cid).expect("the scene's comp");
|
||||
let layer = comp.layers.last().expect("the layer");
|
||||
let t = |k: u64| Tick::from_seconds_f64(k as f64 / 30.0);
|
||||
assert!(r.layer_is_static(&effectcraft_render::EvalCtx::new(&s.p, s.cid, comp, t(2)), layer));
|
||||
let mut frames = Vec::new();
|
||||
for k in 0..3 {
|
||||
let before = g.ctx.transfer_stats().upload_bytes;
|
||||
frames.push(g.render(&r, s.cid, t(k)).expect("the GPU renders the frame").data);
|
||||
let up = g.ctx.transfer_stats().upload_bytes - before;
|
||||
if k == 2 {
|
||||
assert_eq!(up, 0, "the cached layer uploaded again");
|
||||
}
|
||||
}
|
||||
assert!(frames[1] == frames[2]);
|
||||
check("static effect layer vs CPU", compare_at(&s, opts(), t(2)), 0.0);
|
||||
}
|
||||
@@ -217,6 +217,9 @@ fn draw_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &
|
||||
if let Some(done) = draw_precomp(e, r, ctx, layer, canvas, opacity) {
|
||||
return done;
|
||||
}
|
||||
if let Some(done) = draw_resident_fx(e, r, ctx, layer, canvas, opacity) {
|
||||
return done;
|
||||
}
|
||||
let Some(st) = r.styled_layer(ctx, layer) else { return Some(()) };
|
||||
if st.plain {
|
||||
return composite_layer(e, r, ctx, layer, &st.body, canvas, opacity);
|
||||
@@ -246,6 +249,44 @@ fn draw_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// A plain layer with effects, its effect stack run on the GPU: the source (+ masks, from the
|
||||
/// layer cache) uploads once while unchanged, the effects stay resident and the result is
|
||||
/// placed directly; the CPU path instead runs each GPU-capable effect run through an
|
||||
/// upload → kernels → readback, then uploads the result again to place it. `None`: not this
|
||||
/// kind of layer (draw it the usual way); `Some(None)`: the GPU failed.
|
||||
fn draw_resident_fx(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &mut GpuImage, opacity: f32) -> Option<Option<()>> {
|
||||
if !layer.switches.effects
|
||||
|| layer.effects().is_none_or(|f| f.children.is_empty())
|
||||
|| styles::active(ctx, layer)
|
||||
|| layer.preserve_transparency
|
||||
|| r.track_matte(ctx, layer).is_some()
|
||||
// Unchanged since the last frame: the cached result uploads once (the usual path).
|
||||
|| r.layer_is_static(ctx, layer)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let input = r.layer_input(ctx, layer, 0)?;
|
||||
if input.img.is_empty() {
|
||||
return Some(Some(()));
|
||||
}
|
||||
let Some(src) = e.g.upload_buf(&input) else { return Some(None) };
|
||||
let pipe = r.pipe();
|
||||
// `layer_input` is in effect space: run the stack there, then back to layer space.
|
||||
let mut fx = crate::effects::GpuFx::new(e, src, input.offset, input.scale, pipe.levels);
|
||||
let origin = r.run_layer_effects_on(ctx, layer, &mut fx);
|
||||
let Some((mut img, mut offset, scale)) = fx.finish() else { return Some(None) };
|
||||
offset = [offset[0] - origin[0] * scale, offset[1] - origin[1] * scale];
|
||||
if img.width == 0 || img.height == 0 {
|
||||
return Some(Some(()));
|
||||
}
|
||||
if let Some(c) = pipe.to_blend() {
|
||||
img = ops::convert(e, &img, &c);
|
||||
}
|
||||
// The placement only reads the buffer's size, offset and scale.
|
||||
let shape = Buf { img: Image { width: img.width, height: img.height, data: Vec::new() }, offset, scale };
|
||||
Some(place_src(e, r, ctx, layer, &shape, img, canvas, layer.blend_mode, opacity, pipe.levels).map(|c| *canvas = c))
|
||||
}
|
||||
|
||||
/// A 2D precomp layer, its nested comp rendered resident on the GPU (`walk::render` one level
|
||||
/// down) instead of by the CPU compositor and uploaded every frame (`Renderer::layer_source`
|
||||
/// renders nested comps on the CPU: depth > 0). Then the layer's effects (resident), its
|
||||
|
||||
@@ -761,6 +761,19 @@ impl<'a> Renderer<'a> {
|
||||
/// always continuously rasterised, and Continuously Rasterize shape layers and vector footage —
|
||||
/// the output scale times the layer's on-screen scale (quarter-octave steps, rounded up, so
|
||||
/// vector content is drawn at its final size and stays sharp when scaled up).
|
||||
/// Whether `layer`'s processed buffer (source → masks → effects) is cached and the same as
|
||||
/// one frame earlier: drawing it from the layer cache (an accelerator uploads it once) beats
|
||||
/// re-running its effects every frame.
|
||||
pub fn layer_is_static(&self, ctx: &EvalCtx, layer: &Layer) -> bool {
|
||||
if self.cache.is_none() {
|
||||
return false;
|
||||
}
|
||||
let (scale, draft, blur) = (self.raster_scale(ctx, layer), self.opts.draft, self.mb_on(ctx, layer));
|
||||
let prev = ctx.time - ctx.comp.frame_rate.frame_duration();
|
||||
let now = cache::layer_key(ctx, layer, scale, draft, blur);
|
||||
now.is_some() && now == cache::layer_key(&ctx.at(prev), layer, scale, draft, blur)
|
||||
}
|
||||
|
||||
pub fn raster_scale(&self, ctx: &EvalCtx, layer: &Layer) -> f64 {
|
||||
let s = self.opts.scale;
|
||||
let continuous = match layer.source {
|
||||
|
||||
Reference in new issue
Block a user