diff --git a/crates/gpu/src/lib.rs b/crates/gpu/src/lib.rs index 8879ab86..4d3f6c77 100644 --- a/crates/gpu/src/lib.rs +++ b/crates/gpu/src/lib.rs @@ -287,6 +287,17 @@ impl Gpu { } } + /// Why a frame was not rendered (best effort: device health, else size / support). + fn decline_reason(&self) -> String { + match self.ctx.check_health() { + Err(e) => format!("device failure / out of memory: {e}"), + Ok(()) => format!( + "the GPU compositor did not handle the frame (unsupported content, a frame over the {} px texture limit, or an allocation failed)", + self.ctx.max_dim + ), + } + } + /// Wait for submitted GPU work with the readback deadline (benchmarks). pub fn wait(&self) { let _ = self.ctx.poll_readbacks(wgpu::PollType::Wait { submission_index: None, timeout: Some(crate::readback::TIMEOUT) }); @@ -299,7 +310,13 @@ impl Accelerator for Gpu { } fn comp_frame(&self, r: &Renderer, comp: ItemId, t: Tick) -> Option { - self.render(r, comp, t) + let img = self.render(r, comp, t); + if img.is_none() { + // The caller renders the frame on the CPU; say so and why, or an export that falls + // back frame after frame (a nested comp the GPU declines) is only mysteriously slow. + log::warn!(target: "effectcraft_gpu", "GPU declined the frame at {:.3} s, the CPU renders it: {}", t.seconds(), self.decline_reason()); + } + img } fn supports_effect(&self, id: &str) -> bool { diff --git a/crates/gpu/src/tests.rs b/crates/gpu/src/tests.rs index 39be29a9..0234f793 100644 --- a/crates/gpu/src/tests.rs +++ b/crates/gpu/src/tests.rs @@ -745,3 +745,43 @@ fn within_memory_returns_the_work_when_there_is_room() { let img = g.within_memory(|| g.ctx.image(64, 32)).expect("room for a 64×32 texture"); assert_eq!((img.width, img.height), (64, 32)); } + +/// Precomp layers (not collapsed) render their nested comp resident on the GPU +/// (`walk::draw_precomp`): match the CPU (nested blend modes, effects inside and outside, +/// transforms, opacity, a blend mode on the precomp layer, half resolution, +/// two instances at different times of the same nested comp). +#[test] +fn nested_precomps_render_resident() { + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + let mut s = Scene::new(depth); + let inner = Comp::new(60, 40, FrameRate::FPS_30, Tick::from_seconds_f64(2.0)); + let inner_id = s.p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.clone().into())); + { + let mut ins = Scene { p: std::mem::take(&mut s.p), cid: inner_id, comp: inner }; + let mut a = ins.footage(50, 30); + ins.effect(&mut a, "ec.blur.gaussian", &[("blurriness", n(3.0))]); + ins.push(a); + let mut b = ins.solid([0.2, 0.7, 0.4], 30, 20); + b.blend_mode = BlendMode::Difference; + set(&mut b, "transform/rotation", Value::Scalar(15.0)); + ins.push(b); + s.p = ins.p; + } + let bg = s.footage(97, 61); + s.push(bg); + let mut pre = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (60, 40), None); + set(&mut pre, "transform/rotation", Value::Scalar(25.0)); + set(&mut pre, "transform/opacity", Value::Scalar(80.0)); + s.effect(&mut pre, "ec.color.levels", &[("gamma", n(1.3))]); + pre.blend_mode = BlendMode::Screen; + s.push(pre); + // A second instance, later in the nested comp's time. + let mut pre2 = build::layer(&mut s.p, &s.comp, "Inner 2", LayerSource::Comp { item: inner_id }, (60, 40), None); + set(&mut pre2, "transform/position", v3(70.0, 40.0)); + pre2.start_time = Tick::from_seconds_f64(-0.5); + s.push(pre2); + check(&format!("nested precomps {depth:?}"), compare_at(&s, opts(), Tick::from_seconds_f64(0.2)), 0.0); + let half = RenderOpts { scale: 0.5, ..opts() }; + check(&format!("nested precomps half {depth:?}"), compare_at(&s, half, Tick::from_seconds_f64(0.2)), 0.0); + } +} diff --git a/crates/gpu/src/walk.rs b/crates/gpu/src/walk.rs index b5bd166c..e2b2910f 100644 --- a/crates/gpu/src/walk.rs +++ b/crates/gpu/src/walk.rs @@ -214,6 +214,9 @@ fn draw_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: & if opacity <= 0.0 { return Some(()); } + if let Some(done) = draw_precomp(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); @@ -243,6 +246,50 @@ fn draw_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: & Some(()) } +/// 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 +/// placement, track matte / Preserve Transparency and blend, as for any layer. `None`: not this +/// kind of layer, or the nested comp needs the CPU path (masks, layer styles, proxies, +/// overrides, frame blending, nested motion-blur sampling; see `Renderer::precomp_source`); +/// `Some(None)`: the GPU failed. +fn draw_precomp(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &mut GpuImage, opacity: f32) -> Option> { + if layer.masks().is_some_and(|m| !m.children.is_empty()) || styles::active(ctx, layer) { + return None; + } + let (sub, item, lt, bs) = r.precomp_source(ctx, layer)?; + let Some(mut src) = render(e, &sub, item, lt) else { return Some(None) }; + let pipe = r.pipe(); + // `masked_source` quantises a non-solid source to the project's depth. + if let Some(l) = pipe.levels { + src = ops::quantize(e, &src, l); + } + let (mut img, offset, scale) = if layer.switches.effects && layer.effects().is_some_and(|f| !f.children.is_empty()) { + // Effects run in effect space (0.7.0): from the layer bounds' corner. + let o = ctx.effect_bounds(layer).1; + let mut fx = crate::effects::GpuFx::new(e, src, [o[0] * bs, o[1] * bs], bs, pipe.levels); + r.run_layer_effects_on(ctx, layer, &mut fx); + let Some((img, off, sc)) = fx.finish() else { return Some(None) }; + (img, [off[0] - o[0] * sc, off[1] - o[1] * sc], sc) + } else { + (src, [0.0; 2], bs) + }; + if img.width == 0 || img.height == 0 { + return Some(Some(())); + } + if let Some(c) = pipe.to_blend() { + img = ops::convert(e, &img, &c); + } + let shape = Buf { img: Image { width: img.width, height: img.height, data: Vec::new() }, offset, scale }; + if r.track_matte(ctx, layer).is_some() || layer.preserve_transparency { + let Some(iso) = place_src(e, r, ctx, layer, &shape, img, &e.zeros(canvas.width, canvas.height), BlendMode::Normal, 1.0, None) else { + return Some(None); + }; + return Some(composite_iso(e, r, ctx, layer, iso, canvas, opacity)); + } + Some(place_src(e, r, ctx, layer, &shape, img, canvas, layer.blend_mode, opacity, pipe.levels).map(|c| *canvas = c)) +} + /// Adjustment layer (`Renderer::draw_adjustment`): the effect stack runs on the comp below, /// resident on the GPU (non-GPU effects read back, run on the CPU and upload), and the result /// replaces the canvas inside the layer's footprint × opacity. `None` = draw it on the CPU. @@ -342,6 +389,23 @@ fn place( return Some(target.clone()); } let src = e.g.upload_buf(buf)?; + place_src(e, r, ctx, layer, buf, src, target, mode, opacity, levels) +} + +/// [`place`] of a GPU image `src` shaped like `buf`. +#[allow(clippy::too_many_arguments)] +fn place_src( + e: &mut Enc, + r: &Renderer, + ctx: &EvalCtx, + layer: &Layer, + buf: &Buf, + src: GpuImage, + target: &GpuImage, + mode: BlendMode, + opacity: f32, + levels: Option, +) -> Option { let pl = r.placement(ctx, layer, buf); if let [m] = pl.matrices.as_slice() { return Some(ops::warp_q(e, target, &src, m, pl.sampling, mode, opacity, pl.seed, None, levels)); diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 79dcc21b..f4a5a98a 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -623,6 +623,30 @@ impl<'a> Renderer<'a> { } } + /// A precomp layer's nested comp for an accelerator to render resident (no CPU render and + /// upload of the nested frame): the renderer one level down, the comp, its time and the + /// buffer scale, exactly as [`Self::layer_source`]'s comp branch would render it. `None` + /// when that branch does more (composition proxy, Essential Properties overrides, frame + /// blending, recursion) or draws nothing (outside the nested comp's span): use the CPU path. + pub fn precomp_source(&self, ctx: &EvalCtx, layer: &Layer) -> Option<(Renderer<'a>, ItemId, Tick, f64)> { + let LayerSource::Comp { item } = layer.source else { return None }; + if layer.switches.adjustment || self.project.comp_contains(item, ctx.comp_id) { + return None; + } + let nc = self.project.comp(item)?; + let lt = ctx.nested_time(layer); + if self.content_times(ctx, layer).is_some() { + return None; + } + if !nc.covers(lt) || self.proxy_for(item).is_some() || !essential_overrides(ctx, layer).is_empty() || frame_blend_mode(ctx, layer) != FrameBlend::Off { + return None; + } + let s = self.opts.scale; + let bs = if s < 1.0 && nc.preserve_resolution { 1.0 } else { s }; + let base = self.nested_through(layer); + Some((Renderer { opts: RenderOpts { scale: bs, ..base.opts }, ..base }, item, lt, bs)) + } + /// The nested comp of a precomp layer whose transformations collapse into this comp: the /// Collapse Transformations switch is on and the layer has no masks, effects or layer /// styles (those force a flattened render of the precomp, as in After Effects). @@ -1988,6 +2012,15 @@ impl<'a> Renderer<'a> { self.run_effect_stack(ctx, layer, adjustment, usize::MAX, None, [0.0; 2], target); } + /// [`Renderer::run_effects_on`] for a (non-adjustment) layer's own pixels: `target` holds + /// them in effect space (as [`Renderer::layer_input`] hands them out) and the result stays + /// there. Returns the effect-space origin in layer coordinates (rebase by its negation). + pub fn run_layer_effects_on(&self, ctx: &EvalCtx, layer: &Layer, target: &mut dyn FxTarget) -> [f64; 2] { + let origin = ctx.effect_bounds(layer).1; + self.run_effect_stack(ctx, layer, false, usize::MAX, None, origin, target); + origin + } + /// An adjustment layer's footprint (its source with masks applied, layer space): where /// its effects show. `None` = the layer changes nothing. pub fn adjustment_footprint(&self, ctx: &EvalCtx, layer: &Layer) -> Option> {