Files
effectcraft/crates/gpu/src/walk.rs
T
Brandon ThomasandClaude Opus 5.5 0c4e533460 M13.22: GPU compositing of Advanced 3D runs with blend modes, track mattes (2D and 3D), Preserve Transparency, and environment backgrounds (Classic and Advanced 3D skies)
render: three_d::adv::SplitRun (Renderer::split_adv_run) hands draw_run's split path to an
accelerator as data (main scene, special layers far to near, 3D mattes drawn solo);
Renderer::sky_draw / three_d::SkyDraw for Environment Light Background layers. gpu: the walk
draws skies (adv_sky kernel), resolves each special layer, hides it behind the nearer main
scene (occlude kernel) and composites it through the 2D path without readbacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 18:26:06 -04:00

356 lines
15 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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. Classic and Advanced 3D runs (environment skies included) and
//! adjustment layers run on the GPU; steps without a GPU implementation (anything a kernel
//! declines) 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<GpuImage> {
// 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.zeros(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);
}
}
if r.depth() == 0
&& let Some((lin, mode, enc)) = pipe.output_hdr()
{
on_cpu(e, &mut canvas, |img| {
effectcraft_render::color::output_hdr(img, lin, mode, enc);
pipe.quantize(img);
})?;
}
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;
}
draw_3d(e, r, ctx, &visible[i..j], canvas)?;
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(())
}
/// A run of consecutive 3D layers: Classic 3D on the GPU (`classic3d`), with 3D adjustment
/// layers splitting the run as on the CPU; Advanced 3D, wireframes and runs the kernel cannot
/// hold draw on the CPU.
fn draw_3d<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, run: &[&'a Layer], canvas: &mut GpuImage) -> Option<()> {
// Environment Light Background layers: the sky behind the rest of the run (drawn first, as
// `three_d::compose::draw_run` does).
if run.iter().any(|l| l.environment_background) {
for l in run.iter().filter(|l| l.environment_background) {
if let Some(sky) = r.sky_draw(ctx, l) {
*canvas = crate::adv3d::sky(e, &sky, canvas)?;
}
}
let rest: Vec<&'a Layer> = run.iter().copied().filter(|l| !l.environment_background).collect();
return draw_3d(e, r, ctx, &rest, canvas);
}
if let Some(k) = run.iter().position(|l| l.switches.adjustment) {
draw_3d(e, r, ctx, &run[..k], canvas)?;
draw_layer(e, r, ctx, run[k], canvas)?;
return draw_3d(e, r, ctx, &run[k + 1..], canvas);
}
if run.is_empty() {
return Some(());
}
// Advanced 3D: rendered and composited on the GPU (`adv3d`).
if let Some(prep) = r.prepare_adv_run(ctx, run, (canvas.width, canvas.height)) {
match crate::adv3d::draw_run(e, &prep, canvas) {
Some(img) => *canvas = img,
None => on_cpu(e, canvas, |img| r.draw_3d_run(ctx, run, img))?,
}
return Some(());
}
// Advanced 3D with layers on the 2D compositing path (blend modes, track mattes, Preserve
// Transparency).
if let Some(split) = r.split_adv_run(ctx, run, (canvas.width, canvas.height)) {
match draw_split(e, r, ctx, &split, canvas) {
Some(img) => *canvas = img,
None => on_cpu(e, canvas, |img| r.draw_3d_run(ctx, run, img))?,
}
return Some(());
}
match r.prepare_3d_run(ctx, run, (canvas.width, canvas.height)).and_then(|prep| crate::classic3d::draw_run(e, &prep, canvas)) {
Some(img) => *canvas = img,
None => on_cpu(e, canvas, |img| r.draw_3d_run(ctx, run, img))?,
}
Some(())
}
/// `three_d::adv::draw_run`'s split path on the GPU canvas: the plain layers as one scene over
/// the canvas, then each special layer (far to near) rendered alone, hidden where the main
/// scene is nearer, matted (a 3D matte through the camera, else the 2D placement), Preserve
/// Transparency and blended. `None` = render the run on the CPU.
fn draw_split(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, split: &effectcraft_render::three_d::adv::SplitRun, canvas: &GpuImage) -> Option<GpuImage> {
let (w, h) = (canvas.width, canvas.height);
let main_prep = split.main();
if main_prep.out != (w, h) {
return None;
}
let encode = !main_prep.linear;
let main = crate::adv3d::resolve_run(e, &main_prep)?;
let mut cv = match &main {
Some(m) => crate::adv3d::finish(e, m, encode, Some(canvas)),
None => canvas.clone(),
};
for i in 0..split.len() {
let Some(iso) = crate::adv3d::resolve_run(e, &split.special(i))? else { continue };
let iso = match &main {
Some(m) => crate::adv3d::occluded(e, &iso, m, encode),
None => crate::adv3d::finish(e, &iso, encode, None),
};
let matte3d = split.with_matte3d(i, |p| match p {
None => Some(e.zeros(w, h)),
Some(p) => Some(match crate::adv3d::resolve_run(e, p)? {
Some(m) => crate::adv3d::finish(e, &m, encode, None),
None => e.zeros(w, h),
}),
});
let matte3d = match matte3d {
Some(m) => Some(m?),
None => None,
};
composite_iso_with(e, r, ctx, split.layer(i), iso, &mut cv, 1.0, matte3d, None)?;
}
Some(cv)
}
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.zeros(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 {
if draw_adjustment(e, r, ctx, layer, canvas).is_some() {
return Some(());
}
return on_cpu(e, canvas, |img| r.draw_layer_cpu(ctx, layer, img));
}
if r.quality(layer) == Quality::Wireframe {
// The layer's bounds as a one-pixel outline (the CPU's pixels).
*canvas = crate::adv3d::wireframe(e, canvas, &r.wireframe_pixels(ctx, layer, (canvas.width, canvas.height)));
return Some(());
}
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.zeros(w, h);
for (b, m) in &st.passes {
iso = place(e, r, ctx, layer, b, &iso, *m, 1.0, None)?;
}
iso = place(e, r, ctx, layer, &st.body, &iso, BlendMode::Normal, 1.0, None)?;
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.zeros(w, h), BlendMode::Normal, 1.0, None)?;
tmp = ops::knockout(e, &tmp, &k);
}
for (b, m) in &st.passes {
tmp = place(e, r, ctx, layer, b, &tmp, *m, 1.0, None)?;
}
tmp = place(e, r, ctx, layer, &st.body, &tmp, layer.blend_mode, 1.0, None)?;
*canvas = ops::channel_mix(e, canvas, &tmp, bl.channels, opacity);
Some(())
}
/// 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.
fn draw_adjustment(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &mut GpuImage) -> Option<()> {
let opacity = r.layer_opacity(ctx, layer);
let Some(foot) = r.adjustment_footprint(ctx, layer) else { return Some(()) };
let (w, h) = (canvas.width, canvas.height);
let matte = place(e, r, ctx, layer, &foot, &e.zeros(w, h), BlendMode::Normal, 1.0, None)?;
let pipe = r.pipe();
// Effects run in the working space, not the linear blending space.
let below = match pipe.from_blend() {
Some(c) => ops::convert(e, canvas, &c),
None => canvas.clone(),
};
let mut fx = crate::effects::GpuFx::new(e, below, [0.0; 2], r.opts.scale, pipe.levels);
r.run_effects_on(ctx, layer, true, &mut fx);
let (mut adjusted, _, _) = fx.finish()?;
if adjusted.width != w || adjusted.height != h {
return Some(());
}
if let Some(c) = pipe.to_blend() {
adjusted = ops::convert(e, &adjusted, &c);
}
*canvas = ops::adjust_mix(e, canvas, &adjusted, &matte, opacity);
Some(())
}
fn composite_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, buf: &Arc<Buf>, canvas: &mut GpuImage, opacity: f32) -> Option<()> {
let matte = r.track_matte(ctx, layer).is_some();
if !matte && !layer.preserve_transparency {
// The comp is quantised after the layer (8/16 bpc) in the same pass.
*canvas = place(e, r, ctx, layer, buf, canvas, layer.blend_mode, opacity, r.pipe().levels)?;
return Some(());
}
// Render in isolation, then matte / preserve transparency, then blend.
let iso = place(e, r, ctx, layer, buf, &e.zeros(canvas.width, canvas.height), BlendMode::Normal, 1.0, None)?;
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, iso: GpuImage, canvas: &mut GpuImage, opacity: f32) -> Option<()> {
composite_iso_with(e, r, ctx, layer, iso, canvas, opacity, None, r.pipe().levels)
}
/// `Renderer::composite_iso_with`: the matte from `matte_img` when given (a 3D matte drawn
/// through the camera), else the matte layer placed in 2D; the blend quantised to `levels`.
#[allow(clippy::too_many_arguments)]
fn composite_iso_with(
e: &mut Enc,
r: &Renderer,
ctx: &EvalCtx,
layer: &Layer,
mut iso: GpuImage,
canvas: &mut GpuImage,
opacity: f32,
matte_img: Option<GpuImage>,
levels: Option<f32>,
) -> Option<()> {
if let Some((m, kind)) = r.track_matte(ctx, layer) {
let mimg = match matte_img.filter(|i| (i.width, i.height) == (canvas.width, canvas.height)) {
Some(i) => i,
None => {
let mut mimg = e.zeros(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, None)?;
}
mimg
}
};
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, levels);
Some(())
}
/// Draw a processed layer buffer onto `target` (transform, motion blur sub-samples), quantising
/// the result to `levels` when set.
#[allow(clippy::too_many_arguments)]
fn place(
e: &mut Enc,
r: &Renderer,
ctx: &EvalCtx,
layer: &Layer,
buf: &Arc<Buf>,
target: &GpuImage,
mode: BlendMode,
opacity: f32,
levels: Option<f32>,
) -> Option<GpuImage> {
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_q(e, target, &src, m, pl.sampling, mode, opacity, pl.seed, None, levels));
}
let mut acc = e.zeros(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, levels))
}