Merge pull request #641 from MatejKozlovsky/main

This commit is contained in:
Claude committed 2026-10-10 20:29:22 +00:00
commit c773c26281
4 files changed
+155 -1

No files matched your search

+18 -1
View File
@@ -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<Image> {
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 {
+40
View File
@@ -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);
}
}
+64
View File
@@ -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<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<f32>,
) -> Option<GpuImage> {
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));
+33
View File
@@ -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<Arc<Buf>> {