Files
effectcraft/crates/gpu/src/lib.rs
T
Brandon ThomasandClaude Opus 5.5 485f562e19 M12.2: gpu compositor and gpu effects
New L3 crate effectcraft-gpu: a wgpu compute backend for the per-frame
compositing walk (layer transforms with the CPU's nearest/bilinear/bicubic
sampling and minification pre-filter, motion-blur accumulation, track
mattes, Preserve Transparency, layer style passes and knockout, all 38
blend modes as a WGSL port of color::blend, 8/16 bpc clamp+quantise,
colour-space conversions). 3D runs, adjustment and wireframe layers run on
the CPU between GPU steps. GPU effects with the CPU semantics: Gaussian,
Fast Box and Directional Blur, Glow, Levels, Curves, Hue/Saturation, Tint,
Fill, Gradient Ramp, Fractal Noise, Drop Shadow, Brightness & Contrast,
Exposure, Invert, Transform (registry gpu badge from effects::GPU_EFFECTS).

render: Backend (Cpu/Gpu/Auto) in RenderOpts, the Accelerator trait and
Renderer::accel, GPU effect chains in the effect stack, hooks for the GPU
walk. Project setting gpu_acceleration (Mercury GPU Acceleration / Software
Only), render.backend command, file.projectSettings renderer. Session and
Render Queue exports carry the accelerator. CLI --gpu and bench --gpu.

Tests compare GPU vs CPU renders (blend modes x bit depths, transforms,
masks, mattes, motion blur, colour management, styles, collapse,
adjustment fallback, every GPU effect) and skip without an adapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 10:59:08 -04:00

128 lines
4.5 KiB
Rust

//! The EffectCraft GPU compositor (Mercury GPU Acceleration's counterpart), on wgpu compute
//! shaders: Metal, Vulkan, Direct3D 12 and WebGPU.
//!
//! The CPU [`Renderer`] stays the reference and keeps rendering layer *content* (sources, masks,
//! CPU effects, layer styles) into its layer cache. [`Gpu`] composites: it uploads the cached
//! layer buffers (once per buffer), applies the layer transforms with the CPU's sampling
//! (nearest / bilinear / bicubic, minification pre-filter), motion-blur sub-samples, track
//! mattes, Preserve Transparency, layer styles' passes, all 38 blend modes and the 8/16 bpc
//! clamping and quantisation, and converts colour spaces. 3D runs, adjustment layers and
//! wireframes run on the CPU between GPU steps (read back, draw, upload).
//!
//! GPU effects ([`effectcraft_effects::GPU_EFFECTS`]) run as compute kernels with the CPU
//! effect's exact steps (padding, box-blur radii, parameter conversions); chains of them are
//! uploaded and read back once.
//!
//! Plug a [`Gpu`] into [`Renderer::accel`] (it implements [`Accelerator`]); renders then use it
//! when [`RenderOpts::backend`](effectcraft_render::RenderOpts) asks for it. The viewer can
//! skip readback entirely with [`Gpu::render_display`], which leaves an RGBA8 texture for
//! egui-wgpu to draw.
mod context;
mod effects;
mod ops;
mod walk;
use std::sync::Arc;
pub use context::{GpuContext, GpuImage};
use effectcraft_effects::Buf;
use effectcraft_project::ItemId;
use effectcraft_raster::Image;
use effectcraft_render::{Accelerator, FxStep, Renderer};
use effectcraft_time::Tick;
pub use wgpu;
use crate::context::Enc;
/// The GPU compositor (cheap to clone; one device shared by all clones).
#[derive(Clone)]
pub struct Gpu {
ctx: Arc<GpuContext>,
}
/// A viewer frame left on the GPU: premultiplied RGBA8 (`wgpu::TextureFormat::Rgba8Unorm`),
/// ready for `egui_wgpu::Renderer::register_native_texture`.
#[derive(Clone, Debug)]
pub struct DisplayFrame {
pub texture: wgpu::Texture,
pub width: u32,
pub height: u32,
}
impl Gpu {
/// On a device of its own (CLI, tests, benchmarks); `None` without a usable adapter.
#[cfg(not(target_arch = "wasm32"))]
pub fn headless() -> Option<Gpu> {
GpuContext::headless().map(Gpu::from_context)
}
/// On an existing device (the desktop app shares egui-wgpu's).
pub fn new(adapter: &wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue) -> Result<Gpu, String> {
GpuContext::new(adapter, device, queue).map(Gpu::from_context)
}
pub fn from_context(ctx: GpuContext) -> Gpu {
Gpu { ctx: Arc::new(ctx) }
}
pub fn context(&self) -> &GpuContext {
&self.ctx
}
pub fn device(&self) -> &wgpu::Device {
&self.ctx.device
}
/// Render a top-level comp frame and leave it on the GPU as a display texture (no
/// readback unless a CPU fallback step needs one). `None` = render on the CPU.
pub fn render_display(&self, r: &Renderer, comp: ItemId, t: Tick) -> Option<DisplayFrame> {
let mut e = Enc::new(&self.ctx);
let img = walk::render(&mut e, r, comp, t)?;
let texture = e.display(&img);
e.submit();
Some(DisplayFrame { texture, width: img.width, height: img.height })
}
/// Read a display frame back as premultiplied RGBA8 (Info panel sampling, eyedroppers).
pub fn read_display(&self, f: &DisplayFrame) -> Option<Vec<u8>> {
Enc::new(&self.ctx).read_texture(&f.texture, f.width, f.height, 4)
}
/// Render a frame on the GPU and read it back (`None` = not handled).
pub fn render(&self, r: &Renderer, comp: ItemId, t: Tick) -> Option<Image> {
if !self.ctx.can_readback() {
return None;
}
let mut e = Enc::new(&self.ctx);
let img = walk::render(&mut e, r, comp, t)?;
e.download(&img)
}
/// Wait for all submitted GPU work (benchmarks).
pub fn wait(&self) {
let _ = self.ctx.device.poll(wgpu::PollType::wait_indefinitely());
}
}
impl Accelerator for Gpu {
fn name(&self) -> String {
self.ctx.name.clone()
}
fn comp_frame(&self, r: &Renderer, comp: ItemId, t: Tick) -> Option<Image> {
self.render(r, comp, t)
}
fn supports_effect(&self, id: &str) -> bool {
self.ctx.can_readback() && effects::supports(id)
}
fn effects(&self, chain: &[FxStep], buf: &Buf, levels: Option<f32>) -> Option<Buf> {
effects::run_chain(&mut Enc::new(&self.ctx), chain, buf, levels)
}
}
#[cfg(test)]
mod tests;