mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-13 23:17:30 +00:00
Merge pull request #648 from storytold/claude/community-pr-review-lwtrlr
Land #641: GPU precomp layers, with follow-ups
This commit is contained in:
4 files changed
+172
-2
No files matched your search
+30
-2
@@ -102,6 +102,8 @@ pub struct Gpu {
|
||||
ctx: Arc<GpuContext>,
|
||||
/// Backend::Auto's per-comp CPU / GPU timings.
|
||||
auto: Arc<effectcraft_render::AutoPick>,
|
||||
/// (comp, reason) pairs already warned about when the GPU declined a frame.
|
||||
declined: Arc<std::sync::Mutex<std::collections::HashSet<(u64, String)>>>,
|
||||
}
|
||||
|
||||
/// A viewer frame left on the GPU: premultiplied RGBA8 (`wgpu::TextureFormat::Rgba8Unorm`) with
|
||||
@@ -133,7 +135,7 @@ impl Gpu {
|
||||
}
|
||||
|
||||
pub fn from_context(ctx: GpuContext) -> Gpu {
|
||||
Gpu { ctx: Arc::new(ctx), auto: Default::default() }
|
||||
Gpu { ctx: Arc::new(ctx), auto: Default::default(), declined: Default::default() }
|
||||
}
|
||||
|
||||
/// A device of its own without blocking, with deferred readbacks (a browser worker, see
|
||||
@@ -287,6 +289,18 @@ 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(()) if !self.ctx.can_readback() => "no GPU readback is available here".into(),
|
||||
Ok(()) => format!(
|
||||
"the GPU compositor did not handle the frame (a region of interest, 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 +313,21 @@ 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 is only mysteriously slow. Once per comp and reason as a
|
||||
// warning (the log keeps only the last few hundred, for the System Report), then at
|
||||
// debug level.
|
||||
let reason = self.decline_reason();
|
||||
let first = self.declined.lock().map(|mut seen| seen.len() < 256 && seen.insert((comp.0, reason.clone()))).unwrap_or(false);
|
||||
if first {
|
||||
log::warn!(target: "effectcraft_gpu", "GPU declined the frame at {:.3} s, the CPU renders it: {reason}", t.seconds());
|
||||
} else {
|
||||
log::debug!(target: "effectcraft_gpu", "GPU declined the frame at {:.3} s: {reason}", t.seconds());
|
||||
}
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
fn supports_effect(&self, id: &str) -> bool {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,55 @@ 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)?;
|
||||
// A nested comp the GPU can't take (nested too deep, over the texture limit, content it
|
||||
// declines) goes back to the CPU path for this layer only; only a device failure fails the
|
||||
// frame.
|
||||
let Some(mut src) = render(e, &sub, item, lt) else {
|
||||
return if e.g.check_health().is_ok() { None } else { 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 +394,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));
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
Reference in new issue
Block a user