//! The per-frame compositing walk on the GPU: the same steps as `Renderer::draw_comp` (layer //! order, 3D runs, collapsed precomps, track mattes, Preserve Transparency, layer styles, motion //! blur, bit-depth quantisation, colour conversions), with layer pixels taken from the CPU //! renderer and its layer cache. Steps without a GPU implementation (3D runs, adjustment and //! wireframe layers) read the canvas back, run on the CPU and upload the result. use std::sync::Arc; use effectcraft_color::BlendMode; use effectcraft_effects::Buf; use effectcraft_project::{ItemId, Layer, Quality}; use effectcraft_raster::Image; use effectcraft_render::{EvalCtx, Renderer, styles}; use effectcraft_time::Tick; use crate::context::{Enc, GpuImage}; use crate::ops; /// Render a comp frame into a GPU image (the canvas in the output space, as /// `Renderer::comp_frame_cpu` returns it). `None` when something cannot run here (the caller /// renders on the CPU instead). pub(crate) fn render<'g>(e: &mut Enc<'g>, r: &Renderer, comp_id: ItemId, t: Tick) -> Option { // Region-of-interest frames are cropped on the CPU path. if r.opts.roi.is_some() { return None; } let ctx = r.eval_ctx(comp_id, t)?; let s = r.opts.scale; let w = ((ctx.comp.width as f64 * s).round() as u32).max(1); let h = ((ctx.comp.height as f64 * s).round() as u32).max(1); if r.depth() > 16 || !e.g.fits(w, h) { return None; } let mut canvas = e.image(w, h); draw_comp(e, r, &ctx, &mut canvas)?; let pipe = r.pipe(); if let Some(c) = pipe.from_blend() { canvas = ops::convert(e, &canvas, &c); } if let Some(l) = pipe.levels { canvas = ops::quantize(e, &canvas, l); } if r.depth() == 0 && let Some(c) = pipe.output() { canvas = ops::convert(e, &canvas, &c); if let Some(l) = pipe.levels { canvas = ops::quantize(e, &canvas, l); } } Some(canvas) } /// Run a CPU drawing step on the canvas (read back, draw, upload). fn on_cpu(e: &mut Enc, canvas: &mut GpuImage, f: impl FnOnce(&mut Image)) -> Option<()> { let mut img = e.download(canvas)?; f(&mut img); *canvas = e.g.upload_image(&img)?; Some(()) } fn draw_comp<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, canvas: &mut GpuImage) -> Option<()> { let visible = r.visible_layers(ctx); let levels = r.pipe().levels; let mut i = 0; while i < visible.len() { if visible[i].is_3d() { let mut j = i; while j < visible.len() && visible[j].is_3d() { j += 1; } on_cpu(e, canvas, |img| r.draw_3d_run(ctx, &visible[i..j], img))?; i = j; } else { match r.collapsed(ctx, visible[i]) { Some(item) => draw_collapsed(e, r, ctx, visible[i], item, canvas)?, None => draw_layer(e, r, ctx, visible[i], canvas)?, } i += 1; } // 8/16 bpc: the comp is an integer buffer after every layer. if let Some(l) = levels { *canvas = ops::quantize(e, canvas, l); } } Some(()) } fn draw_collapsed<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, layer: &Layer, item: ItemId, canvas: &mut GpuImage) -> Option<()> { let opacity = r.layer_opacity(ctx, layer); if opacity <= 0.0 { return Some(()); } let matte = r.track_matte(ctx, layer).is_some(); if layer.blend_mode == BlendMode::Normal && !matte && !layer.preserve_transparency { if let Some((sub, nctx)) = r.collapse_into(ctx, layer, item, opacity) { draw_comp(e, &sub, &nctx, canvas)?; } return Some(()); } let Some((sub, nctx)) = r.collapse_into(ctx, layer, item, 1.0) else { return Some(()) }; let mut iso = e.image(canvas.width, canvas.height); draw_comp(e, &sub, &nctx, &mut iso)?; composite_iso(e, r, ctx, layer, iso, canvas, opacity) } fn draw_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &mut GpuImage) -> Option<()> { if layer.switches.adjustment || layer.switches.quality == Quality::Wireframe { // Adjustment layers run their effects on the comp below (CPU, with GPU effects where // available); wireframes draw outlines. return on_cpu(e, canvas, |img| r.draw_layer_cpu(ctx, layer, img)); } let opacity = r.layer_opacity(ctx, layer); if opacity <= 0.0 { return Some(()); } 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); } // Layer styles: exterior passes with their own modes, then the body; the layer's opacity // fades the whole stack; knockout and channel switches as on the CPU. let bl = styles::blending(ctx, layer); let (w, h) = (canvas.width, canvas.height); if r.track_matte(ctx, layer).is_some() || layer.preserve_transparency { let mut iso = e.image(w, h); for (b, m) in &st.passes { iso = place(e, r, ctx, layer, b, &iso, *m, 1.0)?; } iso = place(e, r, ctx, layer, &st.body, &iso, BlendMode::Normal, 1.0)?; return composite_iso(e, r, ctx, layer, iso, canvas, opacity); } let mut tmp = canvas.clone(); if bl.knockout > 0 { let k = place(e, r, ctx, layer, &st.content, &e.image(w, h), BlendMode::Normal, 1.0)?; tmp = ops::knockout(e, &tmp, &k); } for (b, m) in &st.passes { tmp = place(e, r, ctx, layer, b, &tmp, *m, 1.0)?; } tmp = place(e, r, ctx, layer, &st.body, &tmp, layer.blend_mode, 1.0)?; *canvas = ops::channel_mix(e, canvas, &tmp, bl.channels, opacity); Some(()) } fn composite_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, buf: &Arc, canvas: &mut GpuImage, opacity: f32) -> Option<()> { let matte = r.track_matte(ctx, layer).is_some(); if !matte && !layer.preserve_transparency { *canvas = place(e, r, ctx, layer, buf, canvas, layer.blend_mode, opacity)?; return Some(()); } // Render in isolation, then matte / preserve transparency, then blend. let iso = place(e, r, ctx, layer, buf, &e.image(canvas.width, canvas.height), BlendMode::Normal, 1.0)?; composite_iso(e, r, ctx, layer, iso, canvas, opacity) } /// Track matte / Preserve Transparency on an isolated layer render, then blend. fn composite_iso(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, mut iso: GpuImage, canvas: &mut GpuImage, opacity: f32) -> Option<()> { if let Some((m, kind)) = r.track_matte(ctx, layer) { let mut mimg = e.image(canvas.width, canvas.height); if m.is_active_at(ctx.time) && let Some(mb) = r.layer_buf(ctx, m) { let mo = ctx.opacity(m) as f32; mimg = place(e, r, ctx, m, &mb, &mimg, BlendMode::Normal, mo)?; } iso = ops::matte(e, &iso, &mimg, kind); } if layer.preserve_transparency { iso = ops::preserve(e, &iso, canvas); } *canvas = ops::blend_full(e, canvas, &iso, layer.blend_mode, opacity, layer.id.0 as u32); Some(()) } /// Draw a processed layer buffer onto `target` (transform, motion blur sub-samples). fn place(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, buf: &Arc, target: &GpuImage, mode: BlendMode, opacity: f32) -> Option { if buf.img.is_empty() || opacity <= 0.0 { return Some(target.clone()); } let src = e.g.upload_buf(buf)?; let pl = r.placement(ctx, layer, buf); if let [m] = pl.matrices.as_slice() { return Some(ops::warp(e, target, &src, m, pl.sampling, mode, opacity, pl.seed, None)); } let mut acc = e.image(target.width, target.height); let k = 1.0 / pl.matrices.len() as f32; for m in &pl.matrices { acc = ops::warp(e, &acc, &src, m, pl.sampling, BlendMode::Normal, 1.0, 0, Some(k)); } Some(ops::blend_full(e, target, &acc, mode, opacity, pl.seed)) }