Merge main into M13.14
This commit is contained in:
commit
8b43adb8a0
|
|
@ -983,7 +983,9 @@ fn adv3d_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option<Item
|
|||
|
||||
/// `bench --gpu`: CPU vs GPU ms/frame for every comp (or `--comp`) at Full and Half. "cold"
|
||||
/// renders everything (no layer cache); "warm" reuses the layer cache (playback / scrubbing of
|
||||
/// unchanged layers: compositing cost); "viewer" is the GPU display path without readback.
|
||||
/// unchanged layers: compositing cost); "viewer" is the GPU display path without readback;
|
||||
/// "auto" is Backend::Auto (Mercury GPU Acceleration) choosing per comp from measured times,
|
||||
/// warm; "up/dn MB" are the GPU's uploads / readbacks per warm GPU frame.
|
||||
fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
|
||||
use effectcraft_render::Backend;
|
||||
let gpu = effectcraft_gpu::Gpu::headless().ok_or_else(|| Failure::Error("--gpu: no usable GPU adapter".into()))?;
|
||||
|
|
@ -992,6 +994,7 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
|
|||
// full-frame adjustment layer with a GPU effect stack) and an Advanced 3D comp (`--adv3d`:
|
||||
// that one only).
|
||||
let mut project = (*s.project).clone();
|
||||
project.settings.gpu_acceleration = true;
|
||||
let comps: Vec<ItemId> = match args.opt("--comp") {
|
||||
_ if args.flag("--adv3d") => adv3d_bench_comp(&mut project).into_iter().collect(),
|
||||
Some(_) => s.active_comp_id().into_iter().collect(),
|
||||
|
|
@ -1005,8 +1008,8 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
|
|||
let project = &project;
|
||||
eprintln!("GPU: {} — median of {n} runs (ms/frame)", effectcraft_render::Accelerator::name(&gpu));
|
||||
eprintln!(
|
||||
"{:<28} {:>5} {:>10} {:>9} {:>9} {:>9} {:>9} {:>9} {:>8} {:>9}",
|
||||
"comp", "res", "size", "cpu cold", "gpu cold", "cpu warm", "gpu warm", "gpu view", "speedup", "gpu≠cpu"
|
||||
"{:<28} {:>5} {:>10} {:>9} {:>9} {:>9} {:>9} {:>9} {:>8} {:>9} {:>9} {:>11}",
|
||||
"comp", "res", "size", "cpu cold", "gpu cold", "cpu warm", "gpu warm", "gpu view", "speedup", "auto", "gpu≠cpu", "up/dn MB"
|
||||
);
|
||||
for cid in comps {
|
||||
let Some(comp) = project.comp(cid) else { continue };
|
||||
|
|
@ -1031,9 +1034,22 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
|
|||
std::hint::black_box(mk(Backend::Cpu, Some(&cache)));
|
||||
});
|
||||
let gcache = LayerCache::default();
|
||||
// Traffic in steady state: after the frame that fills the layer cache.
|
||||
std::hint::black_box(mk(Backend::Gpu, Some(&gcache)));
|
||||
let t0 = gpu.context().transfer_stats();
|
||||
let gpu_warm = time_ms(n, || {
|
||||
std::hint::black_box(mk(Backend::Gpu, Some(&gcache)));
|
||||
});
|
||||
let t1 = gpu.context().transfer_stats();
|
||||
let per = |b: u64| b as f64 / (n + 1) as f64 / (1u64 << 20) as f64;
|
||||
let traffic = format!("{:.1}/{:.1}", per(t1.upload_bytes - t0.upload_bytes), per(t1.readback_bytes - t0.readback_bytes));
|
||||
// Auto: warm-up frames on both sides, then the measured choice.
|
||||
for _ in 0..2 * effectcraft_render::AutoPick::WARMUP {
|
||||
std::hint::black_box(mk(Backend::Auto, Some(&gcache)));
|
||||
}
|
||||
let auto = time_ms(n, || {
|
||||
std::hint::black_box(mk(Backend::Auto, Some(&gcache)));
|
||||
});
|
||||
let view = time_ms(n, || {
|
||||
let mut r = Renderer::new(project, s.footage.as_ref(), RenderOpts { scale, backend: Backend::Gpu, ..Default::default() });
|
||||
r.expr = s.expr.as_deref();
|
||||
|
|
@ -1060,7 +1076,7 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
|
|||
let pct = 100.0 * off as f64 / a.data.len().max(1) as f64;
|
||||
let speedup = cpu_warm / gpu_warm.max(1e-9);
|
||||
eprintln!(
|
||||
"{name:<28} {label:>5} {size:>10} {cpu_cold:>9.2} {gpu_cold:>9.2} {cpu_warm:>9.2} {gpu_warm:>9.2} {view:>9.2} {speedup:>7.2}x {pct:>8.3}%"
|
||||
"{name:<28} {label:>5} {size:>10} {cpu_cold:>9.2} {gpu_cold:>9.2} {cpu_warm:>9.2} {gpu_warm:>9.2} {view:>9.2} {speedup:>7.2}x {auto:>9.2} {pct:>8.3}% {traffic:>11}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -100,6 +100,15 @@ pub fn gpu_supported(id: &str, ctx: &crate::EffectCtx) -> bool {
|
|||
match id {
|
||||
"ec.generate.cellpattern" if (6..=10).contains(&ctx.params.e("cellPattern")) => return false,
|
||||
"ec.distort.turbulentdisplace" if ctx.params.e("pinning") >= 8 => return false,
|
||||
// Fisheye / Twist (a crease at the unit circle) bent past 50 % or with Horizontal /
|
||||
// Vertical Distortion: Newton's inverse wanders chaotically before converging near the
|
||||
// fold, so f32 lands on other pixels than the CPU's f64.
|
||||
"ec.distort.warp"
|
||||
if matches!(ctx.params.e("warpStyle"), 11 | 14)
|
||||
&& (ctx.params.f("bend").abs() > 50.0 || ctx.params.f("horizontalDistortion") != 0.0 || ctx.params.f("verticalDistortion") != 0.0) =>
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
let Some((_, controls)) = CPU_ONLY_CONTROLS.iter().find(|(e, _)| *e == id) else { return true };
|
||||
|
|
|
|||
|
|
@ -507,6 +507,15 @@ fn bez(p0: [f64; 2], p1: [f64; 2], p2: [f64; 2], p3: [f64; 2], t: f64) -> [f64;
|
|||
}
|
||||
|
||||
fn bezier_warp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let (n, dest, srcp) = bezier_grid(ctx, b.offset, b.scale);
|
||||
b.img = grid_warp(&b.img, b.img.width, b.img.height, n, n, &dest, &srcp);
|
||||
b
|
||||
}
|
||||
|
||||
/// Bezier Warp's n×n grid: (n, destination points, source points) in buffer pixels (the GPU
|
||||
/// rasterises the same grid).
|
||||
pub fn bezier_grid(ctx: &EffectCtx, offset: [f64; 2], scale: f64) -> (usize, Vec<Pt>, Vec<Pt>) {
|
||||
let to_px = |p: [f64; 2]| (p[0] * scale + offset[0], p[1] * scale + offset[1]);
|
||||
let g = |id: &str| ctx.params.v2(id);
|
||||
let (tl, tlt, trt, tr) = (g("topLeftVertex"), g("topLeftTangent"), g("topRightTangent"), g("rightTopVertex"));
|
||||
let (rtt, rbt, br) = (g("rightTopTangent"), g("rightBottomTangent"), g("bottomRightVertex"));
|
||||
|
|
@ -529,12 +538,11 @@ fn bezier_warp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let bil = (1.0 - u) * (1.0 - v) * tl[k] + u * (1.0 - v) * tr[k] + (1.0 - u) * v * bl[k] + u * v * br[k];
|
||||
s[k] = (1.0 - v) * c0[k] + v * c1[k] + (1.0 - u) * d0[k] + u * d1[k] - bil;
|
||||
}
|
||||
dest.push(b.to_px(s));
|
||||
srcp.push(b.to_px([u * lw, v * lh]));
|
||||
dest.push(to_px(s));
|
||||
srcp.push(to_px([u * lw, v * lh]));
|
||||
}
|
||||
}
|
||||
b.img = grid_warp(&b.img, b.img.width, b.img.height, n, n, &dest, &srcp);
|
||||
b
|
||||
(n, dest, srcp)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Bend It
|
||||
|
|
|
|||
|
|
@ -386,8 +386,25 @@ fn cc_split2(ctx: &EffectCtx, b: Buf) -> Buf {
|
|||
|
||||
// ---------------------------------------------------------------- Smear (mask based)
|
||||
|
||||
fn smear(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let (Some((src_m, _)), Some((bound, _))) = (mask_px(ctx, &b, "sourceMask"), mask_px(ctx, &b, "boundaryMask")) else { return b };
|
||||
/// Smear's set-up in buffer pixels (shared with the GPU kernel).
|
||||
pub struct SmearSetup {
|
||||
/// Boundary outline and the source outline moved by offset / rotation / scale.
|
||||
pub bound: Vec<[f64; 2]>,
|
||||
pub moved: Vec<[f64; 2]>,
|
||||
/// Source outline centroid, rotation (cos, sin), scale, offset (buffer pixels).
|
||||
pub c: [f64; 2],
|
||||
pub cs: f64,
|
||||
pub sn: f64,
|
||||
pub scl: f64,
|
||||
pub off: [f64; 2],
|
||||
pub pct: f64,
|
||||
/// Elasticity exponent.
|
||||
pub kexp: f64,
|
||||
}
|
||||
|
||||
/// `None` when Smear leaves the buffer as it is (a mask missing, Percent 0).
|
||||
pub fn smear_setup(ctx: &EffectCtx, b: &Buf) -> Option<SmearSetup> {
|
||||
let (Some((src_m, _)), Some((bound, _))) = (mask_px(ctx, b, "sourceMask"), mask_px(ctx, b, "boundaryMask")) else { return None };
|
||||
let pct = ctx.params.f("percent") / 100.0;
|
||||
let off = ctx.params.v2("maskOffset");
|
||||
let (ox, oy) = (off[0] * b.scale, off[1] * b.scale);
|
||||
|
|
@ -400,23 +417,30 @@ fn smear(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let (dx, dy) = ((p[0] - c[0]) * scl, (p[1] - c[1]) * scl);
|
||||
[c[0] + dx * cs - dy * sn + ox, c[1] + dx * sn + dy * cs + oy]
|
||||
};
|
||||
let moved: Vec<[f64; 2]> = src_m.iter().map(|&q| fwd(q)).collect();
|
||||
if pct == 0.0 {
|
||||
return None;
|
||||
}
|
||||
Some(SmearSetup { bound, moved, c, cs, sn, scl, off: [ox, oy], pct, kexp })
|
||||
}
|
||||
|
||||
fn smear(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let Some(s) = smear_setup(ctx, &b) else { return b };
|
||||
let (c, cs, sn, scl, [ox, oy]) = (s.c, s.cs, s.sn, s.scl, s.off);
|
||||
let inv = |x: f64, y: f64| -> (f64, f64) {
|
||||
let (dx, dy) = (x - ox - c[0], y - oy - c[1]);
|
||||
(c[0] + (dx * cs + dy * sn) / scl, c[1] + (-dx * sn + dy * cs) / scl)
|
||||
};
|
||||
let moved: Vec<[f64; 2]> = src_m.iter().map(|&q| fwd(q)).collect();
|
||||
if pct == 0.0 {
|
||||
return b;
|
||||
}
|
||||
let (bound, moved, pct, kexp) = (&s.bound, &s.moved, s.pct, s.kexp);
|
||||
b.img = remap(&b.img, false, |x, y| {
|
||||
if !point_in_poly(&bound, x, y) {
|
||||
if !point_in_poly(bound, x, y) {
|
||||
return Some((x, y));
|
||||
}
|
||||
let w = if point_in_poly(&moved, x, y) {
|
||||
let w = if point_in_poly(moved, x, y) {
|
||||
1.0
|
||||
} else {
|
||||
let dm = dist_to_poly(&moved, true, x, y);
|
||||
let db = dist_to_poly(&bound, true, x, y);
|
||||
let dm = dist_to_poly(moved, true, x, y);
|
||||
let db = dist_to_poly(bound, true, x, y);
|
||||
(db / (db + dm).max(1e-9)).powf(1.0 / kexp)
|
||||
};
|
||||
let (ix, iy) = inv(x, y);
|
||||
|
|
@ -464,15 +488,33 @@ fn map_fraction(pairs: &[(f64, f64)], u: f64, closed: bool) -> f64 {
|
|||
(a.0 + ds * t).rem_euclid(1.0)
|
||||
}
|
||||
|
||||
fn reshape(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let (Some((sm, sc)), Some((dm, dc))) = (mask_px(ctx, &b, "sourceMask"), mask_px(ctx, &b, "destinationMask")) else { return b };
|
||||
/// Points Reshape samples each outline at.
|
||||
pub const RESHAPE_POINTS: usize = 48;
|
||||
|
||||
/// Reshape's set-up in buffer pixels (shared with the GPU kernel).
|
||||
pub struct ReshapeSetup {
|
||||
/// The destination outline's sample points and the displacement back to the source
|
||||
/// outline at each.
|
||||
pub dest: Vec<[f64; 2]>,
|
||||
pub disp: Vec<[f64; 2]>,
|
||||
pub bound: Option<Vec<[f64; 2]>>,
|
||||
/// Elasticity exponent.
|
||||
pub kexp: f64,
|
||||
pub pct: f64,
|
||||
/// Smooth interpolation of the boundary fade.
|
||||
pub smooth: bool,
|
||||
}
|
||||
|
||||
/// `None` when Reshape leaves the buffer as it is (a mask missing, Percent 0).
|
||||
pub fn reshape_setup(ctx: &EffectCtx, b: &Buf) -> Option<ReshapeSetup> {
|
||||
let (Some((sm, sc)), Some((dm, dc))) = (mask_px(ctx, b, "sourceMask"), mask_px(ctx, b, "destinationMask")) else { return None };
|
||||
let pct = ctx.params.f("percent") / 100.0;
|
||||
if pct == 0.0 {
|
||||
return b;
|
||||
return None;
|
||||
}
|
||||
let bound = mask_px(ctx, &b, "boundaryMask").map(|m| m.0);
|
||||
let bound = mask_px(ctx, b, "boundaryMask").map(|m| m.0);
|
||||
let kexp = elastic_exp(ctx.params.e("elasticity"));
|
||||
const N: usize = 48;
|
||||
const N: usize = RESHAPE_POINTS;
|
||||
let s = resample_poly(&sm, sc, N);
|
||||
let d = resample_poly(&dm, dc, N);
|
||||
// Align the starting correspondence point (closed shapes): best cyclic rotation.
|
||||
|
|
@ -506,6 +548,12 @@ fn reshape(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
})
|
||||
.collect();
|
||||
let smooth = ctx.params.e("interpolation") == 2;
|
||||
Some(ReshapeSetup { dest: d, disp, bound, kexp, pct, smooth })
|
||||
}
|
||||
|
||||
fn reshape(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
const N: usize = RESHAPE_POINTS;
|
||||
let Some(ReshapeSetup { dest: d, disp, bound, kexp, pct, smooth }) = reshape_setup(ctx, &b) else { return b };
|
||||
b.img = remap(&b.img, false, |x, y| {
|
||||
if let Some(bd) = &bound
|
||||
&& !point_in_poly(bd, x, y)
|
||||
|
|
|
|||
|
|
@ -74,7 +74,8 @@ pub use color_fx::{
|
|||
pub use color2::{Curve, auto_correct_settings, equalize_tables, luma_clip_points, shadow_highlight_amounts};
|
||||
pub use color3::OffsetCurve;
|
||||
pub use distort::transform_shutter;
|
||||
pub use distort2::{MAGNIFY_MODES, parse_mesh};
|
||||
pub use distort2::{MAGNIFY_MODES, bezier_grid, parse_mesh};
|
||||
pub use distort3::{RESHAPE_POINTS, ReshapeSetup, SmearSetup, reshape_setup, smear_setup};
|
||||
pub use distort4::liquify_mesh;
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::build::Ids;
|
||||
|
|
@ -914,6 +915,22 @@ pub const GPU_EFFECTS: &[&str] = &[
|
|||
"ec.channel.minimax",
|
||||
"ec.time.echo",
|
||||
"ec.time.posterizetime",
|
||||
// effectcraft-gpu fx_warp (warps, cartoon)
|
||||
"ec.distort.warp",
|
||||
"ec.distort.bezierwarp",
|
||||
"ec.distort.ccbendit",
|
||||
"ec.distort.ccpageturn",
|
||||
"ec.distort.smear",
|
||||
"ec.distort.reshape",
|
||||
"ec.stylize.coloremboss",
|
||||
"ec.stylize.cartoon",
|
||||
// effectcraft-gpu fx_extra (shapes, bevels)
|
||||
"ec.generate.circle",
|
||||
"ec.generate.ellipse",
|
||||
"ec.transition.iriswipe",
|
||||
"ec.perspective.bevelalpha",
|
||||
"ec.perspective.beveledges",
|
||||
"ec.obsolete.gaussianlegacy",
|
||||
];
|
||||
|
||||
/// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ pub(crate) fn apply(e: &mut Enc, src: &GpuImage, d: &PlaneDof) -> Option<GpuImag
|
|||
let padded = e.image(w, h);
|
||||
e.copy_into(src, &padded, d.pad, d.pad);
|
||||
let boosted = if d.highlight.gain > 0.0 {
|
||||
let out = e.image(w, h);
|
||||
let out = e.scratch(w, h);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [d.highlight.gain as f32, d.highlight.threshold as f32, d.highlight.saturation as f32, 0.0];
|
||||
e.pixels("bokeh_boost", &p, &padded, None, &out, None);
|
||||
|
|
@ -47,7 +47,7 @@ pub(crate) fn apply(e: &mut Enc, src: &GpuImage, d: &PlaneDof) -> Option<GpuImag
|
|||
let (lo, hi) = (d.levels[0], d.levels[d.levels.len() - 1]);
|
||||
let mut acc = e.image(w, h);
|
||||
for (k, (off, count, norm)) in levels.into_iter().enumerate() {
|
||||
let out = e.image(w, h);
|
||||
let out = e.scratch(w, h);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [off, count, k as u32, n];
|
||||
p.u[1] = [u32::from(k == 0), 0, 0, 0];
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ pub(crate) fn draw_run(e: &mut Enc, run: &Run3d, canvas: &GpuImage) -> Option<Gp
|
|||
let mb = e.data(&mattes);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [run.planes.len() as u32, run.lights.len() as u32, run.casters.len() as u32, 0];
|
||||
let out = e.image(w, h);
|
||||
let out = e.scratch(w, h);
|
||||
e.dispatch_ext("classic3d", &p, &atlas, Some(canvas), &out, Some(&mb), (w.div_ceil(16), h.div_ceil(16)), Some([&pb, &gb, &lb, &cb]));
|
||||
Some(out)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! Device, pipelines, textures, uploads and readback.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
use std::sync::{Arc, Mutex, Weak};
|
||||
|
||||
use effectcraft_effects::Buf;
|
||||
|
|
@ -30,6 +30,7 @@ const ENTRIES: &[&str] = &[
|
|||
"bokeh_boost",
|
||||
"bokeh_prefix",
|
||||
"bokeh_gather",
|
||||
"fill",
|
||||
];
|
||||
|
||||
/// Entry points that also bind group 1 (four read-only storage buffers; see
|
||||
|
|
@ -43,12 +44,157 @@ pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba32Float;
|
|||
/// Uploaded layer buffers kept on the GPU (bytes).
|
||||
const UPLOAD_BUDGET: usize = 1536 << 20;
|
||||
|
||||
/// Working textures kept for reuse once their last image is dropped (bytes).
|
||||
const POOL_BUDGET: usize = 768 << 20;
|
||||
|
||||
/// A GPU image: premultiplied RGBA f32 in a texture (cheap to clone; immutable once written).
|
||||
/// Working images come from the context's texture pool and go back to it when the last clone
|
||||
/// is dropped.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GpuImage {
|
||||
pub texture: wgpu::Texture,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
_lease: Option<Arc<Lease>>,
|
||||
/// Already clamped and quantised to this many levels (8/16 bpc), so quantising again is a
|
||||
/// no-op (see `ops::quantize`).
|
||||
pub(crate) levels: Option<f32>,
|
||||
}
|
||||
|
||||
impl GpuImage {
|
||||
/// An image on a texture of its own (not pooled).
|
||||
pub fn new(texture: wgpu::Texture, width: u32, height: u32) -> GpuImage {
|
||||
GpuImage { texture, width, height, _lease: None, levels: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// Hands a pooled texture back to its [`Pool`] when the last [`GpuImage`] holding it drops.
|
||||
struct Lease {
|
||||
texture: wgpu::Texture,
|
||||
pool: Weak<Mutex<Pool>>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Lease {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("Lease")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Lease {
|
||||
fn drop(&mut self) {
|
||||
if let Some(pool) = self.pool.upgrade()
|
||||
&& let Ok(mut p) = pool.lock()
|
||||
{
|
||||
p.release(self.texture.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recycled working textures: allocating (and zero-initialising) a 1080p RGBA f32 texture
|
||||
/// costs more than compositing a small layer into it, and a frame makes several per layer.
|
||||
///
|
||||
/// A released texture may still be read by commands recorded in an encoder that has not been
|
||||
/// submitted yet. Reusing it in another encoder that is submitted first would overwrite it
|
||||
/// before those reads run, so a released texture waits until every encoder that was open when
|
||||
/// it was released has been submitted (or dropped).
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Pool {
|
||||
free: HashMap<(u32, u32), Vec<wgpu::Texture>>,
|
||||
/// (texture, newest encoder open at its release).
|
||||
released: Vec<(wgpu::Texture, u64)>,
|
||||
/// Encoders with recorded, unsubmitted work.
|
||||
open: BTreeSet<u64>,
|
||||
next: u64,
|
||||
bytes: usize,
|
||||
/// Working textures reused / newly allocated (diagnostics, tests).
|
||||
hits: u64,
|
||||
misses: u64,
|
||||
}
|
||||
|
||||
/// Counts of uploads (CPU → GPU) and readbacks (GPU → CPU).
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Transfers {
|
||||
ups: std::sync::atomic::AtomicU64,
|
||||
up_bytes: std::sync::atomic::AtomicU64,
|
||||
downs: std::sync::atomic::AtomicU64,
|
||||
down_bytes: std::sync::atomic::AtomicU64,
|
||||
}
|
||||
|
||||
/// A snapshot of [`GpuContext::transfer_stats`].
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct TransferStats {
|
||||
pub uploads: u64,
|
||||
pub upload_bytes: u64,
|
||||
pub readbacks: u64,
|
||||
pub readback_bytes: u64,
|
||||
}
|
||||
|
||||
impl Transfers {
|
||||
fn count(&self, up: bool, bytes: usize) {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
let (n, b) = if up { (&self.ups, &self.up_bytes) } else { (&self.downs, &self.down_bytes) };
|
||||
n.fetch_add(1, Relaxed);
|
||||
b.fetch_add(bytes as u64, Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
fn tex_bytes(t: &wgpu::Texture) -> usize {
|
||||
t.width() as usize * t.height() as usize * 16
|
||||
}
|
||||
|
||||
impl Pool {
|
||||
fn release(&mut self, t: wgpu::Texture) {
|
||||
match self.open.last() {
|
||||
Some(&newest) => self.released.push((t, newest)),
|
||||
None => self.make_free(t),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_free(&mut self, t: wgpu::Texture) {
|
||||
let b = tex_bytes(&t);
|
||||
if self.bytes + b > POOL_BUDGET {
|
||||
return;
|
||||
}
|
||||
self.bytes += b;
|
||||
self.free.entry((t.width(), t.height())).or_default().push(t);
|
||||
}
|
||||
|
||||
fn open(&mut self) -> u64 {
|
||||
self.next += 1;
|
||||
self.open.insert(self.next);
|
||||
self.next
|
||||
}
|
||||
|
||||
/// Encoder `id` was submitted or dropped: free what no open encoder can still read.
|
||||
fn close(&mut self, id: u64) {
|
||||
self.open.remove(&id);
|
||||
let oldest = self.open.first().copied().unwrap_or(u64::MAX);
|
||||
if self.released.iter().all(|(_, w)| *w >= oldest) {
|
||||
return;
|
||||
}
|
||||
let (ready, wait): (Vec<_>, Vec<_>) = std::mem::take(&mut self.released).into_iter().partition(|(_, w)| *w < oldest);
|
||||
self.released = wait;
|
||||
for (t, _) in ready {
|
||||
self.make_free(t);
|
||||
}
|
||||
}
|
||||
|
||||
fn take(&mut self, w: u32, h: u32) -> Option<wgpu::Texture> {
|
||||
let t = self.free.get_mut(&(w, h)).and_then(Vec::pop);
|
||||
match &t {
|
||||
Some(t) => {
|
||||
self.bytes -= tex_bytes(t);
|
||||
self.hits += 1;
|
||||
}
|
||||
None => self.misses += 1,
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn clear(&mut self) {
|
||||
self.free.clear();
|
||||
self.bytes = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Uniform parameter block shared by every kernel (see `shaders/common.wgsl`).
|
||||
|
|
@ -100,9 +246,17 @@ pub struct GpuContext {
|
|||
pipelines: HashMap<&'static str, wgpu::ComputePipeline>,
|
||||
display_bgl: wgpu::BindGroupLayout,
|
||||
display: wgpu::ComputePipeline,
|
||||
dummy: wgpu::Texture,
|
||||
dummy_tex: wgpu::TextureView,
|
||||
dummy_buf: wgpu::Buffer,
|
||||
uploads: Mutex<Uploads>,
|
||||
pool: Arc<Mutex<Pool>>,
|
||||
/// CPU ↔ GPU traffic (diagnostics: `bench --gpu`).
|
||||
pub(crate) transfers: Transfers,
|
||||
/// Readback staging buffers by size.
|
||||
staging: Mutex<HashMap<u64, Vec<wgpu::Buffer>>>,
|
||||
/// Transparent read-only images by size (see [`Enc::zeros`]).
|
||||
zeros: Mutex<HashMap<(u32, u32), GpuImage>>,
|
||||
/// Advanced 3D render pipelines (built on first use).
|
||||
pub(crate) adv3d: std::sync::OnceLock<crate::adv3d::Pipes>,
|
||||
/// GPU particle pipeline (built on first use) and simulation checkpoints.
|
||||
|
|
@ -158,6 +312,8 @@ impl GpuContext {
|
|||
include_str!("shaders/fx_stylize.wgsl"),
|
||||
include_str!("shaders/fx_noise.wgsl"),
|
||||
include_str!("shaders/fx_tone.wgsl"),
|
||||
include_str!("shaders/fx_warp.wgsl"),
|
||||
include_str!("shaders/fx_extra.wgsl"),
|
||||
]
|
||||
.concat();
|
||||
let module =
|
||||
|
|
@ -215,6 +371,8 @@ impl GpuContext {
|
|||
.chain(crate::fx_stylize::KERNELS)
|
||||
.chain(crate::fx_noise::KERNELS)
|
||||
.chain(crate::fx_tone::KERNELS)
|
||||
.chain(crate::fx_warp::KERNELS)
|
||||
.chain(crate::fx_extra::KERNELS)
|
||||
.map(|e| (e, &layout))
|
||||
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
||||
.map(|(e, layout)| {
|
||||
|
|
@ -274,8 +432,13 @@ impl GpuContext {
|
|||
display_bgl,
|
||||
display,
|
||||
dummy_tex: dummy.create_view(&Default::default()),
|
||||
dummy,
|
||||
dummy_buf,
|
||||
uploads: Mutex::new(Uploads::default()),
|
||||
pool: Default::default(),
|
||||
staging: Default::default(),
|
||||
transfers: Default::default(),
|
||||
zeros: Default::default(),
|
||||
adv3d: std::sync::OnceLock::new(),
|
||||
particles: Default::default(),
|
||||
particle_states: Default::default(),
|
||||
|
|
@ -300,7 +463,13 @@ impl GpuContext {
|
|||
let (device, queue) =
|
||||
pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor { label: Some("effectcraft gpu"), required_limits, ..Default::default() }))
|
||||
.ok()?;
|
||||
device.on_uncaptured_error(Arc::new(|e| log::error!("wgpu: {e}")));
|
||||
device.on_uncaptured_error(Arc::new(|e| {
|
||||
// Under test a validation error (a WGSL typo invalidates every kernel) fails loudly.
|
||||
if cfg!(test) {
|
||||
panic!("wgpu: {e}");
|
||||
}
|
||||
log::error!("wgpu: {e}")
|
||||
}));
|
||||
GpuContext::new(&adapter, device, queue).map_err(|e| log::info!("gpu: {e}")).ok()
|
||||
}
|
||||
|
||||
|
|
@ -313,9 +482,13 @@ impl GpuContext {
|
|||
w > 0 && h > 0 && w <= self.max_dim && h <= self.max_dim
|
||||
}
|
||||
|
||||
/// A new (zeroed) working texture.
|
||||
/// A new (zeroed) texture of its own (not pooled).
|
||||
pub(crate) fn image(&self, w: u32, h: u32) -> GpuImage {
|
||||
let texture = self.device.create_texture(&wgpu::TextureDescriptor {
|
||||
GpuImage::new(self.texture(w, h), w, h)
|
||||
}
|
||||
|
||||
fn texture(&self, w: u32, h: u32) -> wgpu::Texture {
|
||||
self.device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: None,
|
||||
size: wgpu::Extent3d { width: w.max(1), height: h.max(1), depth_or_array_layers: 1 },
|
||||
mip_level_count: 1,
|
||||
|
|
@ -324,8 +497,35 @@ impl GpuContext {
|
|||
format: FORMAT,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
GpuImage { texture, width: w, height: h }
|
||||
})
|
||||
}
|
||||
|
||||
/// A pooled working texture: (image, reused). A reused texture holds old pixels; a new
|
||||
/// one is zeroed.
|
||||
fn pooled(&self, w: u32, h: u32) -> (GpuImage, bool) {
|
||||
let (w, h) = (w.max(1), h.max(1));
|
||||
let reused = self.pool.lock().ok().and_then(|mut p| p.take(w, h));
|
||||
let was_reused = reused.is_some();
|
||||
let texture = reused.unwrap_or_else(|| self.texture(w, h));
|
||||
let lease = Arc::new(Lease { texture: texture.clone(), pool: Arc::downgrade(&self.pool) });
|
||||
(GpuImage { texture, width: w, height: h, _lease: Some(lease), levels: None }, was_reused)
|
||||
}
|
||||
|
||||
/// Uploads and readbacks so far (images; uploads of cached layers count once).
|
||||
pub fn transfer_stats(&self) -> TransferStats {
|
||||
use std::sync::atomic::Ordering::Relaxed;
|
||||
let t = &self.transfers;
|
||||
TransferStats {
|
||||
uploads: t.ups.load(Relaxed),
|
||||
upload_bytes: t.up_bytes.load(Relaxed),
|
||||
readbacks: t.downs.load(Relaxed),
|
||||
readback_bytes: t.down_bytes.load(Relaxed),
|
||||
}
|
||||
}
|
||||
|
||||
/// Texture pool counters: (working textures reused, newly allocated).
|
||||
pub fn pool_stats(&self) -> (u64, u64) {
|
||||
self.pool.lock().map(|p| (p.hits, p.misses)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Upload a CPU image (`None` when it does not fit the device).
|
||||
|
|
@ -333,6 +533,7 @@ impl GpuContext {
|
|||
if !self.fits(img.width, img.height) {
|
||||
return None;
|
||||
}
|
||||
self.transfers.count(true, img.data.len() * 16);
|
||||
let g = self.image(img.width, img.height);
|
||||
self.queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo { texture: &g.texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
|
||||
|
|
@ -390,16 +591,34 @@ impl GpuContext {
|
|||
u.map.clear();
|
||||
u.bytes = 0;
|
||||
}
|
||||
if let Ok(mut p) = self.pool.lock() {
|
||||
p.clear();
|
||||
}
|
||||
if let Ok(mut s) = self.staging.lock() {
|
||||
s.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Records GPU work for one render (a command encoder submitted at readback or the end).
|
||||
pub(crate) struct Enc<'g> {
|
||||
pub g: &'g GpuContext,
|
||||
enc: Option<wgpu::CommandEncoder>,
|
||||
/// The open command encoder and its id in the texture pool.
|
||||
enc: Option<(wgpu::CommandEncoder, u64)>,
|
||||
pending: usize,
|
||||
}
|
||||
|
||||
impl Drop for Enc<'_> {
|
||||
fn drop(&mut self) {
|
||||
// Unsubmitted work is discarded, so nothing recorded will read pooled textures.
|
||||
if let Some((_, id)) = self.enc.take()
|
||||
&& let Ok(mut p) = self.g.pool.lock()
|
||||
{
|
||||
p.close(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'g> Enc<'g> {
|
||||
pub fn new(g: &'g GpuContext) -> Enc<'g> {
|
||||
Enc { g, enc: None, pending: 0 }
|
||||
|
|
@ -407,12 +626,50 @@ impl<'g> Enc<'g> {
|
|||
|
||||
pub(crate) fn encoder(&mut self) -> &mut wgpu::CommandEncoder {
|
||||
let g = self.g;
|
||||
self.enc.get_or_insert_with(|| g.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("effectcraft") }))
|
||||
&mut self
|
||||
.enc
|
||||
.get_or_insert_with(|| {
|
||||
let id = g.pool.lock().map(|mut p| p.open()).unwrap_or(0);
|
||||
(g.device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("effectcraft") }), id)
|
||||
})
|
||||
.0
|
||||
}
|
||||
|
||||
/// A new zeroed image.
|
||||
pub fn image(&self, w: u32, h: u32) -> GpuImage {
|
||||
self.g.image(w, h)
|
||||
/// A new zeroed image (pooled; a reused texture is cleared by a kernel).
|
||||
pub fn image(&mut self, w: u32, h: u32) -> GpuImage {
|
||||
let (img, reused) = self.g.pooled(w, h);
|
||||
if reused {
|
||||
self.fill(&img, 0.0);
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
/// A new image for a kernel that writes every pixel of it (pooled; contents undefined).
|
||||
/// Under test it starts out as NaNs, so a kernel that skips pixels fails the CPU oracle.
|
||||
pub fn scratch(&mut self, w: u32, h: u32) -> GpuImage {
|
||||
let (img, _) = self.g.pooled(w, h);
|
||||
if cfg!(test) {
|
||||
self.fill(&img, f32::NAN);
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
/// A transparent image to read from (shared per size; never written).
|
||||
pub fn zeros(&self, w: u32, h: u32) -> GpuImage {
|
||||
let (w, h) = (w.max(1), h.max(1));
|
||||
let Ok(mut z) = self.g.zeros.lock() else { return self.g.image(w, h) };
|
||||
if z.len() > 8 && !z.contains_key(&(w, h)) {
|
||||
z.clear();
|
||||
}
|
||||
z.entry((w, h)).or_insert_with(|| self.g.image(w, h)).clone()
|
||||
}
|
||||
|
||||
/// Set every pixel of `img` to `v`.
|
||||
fn fill(&mut self, img: &GpuImage, v: f32) {
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [v; 4];
|
||||
let dummy = GpuImage::new(self.g.dummy.clone(), 1, 1);
|
||||
self.pixels("fill", &p, &dummy, None, img, None);
|
||||
}
|
||||
|
||||
/// Run kernel `entry` writing `out` (dispatched over `groups` workgroups).
|
||||
|
|
@ -509,37 +766,46 @@ impl<'g> Enc<'g> {
|
|||
|
||||
/// Submit recorded work.
|
||||
pub fn submit(&mut self) {
|
||||
if let Some(enc) = self.enc.take() {
|
||||
if let Some((enc, id)) = self.enc.take() {
|
||||
self.g.queue.submit([enc.finish()]);
|
||||
if let Ok(mut p) = self.g.pool.lock() {
|
||||
p.close(id);
|
||||
}
|
||||
}
|
||||
self.pending = 0;
|
||||
}
|
||||
|
||||
/// Read an image back to the CPU (submits and waits). `None` where readback is impossible.
|
||||
pub fn download(&mut self, img: &GpuImage) -> Option<Image> {
|
||||
let bytes = self.read_texture(&img.texture, img.width, img.height, 16)?;
|
||||
let mut out = Image::new(img.width, img.height);
|
||||
for (p, c) in out.data.iter_mut().zip(bytes.chunks_exact(16)) {
|
||||
for (k, v) in p.iter_mut().enumerate() {
|
||||
*v = f32::from_le_bytes([c[4 * k], c[4 * k + 1], c[4 * k + 2], c[4 * k + 3]]);
|
||||
}
|
||||
}
|
||||
let dst: &mut [u8] = bytemuck::cast_slice_mut(&mut out.data);
|
||||
self.read_texture_into(&img.texture, img.width, img.height, 16, dst)?;
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// Tightly packed texel bytes of a texture (`bpp` bytes per pixel).
|
||||
pub fn read_texture(&mut self, texture: &wgpu::Texture, w: u32, h: u32, bpp: u32) -> Option<Vec<u8>> {
|
||||
let mut out = vec![0u8; (w * bpp) as usize * h as usize];
|
||||
self.read_texture_into(texture, w, h, bpp, &mut out)?;
|
||||
Some(out)
|
||||
}
|
||||
|
||||
/// [`Enc::read_texture`] into `out` (`w × bpp × h` bytes), through a reused staging buffer.
|
||||
fn read_texture_into(&mut self, texture: &wgpu::Texture, w: u32, h: u32, bpp: u32, out: &mut [u8]) -> Option<()> {
|
||||
if !self.g.can_readback() {
|
||||
return None;
|
||||
}
|
||||
let row = w * bpp;
|
||||
let padded = row.div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||
self.g.transfers.count(false, out.len());
|
||||
let row = (w * bpp) as usize;
|
||||
let padded = (w * bpp).div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
|
||||
let size = padded as u64 * h as u64;
|
||||
let buf = self.g.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("readback"),
|
||||
size,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
let buf = self.g.staging.lock().ok().and_then(|mut s| s.get_mut(&size).and_then(Vec::pop)).unwrap_or_else(|| {
|
||||
self.g.device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("readback"),
|
||||
size,
|
||||
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
})
|
||||
});
|
||||
self.encoder().copy_texture_to_buffer(
|
||||
wgpu::TexelCopyTextureInfo { texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
|
||||
|
|
@ -556,14 +822,26 @@ impl<'g> Enc<'g> {
|
|||
return None;
|
||||
}
|
||||
rx.recv().ok()?.ok()?;
|
||||
let view = buf.get_mapped_range(..).ok()?;
|
||||
let mut out = Vec::with_capacity((row * h) as usize);
|
||||
for y in 0..h as usize {
|
||||
out.extend_from_slice(&view[y * padded as usize..y * padded as usize + row as usize]);
|
||||
{
|
||||
let view = buf.get_mapped_range(..).ok()?;
|
||||
if row == padded as usize {
|
||||
out.copy_from_slice(&view[..out.len()]);
|
||||
} else {
|
||||
for (y, dst) in out.chunks_exact_mut(row).enumerate() {
|
||||
let s = y * padded as usize;
|
||||
dst.copy_from_slice(&view[s..s + row]);
|
||||
}
|
||||
}
|
||||
}
|
||||
drop(view);
|
||||
buf.unmap();
|
||||
Some(out)
|
||||
// Keep a few staging buffers per size (frames read back at the same size every time).
|
||||
if let Ok(mut s) = self.g.staging.lock() {
|
||||
let v = s.entry(size).or_default();
|
||||
if v.len() < 2 && size <= 256 << 20 {
|
||||
v.push(buf);
|
||||
}
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
/// [`Enc::read_texture`] without waiting for the GPU (the browser's main thread can't): `done`
|
||||
|
|
|
|||
|
|
@ -131,6 +131,8 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
_ if crate::fx_stylize::IDS.contains(&id) => crate::fx_stylize::apply(e, id, ctx, b),
|
||||
_ if crate::fx_noise::IDS.contains(&id) => crate::fx_noise::apply(e, id, ctx, b),
|
||||
_ if crate::fx_tone::IDS.contains(&id) => crate::fx_tone::apply(e, id, ctx, b),
|
||||
_ if crate::fx_warp::IDS.contains(&id) => crate::fx_warp::apply(e, id, ctx, b),
|
||||
_ if crate::fx_extra::IDS.contains(&id) => crate::fx_extra::apply(e, id, ctx, b),
|
||||
_ => pointwise(e, id, ctx, b),
|
||||
}
|
||||
}
|
||||
|
|
@ -162,7 +164,7 @@ pub(crate) fn box_passes(e: &mut Enc, img: &GpuImage, rx: &[usize], ry: &[usize]
|
|||
let (lines, n) = if vertical { (cur.width, cur.height) } else { (cur.height, cur.width) };
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [r as u32, repeat as u32, block, 0];
|
||||
let out = e.image(cur.width, cur.height);
|
||||
let out = e.scratch(cur.width, cur.height);
|
||||
e.dispatch(if vertical { "box_v" } else { "box_h" }, &p, &cur, None, &out, None, (lines.div_ceil(64), n.div_ceil(block)));
|
||||
cur = out;
|
||||
}
|
||||
|
|
@ -225,7 +227,7 @@ fn directional(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
let n = (len.ceil() as usize * 2 + 1).clamp(3, 257);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [a.sin() as f32, -a.cos() as f32, len as f32, n as f32];
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("directional", &p, &b.img, None, &out, None);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
|
|
@ -258,7 +260,7 @@ fn glow(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.e("operation").min(2);
|
||||
p.f[0][0] = intensity;
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("glow_combine", &p, &b.img, Some(&blurred), &out, None);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
|
|
@ -285,7 +287,7 @@ fn drop_shadow(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
sh = gaussian_blur(e, &sh, soft / 2.0, soft / 2.0, false);
|
||||
}
|
||||
if !only {
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("shadow_combine", &Params::default(), &sh, Some(&b.img), &out, None);
|
||||
sh = out;
|
||||
}
|
||||
|
|
@ -338,7 +340,7 @@ fn curves(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
p.u[0][0] = 8;
|
||||
p.f[0] = [offs[0], offs[1], offs[2], offs[3]];
|
||||
p.f[1][0] = offs[4];
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("pointwise", &p, &b.img, None, &out, Some(&buf));
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
|
@ -464,7 +466,7 @@ fn pointwise(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
}
|
||||
_ => return None,
|
||||
}
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("pointwise", &p, &b.img, None, &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
}
|
||||
_ => return None,
|
||||
}
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxc_point", &p, &b.img, None, &out, data.as_ref());
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
|
@ -198,7 +198,7 @@ fn lumetri(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
]);
|
||||
debug_assert_eq!(d.len(), 50);
|
||||
let buf = e.data(&d);
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxc_lumetri", &Params::default(), &b.img, None, &out, Some(&buf));
|
||||
let sharpen = f("creative/adjustments/sharpen");
|
||||
if sharpen == 0.0 {
|
||||
|
|
@ -227,7 +227,7 @@ fn morph(e: &mut Enc, img: &GpuImage, r: u32, modes: [u32; 4]) -> GpuImage {
|
|||
let mut p = Params::default();
|
||||
p.u[0] = [r, vertical, 0, 0];
|
||||
p.u[1] = modes;
|
||||
let out = e.image(cur.width, cur.height);
|
||||
let out = e.scratch(cur.width, cur.height);
|
||||
e.pixels("fxc_morph", &p, &cur, None, &out, None);
|
||||
cur = out;
|
||||
}
|
||||
|
|
@ -248,7 +248,7 @@ pub(crate) fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32;
|
|||
let b = morph(e, img, r0 + 1, modes);
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = t;
|
||||
let out = e.image(img.width, img.height);
|
||||
let out = e.scratch(img.width, img.height);
|
||||
e.pixels("fxc_lerp", &p, &a, Some(&b), &out, None);
|
||||
out
|
||||
}
|
||||
|
|
@ -328,7 +328,7 @@ fn key_light(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
let mut m = raw.clone();
|
||||
if rollback > 0.0 {
|
||||
let sg_img = morph_frac(e, &m, rollback, [0, 1, 2, 2]);
|
||||
let out = e.image(w, h);
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxc_kl_rollback", &Params::default(), &sg_img, Some(&raw), &out, None);
|
||||
m = out;
|
||||
}
|
||||
|
|
@ -367,7 +367,7 @@ fn key_light(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
}
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = hard;
|
||||
let out = e.image(w, h);
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxc_kl_edge", &p, &planes, Some(&band), &out, None);
|
||||
planes = out;
|
||||
}
|
||||
|
|
@ -390,7 +390,7 @@ fn key_light(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
p.f[5] = ["sourceCrops/cropLeft", "sourceCrops/cropRight", "sourceCrops/cropTop", "sourceCrops/cropBottom"].map(|k| (pr.f(k) / 100.0) as f32);
|
||||
p.f[6] = [b.offset[0] as f32, b.offset[1] as f32, b.scale as f32, 0.0];
|
||||
p.f[7] = [ctx.layer_size[0].max(1e-9) as f32, ctx.layer_size[1].max(1e-9) as f32, 0.0, 0.0];
|
||||
let out = e.image(w, h);
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxc_kl_final", &p, &b.img, Some(&planes), &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<G
|
|||
|
||||
/// Run a same-size per-pixel kernel over the buffer.
|
||||
fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels(entry, p, &b.img, None, &out, data);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
|
|
@ -267,7 +267,7 @@ fn displacement_map(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
p.u[1] = [w as u32, h as u32, 0, 0];
|
||||
p.f[0] = [mh as f32, mv as f32, 0.0, 0.0];
|
||||
let buf = e.data(&data);
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("dst_displace", &p, &b.img, map.as_ref(), &out, Some(&buf));
|
||||
b.img = out;
|
||||
Some(b)
|
||||
|
|
@ -300,7 +300,12 @@ fn mesh_warp(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
srcp.push(b.to_px([gi as f64 / nx as f64 * lw, gj as f64 / ny as f64 * lh]));
|
||||
}
|
||||
}
|
||||
// distort2::grid_warp: triangles in order, bucketed per output row.
|
||||
grid_warp(e, b, nx, ny, &dest, &srcp)
|
||||
}
|
||||
|
||||
/// distort2::grid_warp: grid point `i` of the (nx+1)×(ny+1) grid at `dest[i]` shows the source at
|
||||
/// `srcp[i]` (Mesh Warp, Bezier Warp). Triangles in order, bucketed per output row.
|
||||
pub(crate) fn grid_warp(e: &mut Enc, b: GBuf, nx: usize, ny: usize, dest: &[(f64, f64)], srcp: &[(f64, f64)]) -> Option<GBuf> {
|
||||
let idx = |i: usize, j: usize| j * (nx + 1) + i;
|
||||
let hh = b.img.height as usize;
|
||||
let mut tri_data: Vec<f32> = Vec::with_capacity(nx * ny * 24);
|
||||
|
|
@ -390,7 +395,7 @@ fn mosaic(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
e.pixels("dst_mosaic_rows", &p, &b.img, None, &rows, Some(&buf));
|
||||
let tiles = e.image(nx as u32, ny as u32);
|
||||
e.pixels("dst_mosaic_tiles", &p, &rows, None, &tiles, Some(&buf));
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("dst_mosaic_out", &p, &tiles, None, &out, Some(&buf));
|
||||
b.img = out;
|
||||
Some(b)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,196 @@
|
|||
//! GPU effects, shapes and bevels (kernels in `shaders/fx_extra.wgsl`, every entry point
|
||||
//! prefixed `fxe_`): Circle, Ellipse, Iris Wipe, Bevel Alpha, Bevel Edges and Gaussian Blur
|
||||
//! (Legacy), with the CPU effects' exact steps. Radii, feathers, light directions and Iris
|
||||
//! Wipe's polygon are computed on the CPU in f64 as the CPU effect does; the kernels work
|
||||
//! relative to the shape's centre.
|
||||
|
||||
use effectcraft_effects::EffectCtx;
|
||||
|
||||
use crate::context::{Enc, Params};
|
||||
use crate::effects::{GBuf, gaussian_blur};
|
||||
|
||||
/// Compute entry points in `fx_extra.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["fxe_circle", "fxe_ellipse", "fxe_iris", "fxe_alpha", "fxe_bevel_alpha", "fxe_bevel_edges"];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.generate.circle",
|
||||
"ec.generate.ellipse",
|
||||
"ec.transition.iriswipe",
|
||||
"ec.perspective.bevelalpha",
|
||||
"ec.perspective.beveledges",
|
||||
"ec.obsolete.gaussianlegacy",
|
||||
];
|
||||
|
||||
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
match id {
|
||||
"ec.generate.circle" => circle(e, ctx, b),
|
||||
"ec.generate.ellipse" => ellipse(e, ctx, b),
|
||||
"ec.transition.iriswipe" => iris_wipe(e, ctx, b),
|
||||
"ec.perspective.bevelalpha" => bevel_alpha(e, ctx, b),
|
||||
"ec.perspective.beveledges" => bevel_edges(e, ctx, b),
|
||||
"ec.obsolete.gaussianlegacy" => gaussian_legacy(e, ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A same-size per-pixel kernel over the buffer.
|
||||
fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, aux: Option<&crate::context::GpuImage>, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels(entry, p, &b.img, aux, &out, data);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
fn f4(v: [f64; 4]) -> [f32; 4] {
|
||||
v.map(|x| x as f32)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Circle (generate::circle)
|
||||
|
||||
fn circle(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let r = pr.f("radius") * b.scale;
|
||||
let thick = pr.f("thickness").max(0.0);
|
||||
let mut f_out = pr.f("feather/feather").max(0.0) * b.scale;
|
||||
let mut f_in = pr.f("feather/featherInner").max(0.0) * b.scale;
|
||||
let inner = match pr.e("edge") {
|
||||
1 => Some(pr.f("edgeRadius") * b.scale),
|
||||
2 => Some(r - thick * b.scale),
|
||||
3 => Some(r - thick / 100.0 * r),
|
||||
4 => {
|
||||
f_out = pr.f("feather/feather").max(0.0) / 100.0 * r;
|
||||
f_in = pr.f("feather/featherInner").max(0.0) / 100.0 * r;
|
||||
Some(r - thick / 100.0 * r)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let (ro, ri) = match inner {
|
||||
Some(i) => (r.max(i), Some(r.min(i).max(0.0))),
|
||||
None => (r, None),
|
||||
};
|
||||
let (f_out, f_in) = (f_out.max(0.5), f_in.max(0.5));
|
||||
let color = pr.color("color");
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [crate::fx_generate::gen_mode(pr.e("blendingMode")), ri.is_some() as u32, pr.b("invert") as u32, 0];
|
||||
p.f[0] = f4([c.0, c.1, ro, ri.unwrap_or(0.0)]);
|
||||
p.f[1] = [f_out as f32, f_in as f32, pr.f("opacity") as f32 / 100.0, 0.0];
|
||||
p.f[2] = [color[0], color[1], color[2], 0.0];
|
||||
run(e, "fxe_circle", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Ellipse (generate2::ellipse)
|
||||
|
||||
fn ellipse(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let rx = (pr.f("width") * b.scale * 0.5).max(0.5);
|
||||
let ry = (pr.f("height") * b.scale * 0.5).max(0.5);
|
||||
let half = (pr.f("thickness") * b.scale * 0.5).max(0.25);
|
||||
let soft = (pr.f("softness") / 100.0).clamp(0.0, 1.0);
|
||||
let (inside, outside) = (pr.color("insideColor"), pr.color("outsideColor"));
|
||||
let mut p = Params::default();
|
||||
p.u[0][1] = pr.b("compositeOnOriginal") as u32;
|
||||
p.f[0] = f4([c.0, c.1, rx, ry]);
|
||||
p.f[1] = [half as f32, soft as f32, 0.0, 0.0];
|
||||
p.f[2] = [inside[0], inside[1], inside[2], 0.0];
|
||||
p.f[3] = [outside[0], outside[1], outside[2], 0.0];
|
||||
run(e, "fxe_ellipse", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Iris Wipe (transition::iris_wipe)
|
||||
|
||||
fn iris_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let outer = pr.f("outerRadius").max(0.0) * b.scale;
|
||||
if outer <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let n = pr.f("points").round().clamp(3.0, 64.0) as usize;
|
||||
let star = pr.b("useInnerRadius");
|
||||
let inner = if star { pr.f("innerRadius").max(0.0) * b.scale } else { outer };
|
||||
let rot = pr.f("rotation").to_radians();
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let m = if star { n * 2 } else { n };
|
||||
let step = std::f64::consts::TAU / m as f64;
|
||||
let verts: Vec<f32> = (0..m)
|
||||
.flat_map(|i| {
|
||||
let r = if star && i % 2 == 1 { inner } else { outer };
|
||||
let a = rot + i as f64 * step;
|
||||
[(r * a.sin()) as f32, (-r * a.cos()) as f32]
|
||||
})
|
||||
.collect();
|
||||
let buf = e.data(&verts);
|
||||
let mut p = Params::default();
|
||||
p.u[0][1] = m as u32;
|
||||
p.f[0] = f4([c.0, c.1, rot, step]);
|
||||
p.f[1] = f4([pr.f("feather") * b.scale, outer.min(inner), outer, 0.0]);
|
||||
run(e, "fxe_iris", &p, b, None, Some(&buf))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Bevels (perspective.rs)
|
||||
|
||||
/// perspective::light_dir.
|
||||
fn light_dir(angle_deg: f64, elevation_deg: f64) -> [f64; 3] {
|
||||
let (a, el) = (angle_deg.to_radians(), elevation_deg.to_radians());
|
||||
let v = [a.sin() * el.cos(), -a.cos() * el.cos(), el.sin()];
|
||||
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-12);
|
||||
v.map(|x| x / l)
|
||||
}
|
||||
|
||||
fn bevel_alpha(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let t = pr.f("edgeThickness") * b.scale;
|
||||
let inten = pr.f("lightIntensity") as f32;
|
||||
if t <= 0.0 || inten == 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let light = pr.color("lightColor");
|
||||
let l = light_dir(pr.f("lightAngle"), 45.0);
|
||||
// The alpha plane's Gaussian (util::gauss_plane: box passes, edges repeated); kernels read x.
|
||||
let alpha = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxe_alpha", &Params::default(), &b.img, None, &alpha, None);
|
||||
let h = gaussian_blur(e, &alpha, t * 0.5, t * 0.5, true);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = f4([l[0], l[1], l[2], t * 1.25]);
|
||||
p.f[1] = [light[0], light[1], light[2], inten];
|
||||
run(e, "fxe_bevel_alpha", &p, b, Some(&h), None)
|
||||
}
|
||||
|
||||
fn bevel_edges(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let (x0, y0, w, h) = (b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
|
||||
let t = pr.f("edgeThickness").clamp(0.0, 0.5) * w.min(h);
|
||||
let inten = pr.f("lightIntensity") as f32;
|
||||
if t <= 0.0 || inten == 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let light = pr.color("lightColor");
|
||||
let l = light_dir(pr.f("lightAngle"), 45.0);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = f4([l[0], l[1], l[2], t]);
|
||||
p.f[1] = [light[0], light[1], light[2], inten];
|
||||
p.f[2] = f4([x0, y0, w, h]);
|
||||
run(e, "fxe_bevel_edges", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Gaussian Blur (Legacy) (obsolete.rs)
|
||||
|
||||
fn gaussian_legacy(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let s = ctx.params.f("blurriness").max(0.0) * 0.5 * b.scale;
|
||||
if s <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let (kx, ky) = match ctx.params.e("blurDimensions") {
|
||||
1 => (1.0, 0.0),
|
||||
2 => (0.0, 1.0),
|
||||
_ => (1.0, 1.0),
|
||||
};
|
||||
if !ctx.adjustment {
|
||||
b.pad(e, (s * 3.0).ceil() as u32)?;
|
||||
}
|
||||
b.img = gaussian_blur(e, &b.img, s * kx, s * ky, ctx.adjustment);
|
||||
Some(b)
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<G
|
|||
/// Run a per-pixel kernel over the buffer.
|
||||
fn run(e: &mut Enc, entry: &str, p: &Params, b: GBuf, aux: Option<&crate::context::GpuImage>, data: Option<&[f32]>) -> Option<GBuf> {
|
||||
let buf = data.map(|d| e.data(d));
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels(entry, p, &b.img, aux, &out, buf.as_ref());
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
|
@ -83,7 +83,7 @@ fn hl(v: f64) -> [f32; 2] {
|
|||
}
|
||||
|
||||
/// Generate blending mode index → `BlendMode::ALL` index (255 = None: replace).
|
||||
fn gen_mode(i: u32) -> u32 {
|
||||
pub(crate) fn gen_mode(i: u32) -> u32 {
|
||||
match effectcraft_effects::gen_mode(i) {
|
||||
None => 255,
|
||||
Some(m) => BlendMode::ALL.iter().position(|&x| x == m).unwrap_or(0) as u32,
|
||||
|
|
@ -149,7 +149,7 @@ fn camera_lens(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
let mut p = Params::default();
|
||||
p.u[0] = [linear as u32, (boost > 1.0) as u32, 0, 0];
|
||||
p.f[0] = [thr, boost, sat, 0.0];
|
||||
let out = e.image(w, h);
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("gen_lens_pre", &p, &src, None, &out, None);
|
||||
src = out;
|
||||
}
|
||||
|
|
@ -172,7 +172,7 @@ fn camera_lens(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
p.u[0] = [spans.len() as u32, repeat as u32, linear as u32, prefix as u32];
|
||||
p.f[0][0] = norm as f32;
|
||||
let buf = e.data(&data);
|
||||
let out = e.image(w, h);
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("gen_lens_blur", &p, &src, pre.as_ref(), &out, Some(&buf));
|
||||
b.img = out;
|
||||
Some(b)
|
||||
|
|
@ -275,7 +275,7 @@ fn gradient_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
let mut p = Params::default();
|
||||
p.u[0][0] = ctx.params.b("invert") as u32;
|
||||
p.f[0] = [done, soft, 0.0, 0.0];
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("gen_gradient_wipe", &p, &b.img, Some(grad.as_ref().unwrap_or(&b.img)), &out, None);
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<G
|
|||
|
||||
/// One `fxk_point` pass over `src`'s size.
|
||||
fn run(e: &mut Enc, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage {
|
||||
let out = e.image(src.width, src.height);
|
||||
let out = e.scratch(src.width, src.height);
|
||||
e.pixels("fxk_point", p, src, aux, &out, data);
|
||||
out
|
||||
}
|
||||
|
|
@ -105,7 +105,7 @@ fn fitted(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, masks_and_effects: b
|
|||
data.extend((0..h).map(|y| (((y as f64 + 0.5 - b.offset[1]) * inv * sy + dy) * o.buf.scale + o.buf.offset[1]) as f32));
|
||||
let src = e.g.upload_image(&o.buf.img)?;
|
||||
let buf = e.data(&data);
|
||||
let out = e.image(w, h);
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxk_fit", &Params::default(), &src, None, &out, Some(&buf));
|
||||
Some(Some(out))
|
||||
}
|
||||
|
|
@ -272,7 +272,7 @@ fn advanced_spill(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
let mut cur = b.img.clone();
|
||||
let mut first = true;
|
||||
while first || cur.width > 1 || cur.height > 1 {
|
||||
let out = e.image(cur.width.div_ceil(16), cur.height.div_ceil(16));
|
||||
let out = e.scratch(cur.width.div_ceil(16), cur.height.div_ceil(16));
|
||||
let mut rp = Params::default();
|
||||
rp.u[0][0] = first as u32;
|
||||
e.pixels("fxk_reduce", &rp, &cur, None, &out, None);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
//! `fxn_`): the CPU effects' exact steps with the pixel loops as compute kernels.
|
||||
//!
|
||||
//! Median, Median (Legacy) and Dust & Scratches take the median of the 512-level quantised
|
||||
//! channels by bisection over the window (radius ≤ [`MEDIAN_MAX_R`] on the GPU). Fractal and
|
||||
//! channels: by bisection over the window up to radius [`MEDIAN_BISECT_R`], above that with the
|
||||
//! CPU's sliding 512-bin histogram per channel (one invocation per row segment). Fractal and
|
||||
//! Turbulent Noise resolve every per-octave term on the CPU in the CPU's f32 order
|
||||
//! (`effectcraft_effects::fractal_gpu`) and evaluate the pixels on the GPU. Echo and Posterize
|
||||
//! Time fetch the layer's other frames through the effect host like the CPU does, then
|
||||
|
|
@ -21,6 +22,7 @@ use crate::effects::{GBuf, box_passes, gaussian_blur};
|
|||
pub(crate) const KERNELS: &[&str] = &[
|
||||
"fxn_quant",
|
||||
"fxn_median",
|
||||
"fxn_median_huang",
|
||||
"fxn_median_out",
|
||||
"fxn_edge",
|
||||
"fxn_smart_edges",
|
||||
|
|
@ -66,8 +68,10 @@ pub(crate) const IDS: &[&str] = &[
|
|||
"ec.time.posterizetime",
|
||||
];
|
||||
|
||||
/// Largest median radius (buffer pixels) the GPU takes; larger ones render on the CPU.
|
||||
pub(crate) const MEDIAN_MAX_R: usize = 16;
|
||||
/// Largest median radius (buffer pixels) taken by bisection over the window; larger radii
|
||||
/// slide a histogram along each row (`fxn_median_huang`), whose cost grows with the radius,
|
||||
/// not its square.
|
||||
pub(crate) const MEDIAN_BISECT_R: usize = 4;
|
||||
|
||||
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
|
|
@ -99,7 +103,7 @@ pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<G
|
|||
|
||||
/// A same-size per-pixel kernel.
|
||||
fn run(e: &mut Enc, entry: &str, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage {
|
||||
let out = e.image(src.width, src.height);
|
||||
let out = e.scratch(src.width, src.height);
|
||||
e.pixels(entry, p, src, aux, &out, data);
|
||||
out
|
||||
}
|
||||
|
|
@ -152,7 +156,15 @@ pub(crate) fn fractal(e: &mut Enc, ctx: &EffectCtx, b: GBuf, turbulent: bool) ->
|
|||
/// noise::median_image (of the straight colour with alpha 1 when `straight`).
|
||||
fn median_image(e: &mut Enc, img: &GpuImage, r: usize, straight: bool) -> GpuImage {
|
||||
let q = run(e, "fxn_quant", &with_u0([straight as u32, 0, 0, 0]), img, None, None);
|
||||
run(e, "fxn_median", &with_u0([r as u32, 0, 0, 0]), &q, None, None)
|
||||
if r <= MEDIAN_BISECT_R {
|
||||
return run(e, "fxn_median", &with_u0([r as u32, 0, 0, 0]), &q, None, None);
|
||||
}
|
||||
// Segments long enough that filling the first window (2r + 1)² costs about as much as
|
||||
// sliding along the rest.
|
||||
let seg = (4 * r as u32).clamp(32, 512).min(q.width);
|
||||
let out = e.scratch(q.width, q.height);
|
||||
e.dispatch("fxn_median_huang", &with_u0([r as u32, seg, 0, 0]), &q, None, &out, None, (q.height.div_ceil(64), q.width.div_ceil(seg)));
|
||||
out
|
||||
}
|
||||
|
||||
fn median(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
|
|
@ -160,9 +172,6 @@ fn median(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
if r == 0 {
|
||||
return Some(b);
|
||||
}
|
||||
if r > MEDIAN_MAX_R {
|
||||
return None;
|
||||
}
|
||||
let (mode, on_alpha, straight) = match id {
|
||||
"ec.noise.medianlegacy" => {
|
||||
let on = ctx.params.b("operateOnAlphaChannel");
|
||||
|
|
@ -251,7 +260,7 @@ fn gf(e: &mut Enc, mode: u32, s: &GpuImage, a: &GpuImage, d: Option<&GpuImage>,
|
|||
}
|
||||
|
||||
/// util::guided_filter on every channel at once (guide `g`, input `p`).
|
||||
fn guided_filter(e: &mut Enc, g: &GpuImage, p: &GpuImage, r: usize, eps: f32) -> GpuImage {
|
||||
pub(crate) fn guided_filter(e: &mut Enc, g: &GpuImage, p: &GpuImage, r: usize, eps: f32) -> GpuImage {
|
||||
let bx = |e: &mut Enc, img: &GpuImage| box_passes(e, img, &[r], &[r], true);
|
||||
let mean_g = bx(e, g);
|
||||
let mean_p = bx(e, p);
|
||||
|
|
@ -443,7 +452,7 @@ fn compound_blur(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
let buf = e.data(&data);
|
||||
let mut p = with_u0([1, invert as u32, 0, 0]);
|
||||
p.u[1][0] = w as u32;
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxn_cb_map", &p, &layer, None, &out, Some(&buf));
|
||||
out
|
||||
}
|
||||
|
|
@ -461,7 +470,7 @@ fn compound_blur(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
})
|
||||
.collect();
|
||||
let (rows, row) = e.image_rows(&map);
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
for k in 0..N - 1 {
|
||||
e.pixels("fxn_cb_level", &with_u0([k as u32, row, 0, 0]), &levels[k], Some(&levels[k + 1]), &out, Some(&rows));
|
||||
}
|
||||
|
|
@ -531,7 +540,7 @@ fn minimax(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
planes = morph(e, &planes, rx, ry, modes(max));
|
||||
}
|
||||
if !dont_shrink {
|
||||
let out = e.image(w, h);
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxn_crop", &with_u0([rx as u32, ry as u32, 0, 0]), &planes, None, &out, None);
|
||||
planes = out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<G
|
|||
|
||||
/// Run a same-size per-pixel kernel over the buffer.
|
||||
fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels(entry, p, &b.img, None, &out, data);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
|
|
@ -486,11 +486,7 @@ fn ripple_pulse(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
}
|
||||
|
||||
fn power_pin(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
// Perspective below 100 % (a bilinear inverse by Newton iterations with finite
|
||||
// differences) renders on the CPU.
|
||||
if ctx.params.f("perspective") / 100.0 < 1.0 {
|
||||
return None;
|
||||
}
|
||||
let persp = (ctx.params.f("perspective") / 100.0).clamp(0.0, 1.0);
|
||||
let (lw, lh) = (ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0));
|
||||
let ex = [ctx.params.f("expandTop"), ctx.params.f("expandLeft"), ctx.params.f("expandRight"), ctx.params.f("expandBottom")];
|
||||
let (x0, y0) = (-ex[1] / 100.0 * lw, -ex[0] / 100.0 * lh);
|
||||
|
|
@ -522,6 +518,13 @@ fn power_pin(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
p.f[4] = r[2];
|
||||
p.f[5] = [x0 as f32, y0 as f32, (x1 - x0) as f32, (y1 - y0) as f32];
|
||||
p.f[6] = [sc as f32, off[0] as f32, off[1] as f32, 0.0];
|
||||
if persp < 1.0 {
|
||||
let rel = |i: usize| [(q[i].x - q[0].x) as f32, (q[i].y - q[0].y) as f32];
|
||||
let (q1, q2, q3) = (rel(1), rel(2), rel(3));
|
||||
p.f[7] = [q[0].x as f32, q[0].y as f32, persp as f32, 1.0];
|
||||
p.f[8] = [q1[0], q1[1], q2[0], q2[1]];
|
||||
p.f[9] = [q3[0], q3[1], 0.0, 0.0];
|
||||
}
|
||||
},
|
||||
b,
|
||||
None,
|
||||
|
|
@ -642,7 +645,7 @@ fn motion_tile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
p.f[0] = [phase as f32, (lw * s) as f32, (lh * s) as f32, 0.0];
|
||||
p.f[1] = [b.offset[0] as f32, b.offset[1] as f32, 0.0, 0.0];
|
||||
let buf = e.data(&data);
|
||||
let out = e.image(nw, nh);
|
||||
let out = e.scratch(nw, nh);
|
||||
e.pixels("fxs_motiontile", &p, &b.img, None, &out, Some(&buf));
|
||||
Some(GBuf { img: out, offset: noff, scale: s })
|
||||
}
|
||||
|
|
@ -676,7 +679,7 @@ fn repetile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
p.u[0] = [nw, ctx.params.e("tiling"), 0, 0];
|
||||
p.f[0] = [x0 as f32, y0 as f32, w as f32, h as f32];
|
||||
let buf = e.data(&data);
|
||||
let out = e.image(nw, nh);
|
||||
let out = e.scratch(nw, nh);
|
||||
e.pixels("fxs_repetile", &p, &b.img, None, &out, Some(&buf));
|
||||
Some(GBuf { img: out, offset: [l, u], scale: s })
|
||||
}
|
||||
|
|
@ -814,7 +817,7 @@ fn roughen_edges(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
p.f[1] = [fm, evo, c, evo_w];
|
||||
p.f[2] = [oct + extra, infl, im, sharp];
|
||||
p.f[3] = [ec[0], ec[1], ec[2], sm];
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxs_roughen", &p, &b.img, Some(&d), &out, None);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
|
|
@ -855,7 +858,7 @@ fn texturize(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
let hgt = gaussian_blur(e, &luma, s, s, true);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [ang.cos() as f32, -ang.sin() as f32, k, 0.0];
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxs_texturize", &p, &b.img, Some(&hgt), &out, None);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
|
|
@ -937,7 +940,7 @@ pub(crate) fn glow(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
let glow_op = effectcraft_effects::glow_operation(ctx);
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = intensity;
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
if operation == 0 && glow_op != BlendMode::Add {
|
||||
p.u[0][0] = ops::mode_id(glow_op);
|
||||
e.pixels("fxs_glow_op", &p, &b.img, Some(&blurred), &out, None);
|
||||
|
|
@ -995,7 +998,7 @@ pub(crate) fn transform(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBu
|
|||
}
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = count as f32;
|
||||
let out = e.image(w, h);
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxs_div", &p, &acc, None, &out, None);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ fn shadow_highlight(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
(pr.f("blend") / 100.0).clamp(0.0, 1.0) as f32,
|
||||
0.0,
|
||||
];
|
||||
let out = e.image(w, h);
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("fxt_point", &p, &b.img, Some(&bases), &out, None);
|
||||
let Some((lo0, hi0)) = clip0 else { return Some(GBuf { img: out, ..b }) };
|
||||
let adjusted = e.download(&out)?;
|
||||
|
|
@ -158,7 +158,7 @@ fn op(code: u32) -> Params {
|
|||
|
||||
fn run(e: &mut Enc, p: &Params, b: GBuf, data: Option<Vec<f32>>) -> Option<GBuf> {
|
||||
let buf = data.map(|d| e.data(&d));
|
||||
let out = e.image(b.img.width, b.img.height);
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxt_point", p, &b.img, None, &out, buf.as_ref());
|
||||
Some(GBuf { img: out, ..b })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,275 @@
|
|||
//! GPU effects, warp family (kernels in `shaders/fx_warp.wgsl`, every entry point prefixed
|
||||
//! `fxw_`): Warp, Bezier Warp, CC Bend It, CC Page Turn, Smear, Reshape, Color Emboss and
|
||||
//! Cartoon, with the CPU effects' exact steps.
|
||||
//!
|
||||
//! Scalar set-up runs on the CPU in f64 exactly as the CPU effect does it (Bezier Warp's grid,
|
||||
//! Smear's moved outline, Reshape's outline correspondence, Bend It's arc geometry) and goes to
|
||||
//! the kernels as parameters or tables; the kernels do the per-pixel work in f32. Warp inverts
|
||||
//! its forward map per pixel with the CPU's Newton iterations, relative to the warp centre so
|
||||
//! f32 keeps sub-pixel precision; Bend It's arc distance is evaluated in a cancellation-free
|
||||
//! form so large radii (small bends) keep their precision too.
|
||||
|
||||
use effectcraft_effects::{Buf, EffectCtx, Image};
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::{GBuf, gaussian_blur};
|
||||
|
||||
/// Compute entry points in `fx_warp.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &[
|
||||
"fxw_warp",
|
||||
"fxw_bendit",
|
||||
"fxw_pageturn",
|
||||
"fxw_smear",
|
||||
"fxw_reshape",
|
||||
"fxw_coloremboss",
|
||||
"fxw_luma",
|
||||
"fxw_keep_alpha",
|
||||
"fxw_shock",
|
||||
"fxw_edges",
|
||||
"fxw_cartoon",
|
||||
];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.distort.warp",
|
||||
"ec.distort.bezierwarp",
|
||||
"ec.distort.ccbendit",
|
||||
"ec.distort.ccpageturn",
|
||||
"ec.distort.smear",
|
||||
"ec.distort.reshape",
|
||||
"ec.stylize.coloremboss",
|
||||
"ec.stylize.cartoon",
|
||||
];
|
||||
|
||||
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
match id {
|
||||
"ec.distort.warp" => warp(e, ctx, b),
|
||||
"ec.distort.bezierwarp" => bezier_warp(e, ctx, b),
|
||||
"ec.distort.ccbendit" => bend_it(e, ctx, b),
|
||||
"ec.distort.ccpageturn" => page_turn(e, ctx, b),
|
||||
"ec.distort.smear" => smear(e, ctx, b),
|
||||
"ec.distort.reshape" => reshape(e, ctx, b),
|
||||
"ec.stylize.coloremboss" => color_emboss(e, ctx, b),
|
||||
"ec.stylize.cartoon" => cartoon(e, ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// A same-size per-pixel kernel.
|
||||
fn run(e: &mut Enc, entry: &str, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage {
|
||||
let out = e.scratch(src.width, src.height);
|
||||
e.pixels(entry, p, src, aux, &out, data);
|
||||
out
|
||||
}
|
||||
|
||||
fn f4(v: [f64; 4]) -> [f32; 4] {
|
||||
v.map(|x| x as f32)
|
||||
}
|
||||
|
||||
/// The buffer's geometry without its pixels (the CPU set-up functions read only that).
|
||||
fn geometry(b: &GBuf) -> Buf {
|
||||
Buf { img: Image::new(0, 0), offset: b.offset, scale: b.scale }
|
||||
}
|
||||
|
||||
/// Points as f32 pairs.
|
||||
fn points(v: &mut Vec<f32>, pts: &[[f64; 2]]) {
|
||||
v.extend(pts.iter().flat_map(|q| [q[0] as f32, q[1] as f32]));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Warp (distort3::warp)
|
||||
|
||||
fn warp(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let k = ctx.params.f("bend") / 100.0;
|
||||
let hd = ctx.params.f("horizontalDistortion") / 100.0;
|
||||
let vd = ctx.params.f("verticalDistortion") / 100.0;
|
||||
if k == 0.0 && hd == 0.0 && vd == 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let style = ctx.params.e("warpStyle");
|
||||
let vertical = ctx.params.e("warpAxis") == 1;
|
||||
let (hx, hy) = (ctx.layer_size[0] * b.scale * 0.5, ctx.layer_size[1] * b.scale * 0.5);
|
||||
if !ctx.adjustment {
|
||||
let pad = ((k.abs() + hd.abs() + vd.abs()) * 0.6 * hx.max(hy)).ceil().min(4096.0);
|
||||
b.pad(e, pad as u32 + 1)?;
|
||||
}
|
||||
let c = b.to_px([ctx.layer_size[0] * 0.5, ctx.layer_size[1] * 0.5]);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [0, style.min(14), vertical as u32, 0];
|
||||
p.f[0] = f4([c.0, c.1, hx.max(1e-6), hy.max(1e-6)]);
|
||||
p.f[1] = f4([k, hd, vd, 0.0]);
|
||||
b.img = run(e, "fxw_warp", &p, &b.img, None, None);
|
||||
Some(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Bezier Warp (distort2::bezier_warp)
|
||||
|
||||
fn bezier_warp(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let (n, dest, srcp) = effectcraft_effects::bezier_grid(ctx, b.offset, b.scale);
|
||||
crate::fx_distort::grid_warp(e, b, n, n, &dest, &srcp)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Bend It (distort2::bend_it)
|
||||
|
||||
fn bend_it(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let mut beta = ctx.params.f("bend").to_radians();
|
||||
if beta.abs() < 1e-6 {
|
||||
return Some(b);
|
||||
}
|
||||
let s = b.to_px(ctx.params.v2("start"));
|
||||
let end = b.to_px(ctx.params.v2("end"));
|
||||
let static_pre = ctx.params.e("renderPrestart") == 1;
|
||||
let l = ((end.0 - s.0).powi(2) + (end.1 - s.1).powi(2)).sqrt();
|
||||
if l < 1e-6 {
|
||||
return Some(b);
|
||||
}
|
||||
let a = ((end.0 - s.0) / l, (end.1 - s.1) / l);
|
||||
let mut n = (-a.1, a.0);
|
||||
if beta < 0.0 {
|
||||
beta = -beta;
|
||||
n = (-n.0, -n.1);
|
||||
}
|
||||
let rho = l / beta;
|
||||
let pe = (rho * beta.sin(), rho - rho * beta.cos());
|
||||
let mut p = Params::default();
|
||||
p.u[0][1] = static_pre as u32;
|
||||
p.f[0] = f4([s.0, s.1, a.0, a.1]);
|
||||
p.f[1] = f4([n.0, n.1, rho, beta]);
|
||||
p.f[2] = f4([pe.0, pe.1, l, 0.0]);
|
||||
p.f[3] = f4([beta.cos(), beta.sin(), -beta.sin(), beta.cos()]);
|
||||
b.img = run(e, "fxw_bendit", &p, &b.img, None, None);
|
||||
Some(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Page Turn (distort2::page_turn)
|
||||
|
||||
fn page_turn(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let f = b.to_px(ctx.params.v2("foldPosition"));
|
||||
let phi = ctx.params.f("foldDirection").to_radians();
|
||||
let r = (ctx.params.f("foldRadius") * b.scale).max(0.5);
|
||||
let lam = ctx.params.f("lightDirection").to_radians();
|
||||
let render = ctx.params.e("render");
|
||||
let bo = ctx.params.f("backPageOpacity") / 100.0;
|
||||
let paper = ctx.params.color("paperColor");
|
||||
let n = (-phi.sin(), phi.cos());
|
||||
let ldot = n.0 * lam.sin() - n.1 * lam.cos();
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [0, (render != 1) as u32, (render != 2) as u32, 0];
|
||||
p.f[0] = f4([f.0, f.1, n.0, n.1]);
|
||||
p.f[1] = [r as f32, ldot as f32, bo as f32, 0.0];
|
||||
p.f[2] = [paper[0], paper[1], paper[2], 0.0];
|
||||
b.img = run(e, "fxw_pageturn", &p, &b.img, None, None);
|
||||
Some(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Smear (distort3::smear)
|
||||
|
||||
fn smear(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let Some(s) = effectcraft_effects::smear_setup(ctx, &geometry(&b)) else { return Some(b) };
|
||||
let mut data = Vec::with_capacity(2 * (s.bound.len() + s.moved.len()));
|
||||
points(&mut data, &s.bound);
|
||||
points(&mut data, &s.moved);
|
||||
let buf = e.data(&data);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [0, s.bound.len() as u32, s.moved.len() as u32, 0];
|
||||
p.f[0] = f4([s.c[0], s.c[1], s.cs, s.sn]);
|
||||
p.f[1] = f4([s.scl, s.off[0], s.off[1], s.pct]);
|
||||
p.f[2][0] = (1.0 / s.kexp) as f32;
|
||||
b.img = run(e, "fxw_smear", &p, &b.img, None, Some(&buf));
|
||||
Some(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Reshape (distort3::reshape)
|
||||
|
||||
fn reshape(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let Some(s) = effectcraft_effects::reshape_setup(ctx, &geometry(&b)) else { return Some(b) };
|
||||
debug_assert_eq!(s.dest.len(), effectcraft_effects::RESHAPE_POINTS);
|
||||
let mut data = Vec::new();
|
||||
points(&mut data, &s.dest);
|
||||
points(&mut data, &s.disp);
|
||||
let nb = s.bound.as_ref().map_or(0, Vec::len);
|
||||
if let Some(bd) = &s.bound {
|
||||
points(&mut data, bd);
|
||||
}
|
||||
let buf = e.data(&data);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [0, nb as u32, s.smooth as u32, 0];
|
||||
p.f[0] = [(s.kexp * 0.5) as f32, s.pct as f32, 0.0, 0.0];
|
||||
b.img = run(e, "fxw_reshape", &p, &b.img, None, Some(&buf));
|
||||
Some(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Color Emboss (stylize2::color_emboss)
|
||||
|
||||
fn color_emboss(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let ang = ctx.params.f("direction").to_radians();
|
||||
let relief = ctx.params.f("relief") * b.scale;
|
||||
let mut p = Params::default();
|
||||
p.f[0] = [(ang.cos() * relief) as f32, (-ang.sin() * relief) as f32, ctx.params.f("contrast") as f32 / 100.0, ctx.params.f("blend") as f32 / 100.0];
|
||||
b.img = run(e, "fxw_coloremboss", &p, &b.img, None, None);
|
||||
Some(b)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Cartoon (stylize2::cartoon)
|
||||
|
||||
fn cartoon(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let render = pr.e("render");
|
||||
let r = (pr.f("detailRadius") * b.scale).round().max(0.0) as usize;
|
||||
let thr = pr.f("detailThreshold") as f32 / 100.0;
|
||||
let steps = pr.f("fill/shadingSteps").max(1.0) as f32;
|
||||
let smooth = pr.f("fill/shadingSmoothness") as f32 / 100.0;
|
||||
let et = pr.f("edge/edgeThreshold") as f32;
|
||||
let ew = pr.f("edge/edgeWidth") * b.scale;
|
||||
let es = pr.f("edge/edgeSoftness") as f32 / 100.0;
|
||||
let eo = pr.f("edge/edgeOpacity") as f32 / 100.0;
|
||||
let black = (pr.f("advanced/edgeBlackLevel") as f32 / 100.0).clamp(0.0, 1.0);
|
||||
let contrast = ((pr.f("advanced/edgeContrast").clamp(0.0, 0.999) * std::f64::consts::FRAC_PI_2).tan() as f32).min(100.0);
|
||||
let none = Params::default();
|
||||
// Edge-preserving smoothing: guided filter on each premultiplied colour channel, guided by
|
||||
// the luma; alpha stays.
|
||||
let mut smoothed = if r > 0 {
|
||||
let guide = run(e, "fxw_luma", &none, &b.img, None, None);
|
||||
let eps = thr * thr * 0.25 + 1e-5;
|
||||
let f = crate::fx_noise::guided_filter(e, &guide, &b.img, r, eps);
|
||||
run(e, "fxw_keep_alpha", &none, &b.img, Some(&f), None)
|
||||
} else {
|
||||
b.img.clone()
|
||||
};
|
||||
let enh = pr.f("advanced/edgeEnhancement") / 100.0;
|
||||
if enh < 0.0 {
|
||||
let s = 1.5 * b.scale;
|
||||
let blur = gaussian_blur(e, &smoothed, s, s, true);
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = (-enh) as f32;
|
||||
smoothed = run(e, "fxc_lerp", &p, &smoothed, Some(&blur), None);
|
||||
} else if enh > 0.0 {
|
||||
let l = run(e, "fxw_luma", &none, &smoothed, None, None);
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = (enh * 2.0 * b.scale) as f32;
|
||||
smoothed = run(e, "fxw_shock", &p, &smoothed, Some(&l), None);
|
||||
}
|
||||
let mut p = Params::default();
|
||||
let mut edge_k = eo;
|
||||
let rows = if render >= 1 {
|
||||
let luma = run(e, "fxw_luma", &none, &smoothed, None, None);
|
||||
let t1 = et * 0.2;
|
||||
let t0 = t1 * (1.0 - es * 0.9);
|
||||
let mut ep = Params::default();
|
||||
ep.f[0] = [t0, t1.max(t0 + 1e-4), 0.0, 0.0];
|
||||
let mut edges = run(e, "fxw_edges", &ep, &luma, None, None);
|
||||
if ew > 1.0 {
|
||||
edges = crate::fx_color::morph_frac(e, &edges, (ew - 1.0) * 0.5, crate::fx_color::DILATE_X);
|
||||
} else {
|
||||
edge_k *= ew.max(0.0) as f32;
|
||||
}
|
||||
Some(e.image_rows(&edges))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
p.u[0] = [0, render, rows.as_ref().map_or(0, |r| r.1), rows.is_some() as u32];
|
||||
p.f[0] = [steps, smooth, edge_k, black];
|
||||
p.f[1][0] = contrast;
|
||||
b.img = run(e, "fxw_cartoon", &p, &b.img, Some(&smoothed), rows.as_ref().map(|r| &r.0));
|
||||
Some(b)
|
||||
}
|
||||
|
|
@ -20,9 +20,18 @@
|
|||
//! 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. Each family lives in its own module with its own WGSL file
|
||||
//! (`fx_color`, `fx_distort`, `fx_generate`, `fx_key`, `fx_noise`, `fx_stylize`, `fx_tone`);
|
||||
//! settings a kernel cannot match fall back to the CPU (`catalog::gpu_supported`, or `None`
|
||||
//! from the family's `apply`).
|
||||
//! (`fx_color`, `fx_distort`, `fx_extra`, `fx_generate`, `fx_key`, `fx_noise`, `fx_stylize`,
|
||||
//! `fx_tone`, `fx_warp`); settings a kernel cannot match fall back to the CPU
|
||||
//! (`catalog::gpu_supported`, or `None` from the family's `apply`).
|
||||
//!
|
||||
//! Working textures come from a pool (`context::Pool`): a frame allocates several full-frame
|
||||
//! RGBA f32 images per layer, and creating and zeroing those cost more than compositing a small
|
||||
//! layer. A released texture is reused once every encoder that could still read it has been
|
||||
//! submitted. 8/16 bpc quantisation after each layer is fused into that layer's composite.
|
||||
//! [`Backend::Auto`](effectcraft_render::Backend) renders each comp on whichever compositor
|
||||
//! measured faster for it ([`effectcraft_render::AutoPick`], kept by [`Gpu`]): a light comp
|
||||
//! that the CPU composites in a millisecond stays there rather than paying for a full-frame
|
||||
//! readback.
|
||||
//!
|
||||
//! GPU particles (`particles`): the stepped particle effects hand their simulation to
|
||||
//! [`effectcraft_effects::psim::ParticleSim`], implemented here with one invocation per particle
|
||||
|
|
@ -40,18 +49,20 @@ mod context;
|
|||
mod effects;
|
||||
mod fx_color;
|
||||
mod fx_distort;
|
||||
mod fx_extra;
|
||||
mod fx_generate;
|
||||
mod fx_key;
|
||||
mod fx_noise;
|
||||
mod fx_stylize;
|
||||
mod fx_tone;
|
||||
mod fx_warp;
|
||||
mod ops;
|
||||
mod particles;
|
||||
mod walk;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use context::{GpuContext, GpuImage};
|
||||
pub use context::{GpuContext, GpuImage, TransferStats};
|
||||
use effectcraft_effects::Buf;
|
||||
use effectcraft_project::ItemId;
|
||||
use effectcraft_raster::Image;
|
||||
|
|
@ -65,6 +76,8 @@ use crate::context::Enc;
|
|||
#[derive(Clone)]
|
||||
pub struct Gpu {
|
||||
ctx: Arc<GpuContext>,
|
||||
/// Backend::Auto's per-comp CPU / GPU timings.
|
||||
auto: Arc<effectcraft_render::AutoPick>,
|
||||
}
|
||||
|
||||
/// A viewer frame left on the GPU: premultiplied RGBA8 (`wgpu::TextureFormat::Rgba8Unorm`),
|
||||
|
|
@ -89,7 +102,7 @@ impl Gpu {
|
|||
}
|
||||
|
||||
pub fn from_context(ctx: GpuContext) -> Gpu {
|
||||
Gpu { ctx: Arc::new(ctx) }
|
||||
Gpu { ctx: Arc::new(ctx), auto: Default::default() }
|
||||
}
|
||||
|
||||
pub fn context(&self) -> &GpuContext {
|
||||
|
|
@ -165,6 +178,10 @@ impl Accelerator for Gpu {
|
|||
fn particles(&self) -> Option<&dyn effectcraft_effects::psim::ParticleSim> {
|
||||
self.ctx.can_readback().then_some(self as &dyn effectcraft_effects::psim::ParticleSim)
|
||||
}
|
||||
|
||||
fn auto_pick(&self) -> Option<&effectcraft_render::AutoPick> {
|
||||
Some(&self.auto)
|
||||
}
|
||||
}
|
||||
|
||||
impl effectcraft_effects::psim::ParticleSim for Gpu {
|
||||
|
|
@ -186,6 +203,8 @@ mod tests_fx_color;
|
|||
#[cfg(test)]
|
||||
mod tests_fx_distort;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_extra;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_generate;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_key;
|
||||
|
|
@ -196,4 +215,6 @@ mod tests_fx_stylize;
|
|||
#[cfg(test)]
|
||||
mod tests_fx_tone;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_warp;
|
||||
#[cfg(test)]
|
||||
mod tests_particles;
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ fn rows(m: &Mat3) -> [[f32; 4]; 3] {
|
|||
|
||||
/// Box-filter halving (minification pre-filter).
|
||||
pub fn half(e: &mut Enc, src: &GpuImage) -> GpuImage {
|
||||
let out = e.image(src.width.div_ceil(2).max(1), src.height.div_ceil(2).max(1));
|
||||
let out = e.scratch(src.width.div_ceil(2).max(1), src.height.div_ceil(2).max(1));
|
||||
e.pixels("half", &Params::default(), src, None, &out, None);
|
||||
out
|
||||
}
|
||||
|
|
@ -44,6 +44,24 @@ pub fn warp(
|
|||
opacity: f32,
|
||||
seed: u32,
|
||||
accumulate: Option<f32>,
|
||||
) -> GpuImage {
|
||||
warp_q(e, dst, src, m, sampling, mode, opacity, seed, accumulate, None)
|
||||
}
|
||||
|
||||
/// [`warp`], clamping and quantising the result to `levels` (8/16 bpc: the comp after a layer,
|
||||
/// in the same pass).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn warp_q(
|
||||
e: &mut Enc,
|
||||
dst: &GpuImage,
|
||||
src: &GpuImage,
|
||||
m: &Mat3,
|
||||
sampling: Sampling,
|
||||
mode: BlendMode,
|
||||
opacity: f32,
|
||||
seed: u32,
|
||||
accumulate: Option<f32>,
|
||||
levels: Option<f32>,
|
||||
) -> GpuImage {
|
||||
if src.width == 0 || src.height == 0 || opacity <= 0.0 {
|
||||
return dst.clone();
|
||||
|
|
@ -79,20 +97,23 @@ pub fn warp(
|
|||
p.f[0] = r[0];
|
||||
p.f[1] = r[1];
|
||||
p.f[2] = r[2];
|
||||
p.f[3] = [opacity, accumulate.unwrap_or(1.0), 0.0, 0.0];
|
||||
p.f[3] = [opacity, accumulate.unwrap_or(1.0), levels.unwrap_or(0.0), 0.0];
|
||||
p.f[4] = [x0 as f32, y0 as f32, x1 as f32, y1 as f32];
|
||||
let out = e.image(dst.width, dst.height);
|
||||
let mut out = e.scratch(dst.width, dst.height);
|
||||
e.pixels("warp_blend", &p, &img, Some(dst), &out, None);
|
||||
out.levels = levels;
|
||||
out
|
||||
}
|
||||
|
||||
/// `Image::blend_from`: blend `src` (same size) onto `dst`.
|
||||
pub fn blend_full(e: &mut Enc, dst: &GpuImage, src: &GpuImage, mode: BlendMode, opacity: f32, seed: u32) -> GpuImage {
|
||||
/// `Image::blend_from`: blend `src` (same size) onto `dst`, clamping and quantising the result
|
||||
/// to `levels` when set (see [`warp_q`]).
|
||||
pub fn blend_full(e: &mut Enc, dst: &GpuImage, src: &GpuImage, mode: BlendMode, opacity: f32, seed: u32, levels: Option<f32>) -> GpuImage {
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [mode_id(mode), 0, seed, 0];
|
||||
p.f[0] = [opacity, 0.0, 0.0, 0.0];
|
||||
let out = e.image(dst.width, dst.height);
|
||||
p.f[0] = [opacity, levels.unwrap_or(0.0), 0.0, 0.0];
|
||||
let mut out = e.scratch(dst.width, dst.height);
|
||||
e.pixels("blend_full", &p, src, Some(dst), &out, None);
|
||||
out.levels = levels;
|
||||
out
|
||||
}
|
||||
|
||||
|
|
@ -105,21 +126,21 @@ pub fn matte(e: &mut Enc, iso: &GpuImage, matte: &GpuImage, kind: MatteKind) ->
|
|||
MatteKind::Luma => 2,
|
||||
MatteKind::LumaInverted => 3,
|
||||
};
|
||||
let out = e.image(iso.width, iso.height);
|
||||
let out = e.scratch(iso.width, iso.height);
|
||||
e.pixels("matte", &p, iso, Some(matte), &out, None);
|
||||
out
|
||||
}
|
||||
|
||||
/// Preserve Transparency: multiply by the alpha below.
|
||||
pub fn preserve(e: &mut Enc, iso: &GpuImage, canvas: &GpuImage) -> GpuImage {
|
||||
let out = e.image(iso.width, iso.height);
|
||||
let out = e.scratch(iso.width, iso.height);
|
||||
e.pixels("preserve", &Params::default(), iso, Some(canvas), &out, None);
|
||||
out
|
||||
}
|
||||
|
||||
/// Knockout: clear what lies below the layer's content.
|
||||
pub fn knockout(e: &mut Enc, canvas: &GpuImage, shape: &GpuImage) -> GpuImage {
|
||||
let out = e.image(canvas.width, canvas.height);
|
||||
let out = e.scratch(canvas.width, canvas.height);
|
||||
e.pixels("knockout", &Params::default(), canvas, Some(shape), &out, None);
|
||||
out
|
||||
}
|
||||
|
|
@ -129,7 +150,7 @@ pub fn channel_mix(e: &mut Enc, canvas: &GpuImage, tmp: &GpuImage, channels: [bo
|
|||
let mut p = Params::default();
|
||||
p.u[0] = [channels[0] as u32, channels[1] as u32, channels[2] as u32, 0];
|
||||
p.f[0][0] = opacity;
|
||||
let out = e.image(canvas.width, canvas.height);
|
||||
let out = e.scratch(canvas.width, canvas.height);
|
||||
e.pixels("channel_mix", &p, canvas, Some(tmp), &out, None);
|
||||
out
|
||||
}
|
||||
|
|
@ -140,17 +161,22 @@ pub fn adjust_mix(e: &mut Enc, canvas: &GpuImage, adjusted: &GpuImage, matte: &G
|
|||
let mut p = Params::default();
|
||||
p.u[0][0] = stride;
|
||||
p.f[0][0] = opacity;
|
||||
let out = e.image(canvas.width, canvas.height);
|
||||
let out = e.scratch(canvas.width, canvas.height);
|
||||
e.pixels("adjust_mix", &p, canvas, Some(adjusted), &out, Some(&rows));
|
||||
out
|
||||
}
|
||||
|
||||
/// Clamp and quantise to `levels` steps (8/16 bpc).
|
||||
pub fn quantize(e: &mut Enc, img: &GpuImage, levels: f32) -> GpuImage {
|
||||
// Quantising is idempotent: an image a fused kernel already quantised stays as it is.
|
||||
if img.levels == Some(levels) {
|
||||
return img.clone();
|
||||
}
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = levels;
|
||||
let out = e.image(img.width, img.height);
|
||||
let mut out = e.scratch(img.width, img.height);
|
||||
e.pixels("quantize", &p, img, None, &out, None);
|
||||
out.levels = Some(levels);
|
||||
out
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +200,7 @@ pub fn convert(e: &mut Enc, img: &GpuImage, c: &Conversion) -> GpuImage {
|
|||
p.f[i] = [m[i][0], m[i][1], m[i][2], 0.0];
|
||||
}
|
||||
}
|
||||
let out = e.image(img.width, img.height);
|
||||
let out = e.scratch(img.width, img.height);
|
||||
e.pixels("convert", &p, img, None, &out, None);
|
||||
out
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
// GPU effects (shapes and bevels): see src/fx_extra.rs. Circle, Ellipse, Iris Wipe, Bevel Alpha
|
||||
// and Bevel Edges, each mirroring the CPU effect operation for operation. Coordinates are pixel
|
||||
// centres relative to the shape's centre (or the layer's corner), so f32 keeps its precision.
|
||||
|
||||
fn fxe_inside(p: vec2<i32>) -> bool {
|
||||
let d = out_dims();
|
||||
return p.x < d.x && p.y < d.y;
|
||||
}
|
||||
|
||||
fn fxe_rel(p: vec2<i32>, c: vec2<f32>) -> vec2<f32> {
|
||||
return vec2<f32>(f32(p.x) + 0.5, f32(p.y) + 0.5) - c;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Circle (generate::circle)
|
||||
// u[0] = (gen mode, has inner, invert); f[0] = (cx, cy, outer radius, inner radius);
|
||||
// f[1] = (outer feather, inner feather, opacity); f[2] = colour.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxe_circle(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxe_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let d = length(fxe_rel(p, P.f[0].xy));
|
||||
var cov = clamp((P.f[0].z - d) / P.f[1].x + 0.5, 0.0, 1.0);
|
||||
if (P.u[0].y != 0u) {
|
||||
cov *= clamp((d - P.f[0].w) / P.f[1].y + 0.5, 0.0, 1.0);
|
||||
}
|
||||
if (P.u[0].z != 0u) {
|
||||
cov = 1.0 - cov;
|
||||
}
|
||||
let a = cov * P.f[1].z;
|
||||
textureStore(out, p, gen_blend(textureLoad(src, p, 0), vec4<f32>(P.f[2].xyz * a, a), P.u[0].x));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Ellipse (generate2::ellipse)
|
||||
// u[0].y = composite on original; f[0] = (cx, cy, rx, ry); f[1] = (half thickness, softness);
|
||||
// f[2] = inside colour; f[3] = outside colour.
|
||||
|
||||
fn fxe_smoothstep(e0: f32, e1: f32, x: f32) -> f32 {
|
||||
if (abs(e1 - e0) < 1e-9) {
|
||||
return select(1.0, 0.0, x < e0);
|
||||
}
|
||||
let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0);
|
||||
return t * t * (3.0 - 2.0 * t);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxe_ellipse(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxe_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let d = fxe_rel(p, P.f[0].xy);
|
||||
let r = P.f[0].zw;
|
||||
let q = length(d / r);
|
||||
var dist = min(r.x, r.y);
|
||||
if (q >= 1e-9) {
|
||||
let g = d / (r * r) / q;
|
||||
dist = abs((q - 1.0) / max(length(g), 1e-9));
|
||||
}
|
||||
let half = P.f[1].x;
|
||||
let soft = P.f[1].y;
|
||||
let t = dist / half;
|
||||
var a = clamp(half - dist + 0.5, 0.0, 1.0);
|
||||
if (soft > 1e-3) {
|
||||
a = 1.0 - fxe_smoothstep(1.0 - soft, 1.0, t);
|
||||
}
|
||||
let k = fxe_smoothstep(0.0, 1.0, t);
|
||||
let cc = P.f[2].xyz + (P.f[3].xyz - P.f[2].xyz) * k;
|
||||
let g4 = vec4<f32>(cc * a, a);
|
||||
if (P.u[0].y != 0u) {
|
||||
let o = textureLoad(src, p, 0);
|
||||
textureStore(out, p, g4 + o * (1.0 - g4.w));
|
||||
} else {
|
||||
textureStore(out, p, g4);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Iris Wipe (transition::iris_wipe)
|
||||
// data = polygon vertices relative to the centre; u[0] = (_, vertex count);
|
||||
// f[0] = (cx, cy, rotation, angle step); f[1] = (feather, radius at the centre, outer radius).
|
||||
|
||||
fn fxe_cross(a: vec2<f32>, b: vec2<f32>) -> f32 {
|
||||
return a.x * b.y - a.y * b.x;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxe_iris(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxe_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let d = fxe_rel(p, P.f[0].xy);
|
||||
let r = length(d);
|
||||
let m = P.u[0].y;
|
||||
let rot = P.f[0].z;
|
||||
let step = P.f[0].w;
|
||||
var rb = P.f[1].y;
|
||||
if (r >= 1e-9) {
|
||||
let tau = 6.2831855;
|
||||
let a0 = atan2(d.x, -d.y) - rot;
|
||||
let a = a0 - floor(a0 / tau) * tau;
|
||||
let i = min(u32(floor(a / step)), m - 1u);
|
||||
let vi = vec2<f32>(data[2u * i], data[2u * i + 1u]);
|
||||
let j = (i + 1u) % m;
|
||||
let vj = vec2<f32>(data[2u * j], data[2u * j + 1u]);
|
||||
let e = vj - vi;
|
||||
let den = fxe_cross(d / r, e);
|
||||
rb = select(fxe_cross(vi, e) / den, P.f[1].z, abs(den) < 1e-12);
|
||||
}
|
||||
let feather = P.f[1].x;
|
||||
var k = select(0.0, 1.0, r >= rb);
|
||||
if (feather > 0.0) {
|
||||
k = clamp((r - rb) / feather + 0.5, 0.0, 1.0);
|
||||
}
|
||||
textureStore(out, p, textureLoad(src, p, 0) * k);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Bevels (perspective.rs)
|
||||
|
||||
// perspective::apply_shade.
|
||||
fn fxe_shade(px: vec4<f32>, s: f32, light: vec3<f32>) -> vec4<f32> {
|
||||
if (s > 0.0) {
|
||||
return vec4<f32>(px.xyz + light * s * px.w, px.w);
|
||||
}
|
||||
if (s < 0.0) {
|
||||
return vec4<f32>(px.xyz * max(1.0 + s, 0.0), px.w);
|
||||
}
|
||||
return px;
|
||||
}
|
||||
|
||||
// Plane::alpha (in x).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxe_alpha(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxe_inside(p)) {
|
||||
return;
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(textureLoad(src, p, 0).w, 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// Bevel Alpha: src = the layer, aux = its blurred alpha (x). f[0] = (light direction, normal
|
||||
// scale k); f[1] = (light colour, intensity).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxe_bevel_alpha(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxe_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
if (px.w <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let dx = (tex_get_clamped(aux, p.x + 1, p.y).x - tex_get_clamped(aux, p.x - 1, p.y).x) * 0.5;
|
||||
let dy = (tex_get_clamped(aux, p.x, p.y + 1).x - tex_get_clamped(aux, p.x, p.y - 1).x) * 0.5;
|
||||
let k = P.f[0].w;
|
||||
let n = normalize(vec3<f32>(-dx * k, -dy * k, 1.0));
|
||||
let l = P.f[0].xyz;
|
||||
let s = (dot(n, l) - l.z) * P.f[1].w * 2.0;
|
||||
textureStore(out, p, fxe_shade(px, s, P.f[1].xyz));
|
||||
}
|
||||
|
||||
// Bevel Edges: f[0] = (light direction, thickness); f[1] = (light colour, intensity);
|
||||
// f[2] = layer rectangle (x0, y0, w, h).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxe_bevel_edges(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxe_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let rc = P.f[2];
|
||||
let q = vec2<f32>(f32(p.x) + 0.5, f32(p.y) + 0.5);
|
||||
let d = vec4<f32>(q.x - rc.x, rc.x + rc.z - q.x, q.y - rc.y, rc.y + rc.w - q.y);
|
||||
if (d.x < 0.0 || d.y < 0.0 || d.z < 0.0 || d.w < 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
var mi = 0;
|
||||
var mv = d.x;
|
||||
for (var i = 1; i < 4; i++) {
|
||||
if (d[i] < mv) {
|
||||
mv = d[i];
|
||||
mi = i;
|
||||
}
|
||||
}
|
||||
if (mv >= P.f[0].w) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
var n2 = vec2<f32>(-1.0, 0.0);
|
||||
if (mi == 1) {
|
||||
n2 = vec2<f32>(1.0, 0.0);
|
||||
} else if (mi == 2) {
|
||||
n2 = vec2<f32>(0.0, -1.0);
|
||||
} else if (mi == 3) {
|
||||
n2 = vec2<f32>(0.0, 1.0);
|
||||
}
|
||||
let tilt = 0.70710677;
|
||||
let n = vec3<f32>(n2 * tilt, tilt);
|
||||
let l = P.f[0].xyz;
|
||||
let s = (dot(n, l) - l.z) * P.f[1].w * 2.0;
|
||||
textureStore(out, p, fxe_shade(px, s, P.f[1].xyz));
|
||||
}
|
||||
|
|
@ -76,6 +76,68 @@ fn fxn_median(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
textureStore(out, p, fxn_div4(lo, vec4<f32>(511.0)));
|
||||
}
|
||||
|
||||
// The same median for large radii, as the CPU computes it: a sliding 512-bin histogram per
|
||||
// channel (Huang). One invocation per row segment of u[0].y pixels: it fills the histogram for
|
||||
// the segment's first window, then slides right one column at a time (remove the column
|
||||
// leaving, add the one entering) and moves each channel's median by the counts below it.
|
||||
// u[0] = (r, segment length); dispatched over (rows, segments) with 64 × 1 workgroups.
|
||||
@compute @workgroup_size(64, 1)
|
||||
fn fxn_median_huang(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let y = i32(gid.x);
|
||||
let seg = i32(P.u[0].y);
|
||||
let x0 = i32(gid.y) * seg;
|
||||
if (y >= dims.y || x0 >= dims.x) {
|
||||
return;
|
||||
}
|
||||
let x1 = min(x0 + seg, dims.x);
|
||||
let r = i32(P.u[0].x);
|
||||
let half = u32((2 * r + 1) * (2 * r + 1) / 2);
|
||||
var hist: array<u32, 2048>;
|
||||
for (var dy = -r; dy <= r; dy++) {
|
||||
for (var dx = -r; dx <= r; dx++) {
|
||||
let v = vec4<u32>(tex_get_clamped(src, x0 + dx, y + dy));
|
||||
hist[v.x] += 1u;
|
||||
hist[512u + v.y] += 1u;
|
||||
hist[1024u + v.z] += 1u;
|
||||
hist[1536u + v.w] += 1u;
|
||||
}
|
||||
}
|
||||
// Per channel: the median level and the count of values below it.
|
||||
var med = vec4<u32>(0u);
|
||||
var lt = vec4<u32>(0u);
|
||||
for (var x = x0; x < x1; x++) {
|
||||
if (x > x0) {
|
||||
for (var dy = -r; dy <= r; dy++) {
|
||||
let o = vec4<u32>(tex_get_clamped(src, x - 1 - r, y + dy));
|
||||
let n = vec4<u32>(tex_get_clamped(src, x + r, y + dy));
|
||||
for (var c = 0u; c < 4u; c++) {
|
||||
hist[c * 512u + o[c]] -= 1u;
|
||||
if (o[c] < med[c]) {
|
||||
lt[c] -= 1u;
|
||||
}
|
||||
hist[c * 512u + n[c]] += 1u;
|
||||
if (n[c] < med[c]) {
|
||||
lt[c] += 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var c = 0u; c < 4u; c++) {
|
||||
let base = c * 512u;
|
||||
while (lt[c] > half) {
|
||||
med[c] -= 1u;
|
||||
lt[c] -= hist[base + med[c]];
|
||||
}
|
||||
while (lt[c] + hist[base + med[c]] <= half) {
|
||||
lt[c] += hist[base + med[c]];
|
||||
med[c] += 1u;
|
||||
}
|
||||
}
|
||||
textureStore(out, vec2<i32>(x, y), fxn_div4(vec4<f32>(med), vec4<f32>(511.0)));
|
||||
}
|
||||
}
|
||||
|
||||
// noise::median_px.
|
||||
fn fxn_median_px(o: vec4<f32>, m: vec4<f32>, on_alpha: bool) -> vec4<f32> {
|
||||
if (on_alpha) {
|
||||
|
|
|
|||
|
|
@ -423,6 +423,36 @@ fn fxs_warp(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
u = u / w;
|
||||
v = v / w;
|
||||
}
|
||||
// Perspective below 100 %: blend toward the bilinear (non-perspective) inverse, found
|
||||
// by Newton iterations from the projective guess as on the CPU; positions relative to
|
||||
// the first corner keep f32's precision. f[7] = (corner 0, perspective, on);
|
||||
// f[8] = (corner 1, corner 2) and f[9].xy = corner 3, relative to corner 0.
|
||||
if (P.f[7].w != 0.0) {
|
||||
let q1 = P.f[8].xy;
|
||||
let q2 = P.f[8].zw;
|
||||
let q3 = P.f[9].xy;
|
||||
let k = q2 - q3 - q1;
|
||||
let t = vec2<f32>(x, y) - P.f[7].xy;
|
||||
var bu = u;
|
||||
var bv = v;
|
||||
for (var i = 0; i < 8; i++) {
|
||||
let f = q1 * bu + q3 * bv + k * (bu * bv) - t;
|
||||
if (abs(f.x) + abs(f.y) < 1e-4) {
|
||||
break;
|
||||
}
|
||||
let ju = q1 + k * bv;
|
||||
let jv = q3 + k * bu;
|
||||
let det = ju.x * jv.y - jv.x * ju.y;
|
||||
if (abs(det) < 1e-12) {
|
||||
break;
|
||||
}
|
||||
bu -= (jv.y * f.x - jv.x * f.y) / det;
|
||||
bv -= (-ju.y * f.x + ju.x * f.y) / det;
|
||||
}
|
||||
let persp = P.f[7].z;
|
||||
u = bu + (u - bu) * persp;
|
||||
v = bv + (v - bv) * persp;
|
||||
}
|
||||
if (u < -1e-9 || u > 1.0 + 1e-9 || v < -1e-9 || v > 1.0 + 1e-9) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,532 @@
|
|||
// GPU effects (warp family): see src/fx_warp.rs. Warp, CC Bend It, CC Page Turn, Smear, Reshape,
|
||||
// Color Emboss and Cartoon, each mirroring the CPU effect in effectcraft-effects operation for
|
||||
// operation. Coordinates are pixel centres (x + 0.5, y + 0.5) in buffer pixels, as in
|
||||
// util::remap; scalar set-up (radii, angles, outlines, correspondence) is computed on the CPU in
|
||||
// f64 and passed in.
|
||||
|
||||
const FXW_PI: f32 = 3.1415927;
|
||||
|
||||
fn fxw_centre(p: vec2<i32>) -> vec2<f32> {
|
||||
return vec2<f32>(f32(p.x) + 0.5, f32(p.y) + 0.5);
|
||||
}
|
||||
|
||||
fn fxw_inside(p: vec2<i32>) -> bool {
|
||||
let d = out_dims();
|
||||
return p.x < d.x && p.y < d.y;
|
||||
}
|
||||
|
||||
// util::unpremul / premul.
|
||||
fn fxw_unpremul(p: vec4<f32>) -> vec3<f32> {
|
||||
if (p.w > 1e-6) {
|
||||
return p.xyz / p.w;
|
||||
}
|
||||
return vec3<f32>(0.0);
|
||||
}
|
||||
|
||||
fn fxw_smoothstep(e0: f32, e1: f32, x: f32) -> f32 {
|
||||
if (abs(e1 - e0) < 1e-9) {
|
||||
return select(1.0, 0.0, x < e0);
|
||||
}
|
||||
let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0);
|
||||
return t * t * (3.0 - 2.0 * t);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Warp (distort3::warp)
|
||||
// u[0] = (_, style, vertical); f[0] = (cx, cy, hx, hy); f[1] = (bend, horizontal, vertical
|
||||
// distortion). Positions are relative to the centre.
|
||||
|
||||
fn fxw_style(style: u32, k: f32, u: f32, v: f32) -> vec2<f32> {
|
||||
let bell = 1.0 - u * u;
|
||||
switch style {
|
||||
case 0u: {
|
||||
return vec2<f32>(u * (1.0 - 0.3 * k * v), v - k * bell);
|
||||
}
|
||||
case 1u: {
|
||||
return vec2<f32>(u, v - k * bell * (v + 1.0) * 0.5);
|
||||
}
|
||||
case 2u: {
|
||||
return vec2<f32>(u, v - k * bell * (1.0 - v) * 0.5);
|
||||
}
|
||||
case 3u: {
|
||||
return vec2<f32>(u, v - k * bell);
|
||||
}
|
||||
case 4u: {
|
||||
return vec2<f32>(u, v * (1.0 + k * bell));
|
||||
}
|
||||
case 5u: {
|
||||
return vec2<f32>(u, v + k * bell * bell * (v + 1.0) * 0.5);
|
||||
}
|
||||
case 6u: {
|
||||
return vec2<f32>(u, v - k * bell * bell * (1.0 - v) * 0.5);
|
||||
}
|
||||
case 7u: {
|
||||
return vec2<f32>(u, v - 0.5 * k * sin(FXW_PI * u));
|
||||
}
|
||||
case 8u: {
|
||||
return vec2<f32>(u, v - 0.5 * k * sin(FXW_PI * u + 0.5 * FXW_PI * v));
|
||||
}
|
||||
case 9u: {
|
||||
return vec2<f32>(u, v * (1.0 + 0.5 * k * sin(FXW_PI * u)));
|
||||
}
|
||||
case 10u: {
|
||||
return vec2<f32>(u, v - 0.5 * k * sin(0.5 * FXW_PI * u));
|
||||
}
|
||||
case 11u: {
|
||||
let r2 = u * u + v * v;
|
||||
if (r2 >= 1.0) {
|
||||
return vec2<f32>(u, v);
|
||||
}
|
||||
let f = 1.0 + k * (1.0 - r2);
|
||||
return vec2<f32>(u * f, v * f);
|
||||
}
|
||||
case 12u: {
|
||||
return vec2<f32>(u * (1.0 + 0.5 * k * (1.0 - v * v)), v * (1.0 + 0.5 * k * bell));
|
||||
}
|
||||
case 13u: {
|
||||
return vec2<f32>(u * (1.0 - 0.5 * k * (1.0 - v * v)), v * (1.0 + 0.5 * k * bell));
|
||||
}
|
||||
default: {
|
||||
let r = sqrt(u * u + v * v);
|
||||
if (r >= 1.0) {
|
||||
return vec2<f32>(u, v);
|
||||
}
|
||||
let a = k * FXW_PI * (1.0 - r);
|
||||
let s = sin(a);
|
||||
let c = cos(a);
|
||||
return vec2<f32>(u * c - v * s, u * s + v * c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fxw_warp_fwd(q: vec2<f32>) -> vec2<f32> {
|
||||
let h = P.f[0].zw;
|
||||
let k = P.f[1].x;
|
||||
let hd = P.f[1].y;
|
||||
let vd = P.f[1].z;
|
||||
let vertical = P.u[0].z != 0u;
|
||||
let n = q / h;
|
||||
var uv = n;
|
||||
if (vertical) {
|
||||
uv = n.yx;
|
||||
}
|
||||
let w = fxw_style(P.u[0].y, k, uv.x, uv.y);
|
||||
var o = w;
|
||||
if (vertical) {
|
||||
o = w.yx;
|
||||
}
|
||||
o.y *= 1.0 + 0.5 * hd * o.x;
|
||||
o.x *= 1.0 + 0.5 * vd * o.y;
|
||||
return o * h;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxw_warp(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxw_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let c = P.f[0].xy;
|
||||
let t = fxw_centre(p) - c;
|
||||
var s = t;
|
||||
for (var i = 0; i < 12; i++) {
|
||||
let f = fxw_warp_fwd(s);
|
||||
let r = f - t;
|
||||
// The CPU stops at 1e-7 px in f64; f32 cannot get there, and iterating on past
|
||||
// convergence can hop across a fold (Twist, Fisheye edges), so stop at 1e-4 px.
|
||||
if (abs(r.x) + abs(r.y) < 1e-4) {
|
||||
break;
|
||||
}
|
||||
let e = 0.01;
|
||||
let a = fxw_warp_fwd(s + vec2<f32>(e, 0.0));
|
||||
let b = fxw_warp_fwd(s + vec2<f32>(0.0, e));
|
||||
let j00 = (a.x - f.x) / e;
|
||||
let j10 = (a.y - f.y) / e;
|
||||
let j01 = (b.x - f.x) / e;
|
||||
let j11 = (b.y - f.y) / e;
|
||||
let det = j00 * j11 - j01 * j10;
|
||||
if (abs(det) < 1e-9) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
s.x -= (j11 * r.x - j01 * r.y) / det;
|
||||
s.y -= (-j10 * r.x + j00 * r.y) / det;
|
||||
}
|
||||
let f = fxw_warp_fwd(s);
|
||||
if (abs(f.x - t.x) + abs(f.y - t.y) > 0.5) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
textureStore(out, p, sample_bilinear(src, s.x + c.x, s.y + c.y));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Bend It (distort2::bend_it)
|
||||
// u[0].y = static prestart; f[0] = (start x, y, axis a x, y); f[1] = (normal n x, y, rho, beta);
|
||||
// f[2] = (end of arc pe x, y, length, _); f[3] = (t x, y, nn x, y).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxw_bendit(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxw_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let s = P.f[0].xy;
|
||||
let a = P.f[0].zw;
|
||||
let n = P.f[1].xy;
|
||||
let rho = P.f[1].z;
|
||||
let beta = P.f[1].w;
|
||||
let pe = P.f[2].xy;
|
||||
let l = P.f[2].z;
|
||||
let t = P.f[3].xy;
|
||||
let nn = P.f[3].zw;
|
||||
let v = fxw_centre(p) - s;
|
||||
let lx = dot(v, a);
|
||||
let ly = dot(v, n);
|
||||
// Arc: rho - |(lx, ly - rho)| without cancellation for large radii.
|
||||
let dy = ly - rho;
|
||||
let th = atan2(lx, -dy);
|
||||
let dd = lx * lx + ly * ly - 2.0 * ly * rho;
|
||||
let w_arc = -dd / (rho + sqrt(max(rho * rho + dd, 0.0)));
|
||||
var cand: array<vec3<f32>, 3>;
|
||||
cand[0] = vec3<f32>(rho * th, w_arc, select(0.0, 1.0, th >= 0.0 && th <= beta));
|
||||
let e = vec2<f32>(lx, ly) - pe;
|
||||
let u2 = dot(e, t);
|
||||
cand[1] = vec3<f32>(l + u2, dot(e, nn), select(0.0, 1.0, u2 >= 0.0));
|
||||
cand[2] = vec3<f32>(lx, ly, select(0.0, 1.0, lx < 0.0 && P.u[0].y != 0u));
|
||||
for (var i = 0; i < 3; i++) {
|
||||
let c = cand[i];
|
||||
if (c.z == 0.0) {
|
||||
continue;
|
||||
}
|
||||
let q = s + a * c.x + n * c.y;
|
||||
let px = sample_bilinear(src, q.x, q.y);
|
||||
if (px.w > 1e-4) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- CC Page Turn (distort2::page_turn)
|
||||
// u[0] = (_, show front, show back); f[0] = (fold x, y, n x, y); f[1] = (radius, light dot,
|
||||
// back opacity, _); f[2] = paper colour.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxw_pageturn(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxw_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let q = fxw_centre(p);
|
||||
let f = P.f[0].xy;
|
||||
let n = P.f[0].zw;
|
||||
let r = P.f[1].x;
|
||||
let ldot = P.f[1].y;
|
||||
let bo = P.f[1].z;
|
||||
let paper = P.f[2].xyz;
|
||||
let show_front = P.u[0].y != 0u;
|
||||
let show_back = P.u[0].z != 0u;
|
||||
let d = dot(q - f, n);
|
||||
if (d > r) {
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
return;
|
||||
}
|
||||
// (u, back side, shade, valid) from the top layer down.
|
||||
var cand: array<vec4<f32>, 4>;
|
||||
if (d <= 0.0) {
|
||||
cand[0] = vec4<f32>(FXW_PI * r - d, 1.0, 0.9, 1.0);
|
||||
cand[3] = vec4<f32>(d, 0.0, 1.0, 1.0);
|
||||
} else {
|
||||
let a = asin(clamp(d / r, -1.0, 1.0));
|
||||
cand[1] = vec4<f32>(FXW_PI * r - r * a, 1.0, 0.6 + 0.4 * cos(a), 1.0);
|
||||
let psi = a / (0.5 * FXW_PI);
|
||||
cand[2] = vec4<f32>(r * a, 0.0, 1.0 - 0.4 * psi * (0.5 + 0.5 * ldot), 1.0);
|
||||
}
|
||||
for (var i = 0; i < 4; i++) {
|
||||
let c = cand[i];
|
||||
if (c.w == 0.0) {
|
||||
continue;
|
||||
}
|
||||
let back = c.y != 0.0;
|
||||
if ((back && !show_back) || (!back && !show_front)) {
|
||||
continue;
|
||||
}
|
||||
let k = c.x - d;
|
||||
let v = sample_bilinear(src, q.x + k * n.x, q.y + k * n.y);
|
||||
if (v.w <= 1e-4) {
|
||||
continue;
|
||||
}
|
||||
var col = fxw_unpremul(v);
|
||||
if (back) {
|
||||
col = paper * (1.0 - bo) + col * bo;
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(col * c.z * v.w, v.w));
|
||||
return;
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(0.0));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- polygons (util)
|
||||
// A polygon of `n` points at data[base..base + 2n].
|
||||
|
||||
fn fxw_pt(base: u32, i: u32) -> vec2<f32> {
|
||||
return vec2<f32>(data[base + 2u * i], data[base + 2u * i + 1u]);
|
||||
}
|
||||
|
||||
fn fxw_in_poly(base: u32, n: u32, x: f32, y: f32) -> bool {
|
||||
if (n < 3u) {
|
||||
return false;
|
||||
}
|
||||
var inside = false;
|
||||
var j = n - 1u;
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let a = fxw_pt(base, i);
|
||||
let b = fxw_pt(base, j);
|
||||
if ((a.y > y) != (b.y > y) && x < (b.x - a.x) * (y - a.y) / (b.y - a.y) + a.x) {
|
||||
inside = !inside;
|
||||
}
|
||||
j = i;
|
||||
}
|
||||
return inside;
|
||||
}
|
||||
|
||||
// Distance to a closed polyline.
|
||||
fn fxw_dist_poly(base: u32, n: u32, q: vec2<f32>) -> f32 {
|
||||
if (n == 0u) {
|
||||
return 3.0e38;
|
||||
}
|
||||
if (n == 1u) {
|
||||
return length(fxw_pt(base, 0u) - q);
|
||||
}
|
||||
var best = 3.0e38;
|
||||
for (var i = 0u; i < n; i++) {
|
||||
let a = fxw_pt(base, i);
|
||||
let b = fxw_pt(base, (i + 1u) % n);
|
||||
let d = b - a;
|
||||
let l2 = dot(d, d);
|
||||
var t = 0.0;
|
||||
if (l2 > 0.0) {
|
||||
t = clamp(dot(q - a, d) / l2, 0.0, 1.0);
|
||||
}
|
||||
let e = a + d * t - q;
|
||||
best = min(best, dot(e, e));
|
||||
}
|
||||
return sqrt(best);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Smear (distort3::smear)
|
||||
// data = boundary outline, moved source outline; u[0] = (_, boundary points, moved points);
|
||||
// f[0] = (centroid x, y, cos, sin); f[1] = (scale, offset x, y, percent); f[2].x = 1 / elasticity.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxw_smear(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxw_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let q = fxw_centre(p);
|
||||
let nb = P.u[0].y;
|
||||
let nm = P.u[0].z;
|
||||
if (!fxw_in_poly(0u, nb, q.x, q.y)) {
|
||||
textureStore(out, p, textureLoad(src, p, 0));
|
||||
return;
|
||||
}
|
||||
var w = 1.0;
|
||||
if (!fxw_in_poly(2u * nb, nm, q.x, q.y)) {
|
||||
let dm = fxw_dist_poly(2u * nb, nm, q);
|
||||
let db = fxw_dist_poly(0u, nb, q);
|
||||
w = pow(db / max(db + dm, 1e-9), P.f[2].x);
|
||||
}
|
||||
let c = P.f[0].xy;
|
||||
let cs = P.f[0].z;
|
||||
let sn = P.f[0].w;
|
||||
let scl = P.f[1].x;
|
||||
let o = P.f[1].yz;
|
||||
let d = q - o - c;
|
||||
let iv = vec2<f32>(c.x + (d.x * cs + d.y * sn) / scl, c.y + (-d.x * sn + d.y * cs) / scl);
|
||||
let k = clamp(w * P.f[1].w, 0.0, 1.0);
|
||||
let s = q + (iv - q) * k;
|
||||
textureStore(out, p, sample_bilinear(src, s.x, s.y));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Reshape (distort3::reshape)
|
||||
// data = 48 destination points, their 48 displacements, then the boundary outline;
|
||||
// u[0] = (_, boundary points (0 = none), smooth); f[0] = (elasticity / 2, percent).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxw_reshape(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxw_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let q = fxw_centre(p);
|
||||
let nb = P.u[0].y;
|
||||
let bbase = 4u * 48u;
|
||||
if (nb > 0u && !fxw_in_poly(bbase, nb, q.x, q.y)) {
|
||||
textureStore(out, p, textureLoad(src, p, 0));
|
||||
return;
|
||||
}
|
||||
var wv = vec2<f32>(0.0);
|
||||
var ws = 0.0;
|
||||
var dmin = 3.0e38;
|
||||
for (var i = 0u; i < 48u; i++) {
|
||||
let e = q - fxw_pt(0u, i);
|
||||
let d2 = dot(e, e);
|
||||
dmin = min(dmin, d2);
|
||||
let w = 1.0 / pow(d2 + 1e-6, P.f[0].x);
|
||||
wv += w * fxw_pt(96u, i);
|
||||
ws += w;
|
||||
}
|
||||
var fade = 1.0;
|
||||
if (nb > 0u) {
|
||||
let db = fxw_dist_poly(bbase, nb, q);
|
||||
fade = db / max(db + sqrt(dmin), 1e-9);
|
||||
if (P.u[0].z != 0u) {
|
||||
fade = fade * fade * (3.0 - 2.0 * fade);
|
||||
}
|
||||
}
|
||||
let k = fade * P.f[0].y / ws;
|
||||
let s = q + wv * k;
|
||||
textureStore(out, p, sample_bilinear(src, s.x, s.y));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Color Emboss (stylize2::color_emboss)
|
||||
// f[0] = (dx, dy, contrast, blend).
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxw_coloremboss(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxw_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
if (px.w <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let q = fxw_centre(p);
|
||||
let d = P.f[0].xy;
|
||||
let c = fxw_unpremul(px);
|
||||
let pa = fxw_unpremul(sample_bilinear_clamped(src, q.x + d.x, q.y + d.y));
|
||||
let pb = fxw_unpremul(sample_bilinear_clamped(src, q.x - d.x, q.y - d.y));
|
||||
let e = max(c + (pa - pb) * P.f[0].z, vec3<f32>(0.0));
|
||||
let o = e + (c - e) * P.f[0].w;
|
||||
textureStore(out, p, vec4<f32>(o * px.w, px.w));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Cartoon (stylize2::cartoon)
|
||||
|
||||
// Plane::luma in every channel (the guided filter's guide).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxw_luma(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxw_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let l = luminance(fxw_unpremul(textureLoad(src, p, 0)));
|
||||
textureStore(out, p, vec4<f32>(l));
|
||||
}
|
||||
|
||||
// Colour from aux (the filtered channels), alpha from src.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxw_keep_alpha(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxw_inside(p)) {
|
||||
return;
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(textureLoad(aux, p, 0).xyz, textureLoad(src, p, 0).w));
|
||||
}
|
||||
|
||||
// Edge Enhancement > 0: one shock-filter step. src = the smoothed picture, aux = its luma (x);
|
||||
// f[0].x = step length.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxw_shock(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxw_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let g0 = tex_get_clamped(aux, p.x, p.y).x;
|
||||
let gr = tex_get_clamped(aux, p.x + 1, p.y).x;
|
||||
let gl = tex_get_clamped(aux, p.x - 1, p.y).x;
|
||||
let gd = tex_get_clamped(aux, p.x, p.y + 1).x;
|
||||
let gu = tex_get_clamped(aux, p.x, p.y - 1).x;
|
||||
let gx = (gr - gl) * 0.5;
|
||||
let gy = (gd - gu) * 0.5;
|
||||
let lap = gr + gl + gd + gu - 4.0 * g0;
|
||||
let m = sqrt(gx * gx + gy * gy);
|
||||
if (m < 1e-4 || abs(lap) < 1e-5) {
|
||||
textureStore(out, p, textureLoad(src, p, 0));
|
||||
return;
|
||||
}
|
||||
let k = -sign(lap) * P.f[0].x / m;
|
||||
let q = fxw_centre(p);
|
||||
textureStore(out, p, sample_bilinear_clamped(src, q.x + gx * k, q.y + gy * k));
|
||||
}
|
||||
|
||||
// Sobel magnitude of the luma (src.x) through the edge threshold: f[0] = (t0, t1).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxw_edges(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxw_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let a = tex_get_clamped(src, p.x - 1, p.y - 1).x;
|
||||
let b = tex_get_clamped(src, p.x, p.y - 1).x;
|
||||
let c = tex_get_clamped(src, p.x + 1, p.y - 1).x;
|
||||
let d = tex_get_clamped(src, p.x - 1, p.y).x;
|
||||
let f = tex_get_clamped(src, p.x + 1, p.y).x;
|
||||
let g = tex_get_clamped(src, p.x - 1, p.y + 1).x;
|
||||
let h = tex_get_clamped(src, p.x, p.y + 1).x;
|
||||
let i = tex_get_clamped(src, p.x + 1, p.y + 1).x;
|
||||
let gx = c + 2.0 * f + i - a - 2.0 * d - g;
|
||||
let gy = g + 2.0 * h + i - a - 2.0 * b - c;
|
||||
let m = sqrt(gx * gx + gy * gy);
|
||||
textureStore(out, p, vec4<f32>(fxw_smoothstep(P.f[0].x, P.f[0].y, m), 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// The cartoon fill and edges. src = the layer, aux = the smoothed picture, data = the edge
|
||||
// plane's rows (x); u[0] = (_, render, edge row stride, has edges); f[0] = (steps, smoothness,
|
||||
// edge opacity × width factor, edge black level); f[1].x = edge contrast.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxw_cartoon(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxw_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let a = px.w;
|
||||
if (a <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let render = P.u[0].y;
|
||||
let c = fxw_unpremul(textureLoad(aux, p, 0));
|
||||
let steps = P.f[0].x;
|
||||
let smoothness = P.f[0].y;
|
||||
let l = luminance(c);
|
||||
let t = max(l, 0.0) * steps;
|
||||
let fr = t - floor(t);
|
||||
var f2 = 0.0;
|
||||
if (smoothness <= 0.0) {
|
||||
f2 = select(0.0, 1.0, fr >= 0.5);
|
||||
} else {
|
||||
f2 = fxw_smoothstep(0.5 - smoothness * 0.5, 0.5 + smoothness * 0.5, fr);
|
||||
}
|
||||
let lq = (floor(t) + f2) / steps;
|
||||
var fill = vec3<f32>(lq);
|
||||
if (l > 1e-4) {
|
||||
fill = c * lq / l;
|
||||
}
|
||||
var e = 0.0;
|
||||
if (P.u[0].w != 0u) {
|
||||
e = data[(u32(p.y) * P.u[0].z + u32(p.x)) * 4u] * P.f[0].z;
|
||||
}
|
||||
let g = clamp((abs((1.0 - e) - P.f[0].w) - 0.5) * P.f[1].x + 0.5, 0.0, 1.0);
|
||||
var o = fill;
|
||||
if (render == 1u) {
|
||||
o = vec3<f32>(g);
|
||||
} else if (render > 1u) {
|
||||
o = fill * g;
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(o * a, a));
|
||||
}
|
||||
|
|
@ -33,7 +33,7 @@ fn warp_blend(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
if ((flags & 2u) != 0u) {
|
||||
d = vec4<f32>(0.0);
|
||||
}
|
||||
textureStore(out, vec2<i32>(x, y), d);
|
||||
store_q(vec2<i32>(x, y), d, P.f[3].z);
|
||||
return;
|
||||
}
|
||||
let r0 = P.f[0];
|
||||
|
|
@ -52,7 +52,7 @@ fn warp_blend(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
if (stencil && (flags & 1u) == 0u) {
|
||||
d = blend_pixel(mode, d, vec4<f32>(0.0), 0.5);
|
||||
}
|
||||
textureStore(out, vec2<i32>(x, y), d);
|
||||
store_q(vec2<i32>(x, y), d, P.f[3].z);
|
||||
return;
|
||||
}
|
||||
var s = sample(src, sampling, px, py);
|
||||
|
|
@ -60,11 +60,11 @@ fn warp_blend(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
if (s.w > 0.0) {
|
||||
d = d + s * P.f[3].y;
|
||||
}
|
||||
textureStore(out, vec2<i32>(x, y), d);
|
||||
store_q(vec2<i32>(x, y), d, P.f[3].z);
|
||||
return;
|
||||
}
|
||||
if (s.w <= 0.0 && !stencil) {
|
||||
textureStore(out, vec2<i32>(x, y), d);
|
||||
store_q(vec2<i32>(x, y), d, P.f[3].z);
|
||||
return;
|
||||
}
|
||||
let op = P.f[3].x;
|
||||
|
|
@ -75,7 +75,7 @@ fn warp_blend(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
if (mode == 1u || mode == 2u) {
|
||||
n = hash_noise(u32(x), u32(y), seed);
|
||||
}
|
||||
textureStore(out, vec2<i32>(x, y), blend_pixel(mode, d, s, n));
|
||||
store_q(vec2<i32>(x, y), blend_pixel(mode, d, s, n), P.f[3].z);
|
||||
}
|
||||
|
||||
// Blend `src` (same size) onto `aux` (Image::blend_from).
|
||||
|
|
@ -91,7 +91,7 @@ fn blend_full(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
let d = textureLoad(aux, p, 0);
|
||||
var s = textureLoad(src, p, 0);
|
||||
if (s.w <= 0.0 && !is_stencil(mode)) {
|
||||
textureStore(out, p, d);
|
||||
store_q(p, d, P.f[0].y);
|
||||
return;
|
||||
}
|
||||
s = s * P.f[0].x;
|
||||
|
|
@ -99,7 +99,7 @@ fn blend_full(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
if (mode == 1u || mode == 2u) {
|
||||
n = hash_noise(gid.x, gid.y, P.u[0].z);
|
||||
}
|
||||
textureStore(out, p, blend_pixel(mode, d, s, n));
|
||||
store_q(p, blend_pixel(mode, d, s, n), P.f[0].y);
|
||||
}
|
||||
|
||||
// Track matte: `src` × matte factor of `aux`. u[0].x = 0 alpha, 1 alpha inverted, 2 luma,
|
||||
|
|
@ -179,15 +179,27 @@ fn quantize(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
let levels = P.f[0].x;
|
||||
textureStore(out, p, quantize_px(textureLoad(src, p, 0), P.f[0].x));
|
||||
}
|
||||
|
||||
fn quantize_px(s: vec4<f32>, levels: f32) -> vec4<f32> {
|
||||
let inv = 1.0 / levels;
|
||||
let s = textureLoad(src, p, 0);
|
||||
let a = round_away(clamp(s.w, 0.0, 1.0) * levels) * inv;
|
||||
var o = vec4<f32>(0.0, 0.0, 0.0, a);
|
||||
o.x = round_away(clamp(s.x, 0.0, a) * levels) * inv;
|
||||
o.y = round_away(clamp(s.y, 0.0, a) * levels) * inv;
|
||||
o.z = round_away(clamp(s.z, 0.0, a) * levels) * inv;
|
||||
textureStore(out, p, o);
|
||||
return o;
|
||||
}
|
||||
|
||||
// Store a compositing result, clamped and quantised to `levels` when > 0 (8/16 bpc: the comp
|
||||
// after a layer, fused into the layer's composite).
|
||||
fn store_q(p: vec2<i32>, v: vec4<f32>, levels: f32) {
|
||||
if (levels > 0.0) {
|
||||
textureStore(out, p, quantize_px(v, levels));
|
||||
} else {
|
||||
textureStore(out, p, v);
|
||||
}
|
||||
}
|
||||
|
||||
// Colour conversion on straight colour (render::color::convert).
|
||||
|
|
@ -959,3 +971,16 @@ fn bokeh_gather(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
}
|
||||
textureStore(out, p, acc);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- pool
|
||||
|
||||
// Set every pixel to f[0] (clearing reused working textures; NaN poison under test).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fill(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (p.x >= dims.x || p.y >= dims.y) {
|
||||
return;
|
||||
}
|
||||
textureStore(out, p, P.f[0]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -156,8 +156,15 @@ pub(crate) fn diff(cpu: &Image, gpu: &Image, tol: f32) -> Diff {
|
|||
let mut d = Diff { max: 0.0, over: 0, total: cpu.data.len(), worst: (0, 0, [0.0; 4], [0.0; 4]), quantized: false };
|
||||
for (i, (a, b)) in cpu.data.iter().zip(&gpu.data).enumerate() {
|
||||
// Absolute below 1, relative above (32 bpc over-range values, e.g. Divide by ~0).
|
||||
let m = (0..4).map(|c| (a[c] - b[c]).abs() / a[c].abs().max(1.0)).fold(0.0f32, f32::max);
|
||||
let m = if m.is_nan() { f32::INFINITY } else { m };
|
||||
// A NaN on one side only (e.g. a kernel that skipped pixels of a NaN-poisoned scratch
|
||||
// image) is the largest difference.
|
||||
let m = (0..4)
|
||||
.map(|c| match (a[c].is_nan(), b[c].is_nan()) {
|
||||
(true, true) => 0.0,
|
||||
(false, false) => (a[c] - b[c]).abs() / a[c].abs().max(1.0),
|
||||
_ => f32::INFINITY,
|
||||
})
|
||||
.fold(0.0f32, f32::max);
|
||||
if m > tol {
|
||||
d.over += 1;
|
||||
}
|
||||
|
|
@ -511,7 +518,7 @@ fn effect_chains_and_mixed_stacks() {
|
|||
let mut l = s.footage(80, 50);
|
||||
s.effect(&mut l, "ec.color.exposure", &[("master/exposure", n(0.7))]);
|
||||
s.effect(&mut l, "ec.blur.gaussian", &[("blurriness", n(5.0))]);
|
||||
s.effect(&mut l, "ec.stylize.coloremboss", &[]);
|
||||
s.effect(&mut l, "ec.stylize.ccplastic", &[]);
|
||||
s.effect(&mut l, "ec.stylize.glow", &[]);
|
||||
s.effect(&mut l, "ec.channel.invert", &[("blend", n(50.0))]);
|
||||
s.push(l);
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ fn gpu_adjustment_chains_match_the_cpu() {
|
|||
#[test]
|
||||
fn mixed_adjustment_chains_fall_back_per_effect() {
|
||||
let mut stack = gpu_stack();
|
||||
stack.insert(1, ("ec.stylize.coloremboss", vec![]));
|
||||
stack.insert(1, ("ec.stylize.ccplastic", vec![]));
|
||||
stack.push(("ec.stylize.glow", vec![("threshold", n(30.0))]));
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let s = adjustment_scene(depth, &stack, true);
|
||||
|
|
@ -93,7 +93,7 @@ impl FxTarget for Probe {
|
|||
fn adjustment_stacks_split_into_gpu_runs_and_cpu_steps() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let mut stack = gpu_stack();
|
||||
stack.insert(1, ("ec.stylize.coloremboss", vec![]));
|
||||
stack.insert(1, ("ec.stylize.ccplastic", vec![]));
|
||||
let s = adjustment_scene(BitDepth::Bpc32, &stack, false);
|
||||
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
|
||||
r.accel = Some(g);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
//! GPU shape and bevel effects (Circle, Ellipse, Iris Wipe, Bevel Alpha, Bevel Edges, Gaussian
|
||||
//! Blur (Legacy)) vs the CPU effects (the oracle): direct on a buffer at full and half
|
||||
//! resolution, as adjustment, and composited at 8 and 32 bpc.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
|
||||
use crate::tests::{c, effect_case, n};
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
fn on() -> Value {
|
||||
Value::Bool(true)
|
||||
}
|
||||
|
||||
fn pt(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circle_and_ellipse() {
|
||||
effect_case("ec.generate.circle", &[("center", pt(30.0, 20.0)), ("radius", n(15.0))]);
|
||||
effect_case("ec.generate.circle", &[("radius", n(18.0)), ("edge", e(2)), ("thickness", n(5.0)), ("feather/feather", n(3.0)), ("blendingMode", e(3))]);
|
||||
effect_case(
|
||||
"ec.generate.circle",
|
||||
&[("radius", n(20.0)), ("edge", e(4)), ("thickness", n(30.0)), ("feather/featherInner", n(10.0)), ("invert", on()), ("opacity", n(60.0))],
|
||||
);
|
||||
effect_case("ec.generate.circle", &[("radius", n(12.0)), ("edge", e(1)), ("edgeRadius", n(25.0)), ("color", c(0.2, 0.8, 0.4))]);
|
||||
effect_case("ec.generate.ellipse", &[("center", pt(35.0, 22.0)), ("width", n(50.0)), ("height", n(30.0)), ("thickness", n(6.0))]);
|
||||
effect_case("ec.generate.ellipse", &[("width", n(40.0)), ("height", n(60.0)), ("softness", n(40.0)), ("compositeOnOriginal", on())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iris_wipe() {
|
||||
effect_case("ec.transition.iriswipe", &[("outerRadius", n(20.0)), ("center", pt(35.0, 22.0))]);
|
||||
effect_case(
|
||||
"ec.transition.iriswipe",
|
||||
&[("outerRadius", n(25.0)), ("points", n(5.0)), ("useInnerRadius", on()), ("innerRadius", n(10.0)), ("rotation", n(20.0)), ("feather", n(4.0))],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bevels_and_legacy_gaussian() {
|
||||
effect_case("ec.perspective.bevelalpha", &[("edgeThickness", n(4.0)), ("lightIntensity", n(0.6))]);
|
||||
effect_case("ec.perspective.bevelalpha", &[("edgeThickness", n(2.0)), ("lightAngle", n(200.0)), ("lightColor", c(1.0, 0.8, 0.5))]);
|
||||
effect_case("ec.perspective.beveledges", &[("edgeThickness", n(0.1)), ("lightIntensity", n(0.5))]);
|
||||
effect_case("ec.perspective.beveledges", &[("edgeThickness", n(0.3)), ("lightAngle", n(130.0))]);
|
||||
effect_case("ec.obsolete.gaussianlegacy", &[("blurriness", n(7.0))]);
|
||||
effect_case("ec.obsolete.gaussianlegacy", &[("blurriness", n(5.0)), ("blurDimensions", e(2))]);
|
||||
}
|
||||
|
|
@ -32,6 +32,17 @@ fn medians() {
|
|||
effect_case("ec.noise.dustscratches", &[("radius", n(2.0)), ("threshold", n(0.0)), ("operateOnAlpha", on())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_radius_medians() {
|
||||
// Past the bisection radius: the sliding-histogram kernel (windows larger than the layer).
|
||||
effect_case("ec.noise.median", &[("radius", n(9.0))]);
|
||||
effect_case("ec.noise.median", &[("radius", n(24.0)), ("operateOnAlpha", on())]);
|
||||
effect_case("ec.noise.medianlegacy", &[("radius", n(17.0))]);
|
||||
effect_case("ec.noise.medianlegacy", &[("radius", n(40.0)), ("operateOnAlphaChannel", on())]);
|
||||
effect_case("ec.noise.dustscratches", &[("radius", n(20.0)), ("threshold", n(10.0))]);
|
||||
effect_case("ec.noise.dustscratches", &[("radius", n(60.0)), ("threshold", n(0.0)), ("operateOnAlpha", on())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_preserving_blurs() {
|
||||
effect_case("ec.blur.bilateral", &[]);
|
||||
|
|
|
|||
|
|
@ -149,6 +149,9 @@ fn twirl_legacy_ripple_pulse_power_pin() {
|
|||
effect_case("ec.distort.ccripplepulse", &[("pulseLevel", n(-60.0)), ("amplitude", n(150.0)), ("renderBump", on()), ("center", pt(25.0, 20.0))]);
|
||||
effect_case("ec.distort.ccpowerpin", &[("topLeft", pt(5.0, 3.0)), ("bottomRight", pt(62.0, 38.0))]);
|
||||
effect_case("ec.distort.ccpowerpin", &[("topRight", pt(80.0, -6.0)), ("bottomLeft", pt(-4.0, 50.0)), ("expandLeft", n(20.0)), ("expandBottom", n(-10.0))]);
|
||||
// Perspective below 100 %: blended toward the bilinear inverse.
|
||||
effect_case("ec.distort.ccpowerpin", &[("topRight", pt(80.0, -6.0)), ("bottomLeft", pt(-4.0, 50.0)), ("perspective", n(0.0))]);
|
||||
effect_case("ec.distort.ccpowerpin", &[("topLeft", pt(12.0, 6.0)), ("bottomRight", pt(75.0, 30.0)), ("perspective", n(40.0)), ("expandTop", n(15.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,191 @@
|
|||
//! GPU warp family effects (Warp, Bezier Warp, CC Bend It, CC Page Turn, Smear, Reshape, Color
|
||||
//! Emboss, Cartoon) vs the CPU effects (the oracle): direct on a buffer at full and half
|
||||
//! resolution, as adjustment, and composited at 8 and 32 bpc.
|
||||
|
||||
use effectcraft_effects::MaskShape;
|
||||
use effectcraft_keyframe::{ShapePath, Value};
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{BitDepth, MaskMode};
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
use crate::tests::{Scene, c, check, compare_at, diff, effect_case, gpu, n, opts, pattern};
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
fn pt(x: f64, y: f64) -> Value {
|
||||
Value::Vec2([x, y])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn warp_styles() {
|
||||
for style in 0..15 {
|
||||
effect_case("ec.distort.warp", &[("warpStyle", e(style)), ("bend", n(45.0))]);
|
||||
}
|
||||
effect_case("ec.distort.warp", &[("warpStyle", e(8)), ("warpAxis", e(1)), ("bend", n(-60.0)), ("horizontalDistortion", n(20.0))]);
|
||||
effect_case("ec.distort.warp", &[("warpStyle", e(4)), ("bend", n(80.0)), ("verticalDistortion", n(-35.0))]);
|
||||
effect_case("ec.distort.warp", &[("warpStyle", e(11)), ("bend", n(-50.0))]);
|
||||
effect_case("ec.distort.warp", &[("warpStyle", e(14)), ("bend", n(-35.0))]);
|
||||
// Fisheye / Twist with a distortion render on the CPU (see catalog::gpu_supported).
|
||||
let spec = effectcraft_effects::find("ec.distort.warp").unwrap();
|
||||
let mut params = effectcraft_effects::Params {
|
||||
values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, [70.0, 44.0]))).collect(),
|
||||
};
|
||||
params.values.insert("warpStyle".into(), e(14));
|
||||
params.values.insert("verticalDistortion".into(), n(-35.0));
|
||||
let ctx = effectcraft_effects::EffectCtx { params: ¶ms, time: 0.0, layer_size: [70.0, 44.0], seed: 0, adjustment: false, env: Default::default() };
|
||||
assert!(!effectcraft_effects::catalog::gpu_supported("ec.distort.warp", &ctx));
|
||||
effect_case("ec.distort.warp", &[("warpStyle", e(3)), ("bend", n(0.0)), ("horizontalDistortion", n(-40.0)), ("verticalDistortion", n(25.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bezier_warp() {
|
||||
effect_case("ec.distort.bezierwarp", &[]);
|
||||
effect_case(
|
||||
"ec.distort.bezierwarp",
|
||||
&[("topLeftVertex", pt(8.0, -4.0)), ("topLeftTangent", pt(30.0, 12.0)), ("rightBottomTangent", pt(80.0, 30.0)), ("leftBottomVertex", pt(-6.0, 50.0))],
|
||||
);
|
||||
effect_case("ec.distort.bezierwarp", &[("topRightTangent", pt(40.0, -20.0)), ("bottomLeftTangent", pt(20.0, 60.0)), ("quality", n(3.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bend_it_and_page_turn() {
|
||||
effect_case("ec.distort.ccbendit", &[("bend", n(45.0))]);
|
||||
effect_case("ec.distort.ccbendit", &[("bend", n(-120.0)), ("start", pt(10.0, 30.0)), ("end", pt(55.0, 8.0)), ("renderPrestart", e(1))]);
|
||||
// A slight bend: a large arc radius.
|
||||
effect_case("ec.distort.ccbendit", &[("bend", n(0.05)), ("start", pt(5.0, 22.0)), ("end", pt(60.0, 22.0))]);
|
||||
effect_case("ec.distort.ccpageturn", &[]);
|
||||
effect_case("ec.distort.ccpageturn", &[("foldRadius", n(4.0)), ("foldDirection", n(30.0)), ("foldPosition", pt(40.0, 20.0))]);
|
||||
effect_case(
|
||||
"ec.distort.ccpageturn",
|
||||
&[("render", e(2)), ("backPageOpacity", n(40.0)), ("paperColor", c(0.9, 0.85, 0.7)), ("lightDirection", n(120.0)), ("foldPosition", pt(30.0, 25.0))],
|
||||
);
|
||||
effect_case("ec.distort.ccpageturn", &[("render", e(1)), ("foldPosition", pt(50.0, 10.0))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_emboss_and_cartoon() {
|
||||
effect_case("ec.stylize.coloremboss", &[]);
|
||||
effect_case("ec.stylize.coloremboss", &[("direction", n(30.0)), ("relief", n(3.5)), ("contrast", n(250.0)), ("blend", n(30.0))]);
|
||||
effect_case("ec.stylize.cartoon", &[]);
|
||||
effect_case("ec.stylize.cartoon", &[("render", e(0)), ("detailRadius", n(4.0)), ("fill/shadingSteps", n(5.0)), ("fill/shadingSmoothness", n(0.0))]);
|
||||
effect_case("ec.stylize.cartoon", &[("render", e(1)), ("edge/edgeWidth", n(0.6)), ("advanced/edgeBlackLevel", n(40.0)), ("advanced/edgeContrast", n(0.8))]);
|
||||
effect_case("ec.stylize.cartoon", &[("edge/edgeWidth", n(3.3)), ("advanced/edgeEnhancement", n(60.0)), ("edge/edgeSoftness", n(20.0))]);
|
||||
effect_case("ec.stylize.cartoon", &[("advanced/edgeEnhancement", n(-50.0)), ("detailRadius", n(0.0))]);
|
||||
}
|
||||
|
||||
/// Mask outlines in layer pixels (70 × 44 layers): a source blob, a destination blob and a
|
||||
/// boundary around both.
|
||||
fn outlines() -> Vec<MaskShape> {
|
||||
let ellipse = |cx: f64, cy: f64, rx: f64, ry: f64| -> Vec<[f64; 2]> {
|
||||
(0..40).map(|i| i as f64 / 40.0 * std::f64::consts::TAU).map(|a| [cx + rx * a.cos(), cy + ry * a.sin()]).collect()
|
||||
};
|
||||
let shape = |name: &str, points| MaskShape { name: name.into(), points, closed: true, inverted: false };
|
||||
vec![shape("Mask 1", ellipse(25.0, 20.0, 9.0, 7.0)), shape("Mask 2", ellipse(42.0, 24.0, 12.0, 6.0)), shape("Mask 3", ellipse(34.0, 22.0, 30.0, 19.0))]
|
||||
}
|
||||
|
||||
/// The effect alone on a buffer with masks (`effect_direct` with the layer's masks).
|
||||
fn direct_with_masks(id: &str, vals: &[(&str, Value)]) {
|
||||
let Some(g) = gpu() else { return };
|
||||
let spec = effectcraft_effects::find(id).unwrap();
|
||||
let size = [70.0, 44.0];
|
||||
let masks = outlines();
|
||||
let mut params =
|
||||
effectcraft_effects::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, size))).collect() };
|
||||
for (k, v) in vals {
|
||||
params.values.insert(k.to_string(), v.clone());
|
||||
}
|
||||
for adjustment in [false, true] {
|
||||
for scale in [1.0, 0.5] {
|
||||
let img = pattern(7, (size[0] * scale) as u32, (size[1] * scale) as u32);
|
||||
let buf = effectcraft_effects::Buf { img, offset: [0.0, 0.0], scale };
|
||||
let ctx = || effectcraft_effects::EffectCtx {
|
||||
params: ¶ms,
|
||||
time: 0.25,
|
||||
layer_size: size,
|
||||
seed: 11,
|
||||
adjustment,
|
||||
env: effectcraft_effects::EffectEnv { masks: &masks, ..Default::default() },
|
||||
};
|
||||
let cpu = (spec.render)(&ctx(), buf.clone());
|
||||
let out =
|
||||
effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect");
|
||||
assert_eq!((out.offset, out.scale), (cpu.offset, cpu.scale), "{id}: geometry");
|
||||
assert!(cpu.img.data != buf.img.data, "{id} {vals:?}: the case changes the picture");
|
||||
let d = diff(&cpu.img, &out.img, 1.0 / 255.0);
|
||||
// f32 point-in-polygon tests may flip a pixel exactly on an outline.
|
||||
assert!(
|
||||
d.over * 1000 <= d.total,
|
||||
"{id} {vals:?} adj {adjustment} scale {scale}: {} of {} pixels over 1/255 (max {}); worst {:?}",
|
||||
d.over,
|
||||
d.total,
|
||||
d.max,
|
||||
d.worst
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Composited at 8 and 32 bpc, with the masks on the layer (mode None: they only feed the effect).
|
||||
fn composite_with_masks(id: &str, vals: &[(&str, Value)]) {
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||
s.push(bg);
|
||||
let mut l = s.footage(70, 44);
|
||||
let mut next = s.p.next_id;
|
||||
for m in outlines() {
|
||||
let path = ShapePath {
|
||||
vertices: m.points.clone(),
|
||||
in_tangents: vec![[0.0; 2]; m.points.len()],
|
||||
out_tangents: vec![[0.0; 2]; m.points.len()],
|
||||
closed: true,
|
||||
..Default::default()
|
||||
};
|
||||
let g = build::mask(&mut Ids(&mut next), &m.name, path, MaskMode::None, [255, 255, 0]);
|
||||
l.props.sub_mut("masks").unwrap().children.push(g.into());
|
||||
}
|
||||
s.p.next_id = next;
|
||||
s.effect(&mut l, id, vals);
|
||||
s.push(l);
|
||||
check(&format!("{id} {vals:?} {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.003);
|
||||
check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.003);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smear_and_reshape() {
|
||||
let smear: &[&[(&str, Value)]] = &[
|
||||
&[("sourceMask", e(1)), ("boundaryMask", e(3)), ("maskOffset", pt(10.0, 3.0))],
|
||||
&[
|
||||
("sourceMask", e(1)),
|
||||
("boundaryMask", e(3)),
|
||||
("maskOffset", pt(-4.0, 6.0)),
|
||||
("maskRotation", n(30.0)),
|
||||
("maskScale", n(130.0)),
|
||||
("elasticity", e(6)),
|
||||
("percent", n(70.0)),
|
||||
],
|
||||
];
|
||||
for vals in smear {
|
||||
direct_with_masks("ec.distort.smear", vals);
|
||||
composite_with_masks("ec.distort.smear", vals);
|
||||
}
|
||||
let reshape: &[&[(&str, Value)]] = &[
|
||||
&[("sourceMask", e(1)), ("destinationMask", e(2))],
|
||||
&[("sourceMask", e(1)), ("destinationMask", e(2)), ("boundaryMask", e(3)), ("interpolation", e(2)), ("elasticity", e(0))],
|
||||
&[
|
||||
("sourceMask", e(1)),
|
||||
("destinationMask", e(2)),
|
||||
("boundaryMask", e(3)),
|
||||
("correspondencePoints", Value::Str("0,0.25 0.5,0.8".into())),
|
||||
("percent", n(60.0)),
|
||||
],
|
||||
];
|
||||
for vals in reshape {
|
||||
direct_with_masks("ec.distort.reshape", vals);
|
||||
composite_with_masks("ec.distort.reshape", vals);
|
||||
}
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ pub(crate) fn render<'g>(e: &mut Enc<'g>, r: &Renderer, comp_id: ItemId, t: Tick
|
|||
if r.depth() > 16 || !e.g.fits(w, h) {
|
||||
return None;
|
||||
}
|
||||
let mut canvas = e.image(w, h);
|
||||
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() {
|
||||
|
|
@ -135,7 +135,7 @@ fn draw_collapsed<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, layer: &
|
|||
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);
|
||||
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)
|
||||
}
|
||||
|
|
@ -165,22 +165,22 @@ fn draw_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &
|
|||
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);
|
||||
let mut iso = e.zeros(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, b, &iso, *m, 1.0, None)?;
|
||||
}
|
||||
iso = place(e, r, ctx, layer, &st.body, &iso, BlendMode::Normal, 1.0)?;
|
||||
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.image(w, h), BlendMode::Normal, 1.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)?;
|
||||
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)?;
|
||||
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(())
|
||||
}
|
||||
|
|
@ -192,7 +192,7 @@ fn draw_adjustment(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canv
|
|||
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, &Arc::new(foot), &e.image(w, h), BlendMode::Normal, 1.0)?;
|
||||
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() {
|
||||
|
|
@ -215,47 +215,60 @@ fn draw_adjustment(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canv
|
|||
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 {
|
||||
*canvas = place(e, r, ctx, layer, buf, canvas, layer.blend_mode, opacity)?;
|
||||
// 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.image(canvas.width, canvas.height), BlendMode::Normal, 1.0)?;
|
||||
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, 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);
|
||||
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)?;
|
||||
mimg = place(e, r, ctx, m, &mb, &mimg, BlendMode::Normal, mo, None)?;
|
||||
}
|
||||
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);
|
||||
*canvas = ops::blend_full(e, canvas, &iso, layer.blend_mode, opacity, layer.id.0 as u32, r.pipe().levels);
|
||||
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<Buf>, target: &GpuImage, mode: BlendMode, opacity: f32) -> Option<GpuImage> {
|
||||
/// 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(e, target, &src, m, pl.sampling, mode, opacity, pl.seed, None));
|
||||
return Some(ops::warp_q(e, target, &src, m, pl.sampling, mode, opacity, pl.seed, None, levels));
|
||||
}
|
||||
let mut acc = e.image(target.width, target.height);
|
||||
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))
|
||||
Some(ops::blend_full(e, target, &acc, mode, opacity, pl.seed, levels))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,144 @@
|
|||
//! [`Backend::Auto`](crate::Backend::Auto)'s per-comp choice between the CPU and the GPU
|
||||
//! compositor, from measured frame times.
|
||||
//!
|
||||
//! A light comp (a couple of small layers) composites faster on the CPU, which only touches
|
||||
//! the layers' bounds, than on the GPU, which works on whole frames and reads the result back;
|
||||
//! heavy comps (many layers, 3D, GPU effects) are faster on the GPU. Neither a layer count nor
|
||||
//! a pixel count predicts the crossover across machines, so Auto measures: per comp, output
|
||||
//! scale and path (readback / viewer display), it keeps an exponentially weighted average of
|
||||
//! the CPU and GPU frame times and renders on the faster one. Each side is warmed up first
|
||||
//! (its first frame, with layer cache misses and texture uploads, is not counted; the fastest of
|
||||
//! the next ones starts the average), and the slower side is re-measured every
|
||||
//! [`AutoPick::REPROBE`] frames so the choice follows the comp as it changes.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use effectcraft_project::ItemId;
|
||||
|
||||
/// Which frames share timing history: one comp at one output scale on one path.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct AutoKey {
|
||||
pub comp: ItemId,
|
||||
/// `RenderOpts::scale` bits.
|
||||
pub scale: u64,
|
||||
/// The viewer's display path (GPU frames stay on the device) rather than a readback.
|
||||
pub display: bool,
|
||||
}
|
||||
|
||||
impl AutoKey {
|
||||
pub fn new(comp: ItemId, scale: f64, display: bool) -> AutoKey {
|
||||
AutoKey { comp, scale: scale.to_bits(), display }
|
||||
}
|
||||
}
|
||||
|
||||
/// Timing history of one [`AutoKey`].
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct AutoStats {
|
||||
/// Averaged ms/frame (`None` until a counted sample).
|
||||
pub cpu_ms: Option<f64>,
|
||||
pub gpu_ms: Option<f64>,
|
||||
/// Frames measured on each side (the first is the uncounted warm-up).
|
||||
pub cpu_frames: u32,
|
||||
pub gpu_frames: u32,
|
||||
/// Frames since the side not chosen was last measured.
|
||||
pub since_probe: u32,
|
||||
/// The GPU declined the last frame it was asked for (the CPU rendered it).
|
||||
pub declined: bool,
|
||||
}
|
||||
|
||||
/// Per-comp CPU / GPU timing history for [`Backend::Auto`](crate::Backend::Auto) (held by the
|
||||
/// accelerator, see [`Accelerator::auto_pick`](crate::Accelerator::auto_pick)).
|
||||
#[derive(Default)]
|
||||
pub struct AutoPick {
|
||||
stats: Mutex<HashMap<AutoKey, AutoStats>>,
|
||||
}
|
||||
|
||||
impl AutoPick {
|
||||
/// Frames measured on each side before choosing (the first is not counted).
|
||||
pub const WARMUP: u32 = 3;
|
||||
/// Weight of a new sample in the running average (slower / faster than the average).
|
||||
pub const ALPHA: f64 = 0.3;
|
||||
pub const ALPHA_DOWN: f64 = 0.5;
|
||||
/// Re-measure the side not chosen after this many frames.
|
||||
pub const REPROBE: u32 = 48;
|
||||
/// The CPU wins only when clearly faster (ties and noise stay on the GPU).
|
||||
pub const MARGIN: f64 = 0.85;
|
||||
/// Keys remembered before the history is reset.
|
||||
const MAX_KEYS: usize = 1024;
|
||||
|
||||
/// Render the frame for `key` on the GPU?
|
||||
pub fn choose(&self, key: AutoKey) -> bool {
|
||||
let Ok(mut m) = self.stats.lock() else { return true };
|
||||
if m.len() > Self::MAX_KEYS && !m.contains_key(&key) {
|
||||
m.clear();
|
||||
}
|
||||
let s = m.entry(key).or_default();
|
||||
// Warm up both sides, alternating, GPU first.
|
||||
let gpu_warm = s.declined || s.gpu_frames >= Self::WARMUP;
|
||||
if !gpu_warm && s.gpu_frames <= s.cpu_frames {
|
||||
return true;
|
||||
}
|
||||
if s.cpu_frames < Self::WARMUP {
|
||||
return false;
|
||||
}
|
||||
let gpu_faster = match (s.gpu_ms, s.cpu_ms) {
|
||||
(Some(g), Some(c)) => c >= g * Self::MARGIN,
|
||||
(g, _) => g.is_some(),
|
||||
};
|
||||
// Periodically re-measure the slower side.
|
||||
if s.since_probe >= Self::REPROBE {
|
||||
s.since_probe = 0;
|
||||
return !gpu_faster;
|
||||
}
|
||||
s.since_probe += 1;
|
||||
gpu_faster
|
||||
}
|
||||
|
||||
/// A frame for `key` took `ms` on the GPU (`gpu`) or the CPU.
|
||||
///
|
||||
/// The warm-up keeps the fastest of its counted frames; after that the average follows
|
||||
/// improvements quickly and slow-downs gradually, and a single frame counts at most twice
|
||||
/// the average (a stall of the machine does not flip the choice for a whole re-probe
|
||||
/// period).
|
||||
pub fn record(&self, key: AutoKey, gpu: bool, ms: f64) {
|
||||
let Ok(mut m) = self.stats.lock() else { return };
|
||||
let s = m.entry(key).or_default();
|
||||
let (avg, frames) = if gpu { (&mut s.gpu_ms, &mut s.gpu_frames) } else { (&mut s.cpu_ms, &mut s.cpu_frames) };
|
||||
*frames = frames.saturating_add(1);
|
||||
if gpu {
|
||||
s.declined = false;
|
||||
}
|
||||
if *frames < 2 {
|
||||
return;
|
||||
}
|
||||
*avg = Some(match *avg {
|
||||
None => ms,
|
||||
Some(a) if *frames <= Self::WARMUP => a.min(ms),
|
||||
Some(a) if ms < a => a + (ms - a) * Self::ALPHA_DOWN,
|
||||
Some(a) => a + (ms.min(2.0 * a) - a) * Self::ALPHA,
|
||||
});
|
||||
}
|
||||
|
||||
/// The GPU did not handle a frame for `key` (it rendered on the CPU instead).
|
||||
pub fn declined(&self, key: AutoKey) {
|
||||
if let Ok(mut m) = self.stats.lock() {
|
||||
let s = m.entry(key).or_default();
|
||||
s.declined = true;
|
||||
s.gpu_ms = None;
|
||||
s.since_probe = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// The history of `key`.
|
||||
pub fn stats(&self, key: AutoKey) -> Option<AutoStats> {
|
||||
self.stats.lock().ok()?.get(&key).copied()
|
||||
}
|
||||
|
||||
/// Forget all history (settings that change both paths' costs).
|
||||
pub fn reset(&self) {
|
||||
if let Ok(mut m) = self.stats.lock() {
|
||||
m.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -326,10 +326,20 @@ fn has_expression(g: &PropGroup) -> bool {
|
|||
})
|
||||
}
|
||||
|
||||
/// Cache key for an adjustment layer's footprint (its source through its masks, see
|
||||
/// `Renderer::adjustment_footprint`): the GPU compositor then uploads it once, not every frame.
|
||||
pub fn footprint_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool) -> Option<u64> {
|
||||
Some(derive(key_any(ctx, layer, scale, draft, false)?, 0xf007_9417))
|
||||
}
|
||||
|
||||
fn key_with(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, footage: bool) -> Option<u64> {
|
||||
if layer.switches.adjustment {
|
||||
return None;
|
||||
}
|
||||
key_any(ctx, layer, scale, draft, footage)
|
||||
}
|
||||
|
||||
fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, footage: bool) -> Option<u64> {
|
||||
let mut h = KeyHasher(0xcbf2_9ce4_8422_2325);
|
||||
match &layer.source {
|
||||
LayerSource::Solid { item } => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
//! Adjustment layers run their effects on everything below, limited by their own bounds/masks.
|
||||
|
||||
pub mod audio;
|
||||
pub mod auto;
|
||||
pub mod cache;
|
||||
pub mod color;
|
||||
pub mod disk_cache;
|
||||
|
|
@ -20,6 +21,7 @@ pub mod three_d;
|
|||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use auto::{AutoKey, AutoPick};
|
||||
pub use cache::{CacheStats, LayerCache};
|
||||
use effectcraft_color::BlendMode;
|
||||
use effectcraft_effects::{Buf, EffectCtx, EffectEnv, EffectHost, LayerPixels, Params};
|
||||
|
|
@ -289,6 +291,11 @@ pub trait Accelerator: Send + Sync {
|
|||
fn particles(&self) -> Option<&dyn effectcraft_effects::psim::ParticleSim> {
|
||||
None
|
||||
}
|
||||
/// Timing history [`Backend::Auto`] uses to send each comp's top-level frames to the
|
||||
/// faster compositor. `None` = Auto always tries the accelerator first.
|
||||
fn auto_pick(&self) -> Option<&AutoPick> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Where an effect stack runs (see [`Renderer::run_effects_on`]): a CPU buffer, or an image
|
||||
|
|
@ -620,16 +627,43 @@ impl<'a> Renderer<'a> {
|
|||
/// Render a composition at comp time `t` (transparent background, comp size × scale).
|
||||
/// Top-level frames go to the GPU when [`Self::active_accel`] is set and handles them.
|
||||
pub fn comp_frame(&self, comp_id: ItemId, t: Tick) -> Image {
|
||||
// Classic 3D runs composite on the GPU too (M12.7), so Auto sends 3D comps there as well.
|
||||
// Classic 3D runs composite on the GPU too (M12.7), so Auto sends 3D comps there as well;
|
||||
// Auto renders each comp on whichever compositor measured faster ([`AutoPick`]).
|
||||
if self.depth == 0
|
||||
&& let Some(a) = self.active_accel()
|
||||
&& let Some(img) = a.comp_frame(self, comp_id, t)
|
||||
{
|
||||
let auto = self.frame_auto(a, comp_id, false);
|
||||
if auto.is_none_or(|(p, key)| p.choose(key)) {
|
||||
let t0 = web_time::Instant::now();
|
||||
if let Some(img) = a.comp_frame(self, comp_id, t) {
|
||||
if let Some((p, key)) = auto {
|
||||
p.record(key, true, t0.elapsed().as_secs_f64() * 1e3);
|
||||
}
|
||||
return img;
|
||||
}
|
||||
if let Some((p, key)) = auto {
|
||||
p.declined(key);
|
||||
}
|
||||
}
|
||||
let t0 = web_time::Instant::now();
|
||||
let img = self.comp_frame_cpu(comp_id, t);
|
||||
if let Some((p, key)) = auto {
|
||||
p.record(key, false, t0.elapsed().as_secs_f64() * 1e3);
|
||||
}
|
||||
return img;
|
||||
}
|
||||
self.comp_frame_cpu(comp_id, t)
|
||||
}
|
||||
|
||||
/// The Auto timing history and key for a top-level frame of `comp` (`None` unless the
|
||||
/// backend is [`Backend::Auto`] and the accelerator keeps history).
|
||||
pub fn frame_auto(&self, a: &'a dyn Accelerator, comp: ItemId, display: bool) -> Option<(&'a AutoPick, AutoKey)> {
|
||||
if self.opts.backend != Backend::Auto || self.opts.roi.is_some() {
|
||||
return None;
|
||||
}
|
||||
Some((a.auto_pick()?, AutoKey::new(comp, self.opts.scale, display)))
|
||||
}
|
||||
|
||||
/// [`Self::comp_frame`] on the CPU compositor only.
|
||||
pub fn comp_frame_cpu(&self, comp_id: ItemId, t: Tick) -> Image {
|
||||
let Some(comp) = self.project.comp(comp_id) else { return Image::new(1, 1) };
|
||||
|
|
@ -1606,9 +1640,20 @@ impl<'a> Renderer<'a> {
|
|||
|
||||
/// 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<Buf> {
|
||||
pub fn adjustment_footprint(&self, ctx: &EvalCtx, layer: &Layer) -> Option<Arc<Buf>> {
|
||||
// Cached, so an accelerator sees the same buffer (and uploads it once) every frame.
|
||||
let key = self.cache.and_then(|_| cache::footprint_key(ctx, layer, self.raster_scale(ctx, layer), self.opts.draft));
|
||||
if let (Some(c), Some(k)) = (self.cache, key)
|
||||
&& let Some(b) = c.get(k)
|
||||
{
|
||||
return Some(b);
|
||||
}
|
||||
let mut foot = self.source(ctx, layer)?;
|
||||
let _ = masks::apply(ctx, layer, &mut foot);
|
||||
let foot = Arc::new(foot);
|
||||
if let (Some(c), Some(k)) = (self.cache, key) {
|
||||
c.insert(k, foot.clone());
|
||||
}
|
||||
Some(foot)
|
||||
}
|
||||
|
||||
|
|
@ -1761,6 +1806,8 @@ pub fn render_frame(project: &Project, comp: ItemId, t: Tick, scale: f64) -> Ima
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_auto;
|
||||
#[cfg(test)]
|
||||
mod tests_remap;
|
||||
#[cfg(test)]
|
||||
mod tests_roi;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
//! Backend::Auto's per-comp CPU / GPU choice (`auto.rs`), with a mock accelerator.
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use effectcraft_color::Label;
|
||||
use effectcraft_project::build;
|
||||
use effectcraft_project::{Comp, ItemId, ItemKind, LayerSource, Project, Solid};
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
|
||||
use crate::{Accelerator, AutoKey, AutoPick, Backend, NoFootage, RenderOpts, Renderer};
|
||||
|
||||
/// An accelerator that takes `delay` per frame (or declines with `handle = false`) and keeps
|
||||
/// Auto timing history.
|
||||
struct TimedAccel {
|
||||
delay: std::time::Duration,
|
||||
handle: bool,
|
||||
calls: AtomicUsize,
|
||||
pick: AutoPick,
|
||||
}
|
||||
|
||||
impl TimedAccel {
|
||||
fn new(ms: u64, handle: bool) -> TimedAccel {
|
||||
TimedAccel { delay: std::time::Duration::from_millis(ms), handle, calls: Default::default(), pick: Default::default() }
|
||||
}
|
||||
fn calls(&self) -> usize {
|
||||
self.calls.load(Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
||||
impl Accelerator for TimedAccel {
|
||||
fn name(&self) -> String {
|
||||
"timed".into()
|
||||
}
|
||||
fn comp_frame(&self, _r: &Renderer, _comp: ItemId, _t: Tick) -> Option<crate::Image> {
|
||||
self.calls.fetch_add(1, Ordering::SeqCst);
|
||||
std::thread::sleep(self.delay);
|
||||
// The centre of the comp, as the CPU draws it (a red solid).
|
||||
self.handle.then(|| {
|
||||
let mut img = crate::Image::new(200, 100);
|
||||
img.set(100, 50, [1.0, 0.0, 0.0, 1.0]);
|
||||
img
|
||||
})
|
||||
}
|
||||
fn supports_effect(&self, _id: &str) -> bool {
|
||||
false
|
||||
}
|
||||
fn effects(&self, _chain: &[crate::FxStep], _buf: &crate::Buf, _levels: Option<f32>) -> Option<crate::Buf> {
|
||||
None
|
||||
}
|
||||
fn auto_pick(&self) -> Option<&AutoPick> {
|
||||
Some(&self.pick)
|
||||
}
|
||||
}
|
||||
|
||||
fn project() -> (Project, ItemId) {
|
||||
let mut p = Project::default();
|
||||
p.settings.gpu_acceleration = true;
|
||||
let mut comp = Comp::new(200, 100, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let sid = p.add_item("Solid", Label::Red, None, ItemKind::Solid(Solid { color: [1.0, 0.0, 0.0], width: 50, height: 50, pixel_aspect: 1.0 }));
|
||||
let l = build::layer(&mut p, &comp, "Solid", LayerSource::Solid { item: sid }, (50, 50), None);
|
||||
comp.layers.push(l);
|
||||
let cid = p.add_item("Comp", Label::Sandstone, None, ItemKind::Comp(comp.into()));
|
||||
(p, cid)
|
||||
}
|
||||
|
||||
/// Render `n` frames with `backend`; how many the accelerator was asked for.
|
||||
fn frames(p: &Project, cid: ItemId, a: &TimedAccel, backend: Backend, n: usize) -> usize {
|
||||
let before = a.calls();
|
||||
for _ in 0..n {
|
||||
let mut r = Renderer::new(p, &NoFootage, RenderOpts { backend, ..Default::default() });
|
||||
r.accel = Some(a);
|
||||
let img = r.comp_frame(cid, Tick::ZERO);
|
||||
// Whichever compositor ran, the frame is the same.
|
||||
assert!(img.get(100, 50)[0] > 0.99 && img.get(100, 50)[3] > 0.99);
|
||||
}
|
||||
a.calls() - before
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_backend_moves_light_comps_to_the_cpu() {
|
||||
// A light comp on a slow accelerator: after the warm-up (three frames on
|
||||
// each side) Auto renders on the CPU, re-measuring the accelerator now and then.
|
||||
let (p, cid) = project();
|
||||
// Several times the CPU's frame time (unoptimised test builds are slow).
|
||||
let t0 = std::time::Instant::now();
|
||||
for _ in 0..3 {
|
||||
Renderer::new(&p, &NoFootage, RenderOpts::default()).comp_frame(cid, Tick::ZERO);
|
||||
}
|
||||
let cpu_ms = t0.elapsed().as_millis() as u64 / 3;
|
||||
let slow = TimedAccel::new((cpu_ms * 5).max(20), true);
|
||||
let n = 6 + 2 * AutoPick::REPROBE as usize;
|
||||
let asked = frames(&p, cid, &slow, Backend::Auto, n);
|
||||
assert!((3..=5).contains(&asked), "asked the slow accelerator {asked} times in {n} frames");
|
||||
let s = slow.pick.stats(AutoKey::new(cid, 1.0, false)).unwrap();
|
||||
assert!(s.gpu_ms.unwrap() > s.cpu_ms.unwrap(), "{s:?}");
|
||||
// Backend::Gpu ignores the history; Software Only never asks.
|
||||
assert_eq!(frames(&p, cid, &slow, Backend::Gpu, 3), 3);
|
||||
let mut sw = p.clone();
|
||||
sw.settings.gpu_acceleration = false;
|
||||
assert_eq!(frames(&sw, cid, &slow, Backend::Auto, 3), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_backend_keeps_fast_accelerators() {
|
||||
// As fast as the CPU (ties stay on the accelerator): every frame but the CPU warm-up and
|
||||
// re-probes.
|
||||
let (p, cid) = project();
|
||||
let fast = TimedAccel::new(0, true);
|
||||
let asked = frames(&p, cid, &fast, Backend::Auto, 40);
|
||||
assert!(asked >= 36, "asked {asked} of 40");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_backend_falls_back_when_the_accelerator_declines() {
|
||||
let (p, cid) = project();
|
||||
let declining = TimedAccel::new(0, false);
|
||||
let n = 2 * AutoPick::REPROBE as usize;
|
||||
let asked = frames(&p, cid, &declining, Backend::Auto, n);
|
||||
// Asked first, then only on the periodic re-probes.
|
||||
assert!((1..=4).contains(&asked), "asked {asked} times in {n} frames");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_pick_follows_changing_costs() {
|
||||
let pick = AutoPick::default();
|
||||
let key = AutoKey::new(ItemId(7), 0.5, true);
|
||||
let cpu = 5.0;
|
||||
let mut gpu = 10.0;
|
||||
let (mut early, mut late) = (0, 0);
|
||||
for frame in 0..400 {
|
||||
// The GPU gets faster halfway (say, its uploads became cached).
|
||||
if frame == 200 {
|
||||
gpu = 1.0;
|
||||
}
|
||||
let g = pick.choose(key);
|
||||
pick.record(key, g, if g { gpu } else { cpu });
|
||||
if (100..200).contains(&frame) && g {
|
||||
early += 1;
|
||||
}
|
||||
if frame >= 300 && g {
|
||||
late += 1;
|
||||
}
|
||||
}
|
||||
assert!(early <= 3, "{early} of 100 frames on the slower GPU");
|
||||
assert!(late >= 97, "{late} of 100 frames on the faster GPU");
|
||||
let s = pick.stats(key).unwrap();
|
||||
assert!(s.gpu_ms.unwrap() < 2.0 && s.cpu_ms.unwrap() > 4.0, "{s:?}");
|
||||
// Keys are separate per comp, scale and path.
|
||||
assert!(pick.stats(AutoKey::new(ItemId(7), 1.0, true)).is_none());
|
||||
assert!(pick.stats(AutoKey::new(ItemId(7), 0.5, false)).is_none());
|
||||
}
|
||||
|
|
@ -589,14 +589,36 @@ impl Worker {
|
|||
r.accel = job.src.gpu.as_ref().map(|g| g as &dyn effectcraft_engine::render::Accelerator);
|
||||
// The GPU leaves the frame in a texture for the viewer; otherwise (Software Only, no
|
||||
// adapter, or a frame the GPU cannot finish here) the CPU renders it.
|
||||
let gpu_frame = match (&job.src.gpu, r.active_accel()) {
|
||||
(Some(g), Some(_)) if job.src.gpu_display => g.render_display(&r, job.comp, job.t),
|
||||
_ => None,
|
||||
};
|
||||
match gpu_frame {
|
||||
Some(f) => FrameImage::Gpu(Arc::new(f)),
|
||||
None => FrameImage::Cpu(Arc::new(to_color_image(&r.comp_frame(job.comp, job.t)))),
|
||||
// Auto (Mercury GPU Acceleration) shows each comp on whichever compositor measured
|
||||
// faster for it (light comps composite faster on the CPU).
|
||||
if let (Some(g), Some(a)) = (&job.src.gpu, r.active_accel())
|
||||
&& job.src.gpu_display
|
||||
{
|
||||
// The browser cannot wait for the GPU to time it (and its CPU path is slow): always
|
||||
// the GPU there.
|
||||
let auto = if cfg!(target_arch = "wasm32") { None } else { r.frame_auto(a, job.comp, true) };
|
||||
if auto.is_none_or(|(p, key)| p.choose(key)) {
|
||||
let t0 = web_time::Instant::now();
|
||||
if let Some(f) = g.render_display(&r, job.comp, job.t) {
|
||||
if let Some((p, key)) = auto {
|
||||
// Time the GPU's work, not just its recording.
|
||||
g.wait();
|
||||
p.record(key, true, t0.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
return FrameImage::Gpu(Arc::new(f));
|
||||
}
|
||||
if let Some((p, key)) = auto {
|
||||
p.declined(key);
|
||||
}
|
||||
}
|
||||
let t0 = web_time::Instant::now();
|
||||
let img = to_color_image(&r.comp_frame_cpu(job.comp, job.t));
|
||||
if let Some((p, key)) = auto {
|
||||
p.record(key, false, t0.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
return FrameImage::Cpu(Arc::new(img));
|
||||
}
|
||||
FrameImage::Cpu(Arc::new(to_color_image(&r.comp_frame(job.comp, job.t))))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -161,8 +161,14 @@ Half, Third and Quarter resolution render proportionally fewer pixels end to end
|
|||
Acceleration, the default, or Mercury Software Only; `render.backend`). The CPU renderer is the
|
||||
reference and keeps rendering layer content (sources, masks, CPU effects, layer styles) into the
|
||||
layer cache. The render crate defines an `Accelerator` trait; `effectcraft_gpu::Gpu` implements it
|
||||
on wgpu compute shaders. `RenderOpts::backend` picks `Cpu`, `Gpu` or `Auto` (GPU when an
|
||||
accelerator is attached and the project's renderer is the GPU). A GPU frame walks the comp like
|
||||
on wgpu compute shaders. `RenderOpts::backend` picks `Cpu`, `Gpu` or `Auto` (an accelerator
|
||||
attached and the project's renderer the GPU). Auto renders each comp's top-level frames on
|
||||
whichever compositor measured faster for it (`render::auto::AutoPick`, kept by the accelerator):
|
||||
per comp, output scale and path (readback or viewer display) it averages CPU and GPU frame times
|
||||
(warm-up on both sides, the fastest warm-up frame starting the average, single stalls capped),
|
||||
picks the GPU unless the CPU is clearly faster, and re-measures the other side every 48 frames.
|
||||
Light comps (a few small layers, which the CPU composites only within their bounds) then stay on
|
||||
the CPU instead of paying for full-frame passes and a readback. A GPU frame walks the comp like
|
||||
`draw_comp`: cached layer buffers are uploaded once per buffer, then transformed with the CPU's
|
||||
sampling (nearest, bilinear, Catmull-Rom bicubic, the same minification pre-filter), motion-blur
|
||||
sub-samples are accumulated, and track mattes, Preserve Transparency, layer style passes,
|
||||
|
|
@ -184,16 +190,23 @@ GPU canvas; only the depth of field reads back its 8-byte radius range. Advanced
|
|||
blend modes, track mattes or Preserve Transparency and environment backgrounds take the CPU's
|
||||
2D compositing path (their scenes still render through `render_3d`). Wireframe outlines draw on
|
||||
the GPU from the CPU's pixel list (`Renderer::wireframe_pixels`). GPU
|
||||
effects (`effects::GPU_EFFECTS`, 152 of them: blurs, colour correction, keying incl. Key Light,
|
||||
mattes, channel, stylize, distortion, transitions, generators, noise, grain and time; see
|
||||
effects (`effects::GPU_EFFECTS`, 166 of them: blurs, colour correction, keying incl. Key Light,
|
||||
mattes, channel, stylize, distortion and warps (Warp, Bezier Warp, Smear, Reshape, CC Bend It, CC
|
||||
Page Turn), Cartoon, bevels, shapes, transitions, generators, noise, grain and time; see
|
||||
[effects.md](effects.md)) repeat the CPU effect's steps (padding, box radii, parameters, hashes)
|
||||
as kernels, in one module per family (`gpu::fx_*` with `shaders/fx_*.wgsl`); consecutive GPU
|
||||
effects run as one chain with one upload and one readback. Statistics that need the whole frame
|
||||
(Auto Levels / Contrast / Color, Equalize, Shadow/Highlight, Color Stabilizer, Remove Grain's
|
||||
noise level) are measured on the CPU from one readback and applied on the GPU; effects reading
|
||||
other frames (Echo, Posterize Time) upload the frames the host renders. Settings a kernel cannot
|
||||
match render on the CPU (`effects::catalog::gpu_supported`). Tests render scenes on both paths and compare them
|
||||
(≤ 1/255 at 8 bpc, ≤ 1e-3 at 32 bpc); they skip without an adapter. The desktop viewer builds the
|
||||
match render on the CPU (`effects::catalog::gpu_supported`; e.g. Warp's Fisheye and Twist past
|
||||
50 % or with a distortion, whose Newton inverse wanders chaotically near the crease). Tests render
|
||||
scenes on both paths and compare them (≤ 1/255 at 8 bpc, ≤ 1e-3 at 32 bpc); they skip without an
|
||||
adapter. Working textures (premultiplied RGBA f32) come from a pool: a released texture is reused
|
||||
once every encoder that could still read it has been submitted, transparent inputs share one
|
||||
zero texture per size, readback staging buffers are reused, and the 8/16 bpc quantisation after
|
||||
each layer is fused into the layer's composite kernel; under test, pooled scratch images start
|
||||
as NaNs (a kernel that skips pixels fails the oracle) and validation errors panic. The desktop viewer builds the
|
||||
`Gpu` on egui-wgpu's device and shows frames from GPU textures without reading them back
|
||||
(`ui-egui::frames`); headless renders, the CLI (unless `--gpu`) and CI use the CPU. On the web
|
||||
(WebGPU) the GPU composites viewer frames; steps that need a readback fall back to the CPU (the
|
||||
|
|
@ -203,8 +216,9 @@ Playground cannons without interacting forces evolve every particle independentl
|
|||
backend (reached through `EffectHost::particles` when the GPU compositor is active) simulates one
|
||||
particle per invocation from the CPU's birth schedule and keeps per-key state checkpoints on the
|
||||
GPU, like `SimCache`; the CPU simulation stays the oracle (tests compare ids and positions).
|
||||
`effectcraft-cli bench --gpu` reports CPU vs GPU ms/frame, speed-up and pixel agreement for every
|
||||
comp plus an adjustment-layer comp.
|
||||
`effectcraft-cli bench --gpu` reports CPU vs GPU ms/frame, speed-up, Auto's ms/frame, pixel
|
||||
agreement and the steady-state upload / readback MB per frame for every comp plus an
|
||||
adjustment-layer comp.
|
||||
|
||||
**Colour and bit depth** (`crates/render/src/color.rs`, `crates/color/src/space.rs`). Pixels are
|
||||
`f32`, but 8 and 16 bpc projects clamp and quantise each layer after its source and masks and
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
EffectCraft ships 306 effects with 3009 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over.
|
||||
|
||||
- **GPU**: 152 effects also run on the GPU compositor with identical results.
|
||||
- **GPU**: 166 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 306 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
- **Status**: 306 implemented in full, 0 partial (what is missing is listed).
|
||||
|
||||
|
|
@ -130,15 +130,15 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Bezier Warp | `ec.distort.bezierwarp` | 13 | | 32 | Implemented |
|
||||
| Bezier Warp | `ec.distort.bezierwarp` | 13 | GPU | 32 | Implemented |
|
||||
| Bulge | `ec.distort.bulge` | 6 | GPU | 32 | Implemented |
|
||||
| CC Bend It | `ec.distort.ccbendit` | 4 | | 32 | Implemented |
|
||||
| CC Bend It | `ec.distort.ccbendit` | 4 | GPU | 32 | Implemented |
|
||||
| CC Bender | `ec.distort.ccbender` | 5 | | 32 | Implemented |
|
||||
| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | | 32 | Implemented |
|
||||
| CC Flo Motion | `ec.distort.ccflomotion` | 7 | GPU | 32 | Implemented |
|
||||
| CC Griddler | `ec.distort.ccgriddler` | 5 | GPU | 32 | Implemented |
|
||||
| CC Lens | `ec.distort.cclens` | 3 | GPU | 32 | Implemented |
|
||||
| CC Page Turn | `ec.distort.ccpageturn` | 7 | | 32 | Implemented |
|
||||
| CC Page Turn | `ec.distort.ccpageturn` | 7 | GPU | 32 | Implemented |
|
||||
| CC Power Pin | `ec.distort.ccpowerpin` | 10 | GPU | 32 | Implemented |
|
||||
| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | GPU | 32 | Implemented |
|
||||
| CC Slant | `ec.distort.ccslant` | 4 | GPU | 32 | Implemented |
|
||||
|
|
@ -157,16 +157,16 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Optics Compensation | `ec.distort.opticscompensation` | 6 | GPU | 32 | Implemented |
|
||||
| Polar Coordinates | `ec.distort.polar` | 2 | GPU | 32 | Implemented |
|
||||
| Puppet | `ec.distort.puppet` | 1 | | 32 | Implemented |
|
||||
| Reshape | `ec.distort.reshape` | 6 | | 32 | Implemented |
|
||||
| Reshape | `ec.distort.reshape` | 6 | GPU | 32 | Implemented |
|
||||
| Ripple | `ec.distort.ripple` | 7 | GPU | 32 | Implemented |
|
||||
| Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Implemented |
|
||||
| Smear | `ec.distort.smear` | 8 | | 32 | Implemented |
|
||||
| Smear | `ec.distort.smear` | 8 | GPU | 32 | Implemented |
|
||||
| Spherize | `ec.distort.spherize` | 2 | GPU | 32 | Implemented |
|
||||
| Transform | `ec.distort.transform` | 12 | GPU | 32 | Implemented |
|
||||
| Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | GPU | 32 | Implemented |
|
||||
| Twirl | `ec.distort.twirl` | 3 | GPU | 32 | Implemented |
|
||||
| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | GPU | 32 | Implemented |
|
||||
| Warp | `ec.distort.warp` | 5 | | 32 | Implemented |
|
||||
| Warp | `ec.distort.warp` | 5 | GPU | 32 | Implemented |
|
||||
| Warp Stabilizer | `ec.distort.warpstabilizer` | 20 | | 32 | Implemented |
|
||||
| Wave Warp | `ec.distort.wavewarp` | 7 | GPU | 32 | Implemented |
|
||||
|
||||
|
|
@ -200,8 +200,8 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| CC Threads | `ec.generate.ccthreads` | 8 | | 32 | Implemented |
|
||||
| Cell Pattern | `ec.generate.cellpattern` | 14 | GPU | 32 | Implemented |
|
||||
| Checkerboard | `ec.generate.checkerboard` | 10 | GPU | 32 | Implemented |
|
||||
| Circle | `ec.generate.circle` | 11 | | 32 | Implemented |
|
||||
| Ellipse | `ec.generate.ellipse` | 8 | | 32 | Implemented |
|
||||
| Circle | `ec.generate.circle` | 11 | GPU | 32 | Implemented |
|
||||
| Ellipse | `ec.generate.ellipse` | 8 | GPU | 32 | Implemented |
|
||||
| Eyedropper Fill | `ec.generate.eyedropperfill` | 5 | | 32 | Implemented |
|
||||
| Fill | `ec.generate.fill` | 7 | GPU | 32 | Implemented |
|
||||
| Fractal | `ec.generate.fractal` | 21 | | 32 | Implemented |
|
||||
|
|
@ -285,7 +285,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Basic 3D | `ec.obsolete.basic3d` | 5 | | 32 | Implemented |
|
||||
| Basic Text | `ec.obsolete.basictext` | 11 | | 32 | Implemented |
|
||||
| Color Key | `ec.key.colorkey` | 4 | GPU | 32 | Implemented |
|
||||
| Gaussian Blur (Legacy) | `ec.obsolete.gaussianlegacy` | 2 | | 32 | Implemented |
|
||||
| Gaussian Blur (Legacy) | `ec.obsolete.gaussianlegacy` | 2 | GPU | 32 | Implemented |
|
||||
| Lightning | `ec.obsolete.lightning` | 25 | | 32 | Implemented |
|
||||
| Luma Key | `ec.key.luma` | 5 | GPU | 32 | Implemented |
|
||||
| Path Text | `ec.obsolete.pathtext` | 32 | | 32 | Implemented |
|
||||
|
|
@ -304,8 +304,8 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
|---|---|---:|:-:|:-:|---|
|
||||
| 3D Camera Tracker | `ec.perspective.cameratracker` | 12 | | 32 | Implemented |
|
||||
| 3D Glasses | `ec.perspective.3dglasses` | 8 | | 32 | Implemented |
|
||||
| Bevel Alpha | `ec.perspective.bevelalpha` | 4 | | 32 | Implemented |
|
||||
| Bevel Edges | `ec.perspective.beveledges` | 4 | | 32 | Implemented |
|
||||
| Bevel Alpha | `ec.perspective.bevelalpha` | 4 | GPU | 32 | Implemented |
|
||||
| Bevel Edges | `ec.perspective.beveledges` | 4 | GPU | 32 | Implemented |
|
||||
| CC Cylinder | `ec.perspective.cccylinder` | 12 | | 32 | Implemented |
|
||||
| CC Environment | `ec.perspective.ccenvironment` | 4 | | 32 | Implemented |
|
||||
| CC Sphere | `ec.perspective.ccsphere` | 14 | | 32 | Implemented |
|
||||
|
|
@ -352,8 +352,8 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| CC Threshold | `ec.stylize.ccthreshold` | 4 | GPU | 32 | Implemented |
|
||||
| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | GPU | 32 | Implemented |
|
||||
| CC Vignette | `ec.stylize.ccvignette` | 4 | GPU | 32 | Implemented |
|
||||
| Cartoon | `ec.stylize.cartoon` | 12 | | 32 | Implemented |
|
||||
| Color Emboss | `ec.stylize.coloremboss` | 4 | | 32 | Implemented |
|
||||
| Cartoon | `ec.stylize.cartoon` | 12 | GPU | 32 | Implemented |
|
||||
| Color Emboss | `ec.stylize.coloremboss` | 4 | GPU | 32 | Implemented |
|
||||
| Emboss | `ec.stylize.emboss` | 4 | GPU | 32 | Implemented |
|
||||
| Find Edges | `ec.stylize.findedges` | 2 | GPU | 32 | Implemented |
|
||||
| Glow | `ec.stylize.glow` | 14 | GPU | 32 | Implemented |
|
||||
|
|
@ -403,7 +403,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | | 32 | Implemented |
|
||||
| Card Wipe | `ec.transition.cardwipe` | 44 | | 32 | Implemented |
|
||||
| Gradient Wipe | `ec.transition.gradientwipe` | 5 | GPU | 32 | Implemented |
|
||||
| Iris Wipe | `ec.transition.iriswipe` | 7 | | 32 | Implemented |
|
||||
| Iris Wipe | `ec.transition.iriswipe` | 7 | GPU | 32 | Implemented |
|
||||
| Linear Wipe | `ec.transition.linearwipe` | 3 | GPU | 32 | Implemented |
|
||||
| Radial Wipe | `ec.transition.radialwipe` | 5 | GPU | 32 | Implemented |
|
||||
| Venetian Blinds | `ec.transition.venetian` | 4 | GPU | 32 | Implemented |
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ How close EffectCraft is to After Effects 2026, feature by feature, and how much
|
|||
| Unweighted | ≈ 94% (half-credit) / 98% (fractions) |
|
||||
| P0 / P1 / P2 | ≈ 96% / 92% / 85% (half-credit); 99% / 97% / 96% (fractions) |
|
||||
| Features done / partial / missing | 80 / 12 / 0 of 92 |
|
||||
| **Effects** | **298 of 298** After Effects 2026 effects exist; 42 still simplified ([effects.md](effects.md)); 152 run on the GPU |
|
||||
| **Effects** | **298 of 298** After Effects 2026 effects exist; 42 still simplified ([effects.md](effects.md)); 166 run on the GPU |
|
||||
| Disabled menu entries left | 0 (the stub list is empty) |
|
||||
| Remaining work | ≈ 19–22 agent-hours at the pace measured so far (≈ 58 on the conservative audit scale) |
|
||||
| **Wall-clock estimate** | **≈ 4–5 hours** with five agents in parallel; ≈ 3 hours for 100% of P0 + P1 |
|
||||
|
|
@ -136,11 +136,11 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
|||
| Text | ≈ 96% | 0.3 | no extruded strokes (M13.12: the Variable Font Axes animator re-spaces the text — advances follow the animated axes; M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
|
||||
| Web | 93% | 0.5 | GPU effects in the browser (WebGPU compositing only on the page), no disk cache, no shared-memory threads inside one engine instance (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10) |
|
||||
| 3D | 88% | 5.5 | multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 |
|
||||
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (152 run on the GPU since M13.9, EFF-5: keying, mattes, channel, colour correction, stylize, distort, noise, blurs and time effects joined the 58 of M12.7; Warp, Bezier Warp, Reshape, Smear, Cartoon, CC Page Turn and others still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (166 run on the GPU since M13.13, EFF-5: Warp, Bezier Warp, Reshape, Smear, CC Bend It, CC Page Turn, Cartoon, Color Emboss, Circle, Ellipse, Iris Wipe, Bevel Alpha / Edges and Gaussian Blur (Legacy) joined the 152 of M13.9; simulations, 3D channel, VR, audio-driven, OCIO, text and the CC light / transition families still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||
| Interface | 74% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
|
||||
| Project | ≈ 68% | 5.5 | auto-save, folder moves, OCIO displays beyond the built-in tone map (Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) |
|
||||
| Masks & roto | 74% | 5.0 | Roto Brush's learned (3.0) segmentation model (variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
|
||||
| Preview | 77% | 2.8 | GPU compositing of Advanced 3D layers with blend modes / track mattes and of environment backgrounds (Advanced 3D runs — raster, motion blur, iris depth of field, compositing — and wireframes run on the GPU since M13.11: CPU warm 3394 → GPU warm 290 ms/frame at 1920×1080 (11.7×; Half 968 → 85 ms), GPU ≠ CPU on 0.005 % of pixels, on an M4 Pro under load; Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) |
|
||||
| Preview | 77% | 2.8 | GPU compositing of Advanced 3D layers with blend modes / track mattes and of environment backgrounds (pooled GPU textures and fused quantisation since M13.13: Lower Third GPU warm 21 → 4 ms/frame, and Auto now picks the CPU or the GPU per comp from measured frame times; Advanced 3D runs — raster, motion blur, iris depth of field, compositing — and wireframes run on the GPU since M13.11: CPU warm 3394 → GPU warm 290 ms/frame at 1920×1080 (11.7×; Half 968 → 85 ms), GPU ≠ CPU on 0.005 % of pixels, on an M4 Pro under load; Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) |
|
||||
| Tracking | ≈ 96% | 0.3 | face tracking is a classical (skin model + feature components + shape model) fitter, not a learned detector: profile views and occluded faces are weak; Rolling Shutter Ripple is approximated by Subspace Warp's mesh density (face tracking (Outline Only / Detailed Features with Face Track Points and Extract & Copy Face Measurements), Subspace Warp's content-preserving mesh warp on subspace-smoothed trajectories, and radial lens distortion (k1, k2) in the camera bundle adjustment with Undistort Footage landed in M13.3; Rolling Shutter Repair in M9.11; point tracker, mask tracking, Warp Stabilizer and the 3D Camera Tracker in M6.x / M12.5 / M12.6) |
|
||||
| Paint | 0% | 4.0 | Brush, Clone Stamp, Eraser |
|
||||
|
||||
|
|
@ -204,6 +204,33 @@ axes since M13.6; Advanced 3D's DOF has iris shapes since M13.8).
|
|||
as real geometry lit by the parent (2D collapsed precomps draw nested 3D layers with the
|
||||
parent's renderer); text and shape strokes extrude as bevelled meshes in paint order.
|
||||
|
||||
## Update: M13.13 GPU performance and the remaining GPU ports
|
||||
|
||||
- **Small comps on the GPU (PRV-3)**: the GPU frame allocated (and zero-initialised) several
|
||||
full-frame RGBA f32 textures per layer; working textures now come from a pool (reused once
|
||||
every encoder that could read them is submitted), transparent inputs share one zero texture,
|
||||
readback staging buffers are reused and the 8/16 bpc quantisation after each layer is fused
|
||||
into the layer's composite. `bench --gpu` on an M4 Pro under load (both binaries run back to
|
||||
back, median of 15): Lower Third 1920×1080 GPU warm 21.0 → 4.1 ms/frame (viewer 11.1 → 2.6;
|
||||
Half 10.1 → 3.1), EffectCraft Intro 27.5 → 7.8 (viewer 21.7 → 3.6), 3D Showcase ≈ 95 → ≈ 60
|
||||
(viewer ≈ 80 → ≈ 40), Adjustment Layers 93 → 36. In steady state the only transfer left is
|
||||
the frame's own readback (0.0 MB uploads per frame on every demo comp; the adjustment
|
||||
footprint is now cached and uploaded once).
|
||||
- **Auto picks per comp**: `Backend::Auto` (Mercury GPU Acceleration) times each comp on both
|
||||
compositors (`render::AutoPick`: per comp, scale and path; warm-up, capped stalls, re-probe
|
||||
every 48 frames) and renders on the faster one, in renders and in the viewer. A light comp
|
||||
stays on the CPU, which composites only the layers' bounds: Lower Third Auto 0.8 ms/frame
|
||||
(CPU warm 1.7, GPU warm 4.1, which is mostly the 32 MB readback).
|
||||
- **GPU ports (EFF-5)**: Warp, Bezier Warp, Reshape, Smear, CC Bend It, CC Page Turn, Cartoon,
|
||||
Color Emboss, Circle, Ellipse, Iris Wipe, Bevel Alpha, Bevel Edges and Gaussian Blur (Legacy)
|
||||
run on the GPU (166 GPU effects); Median, Median (Legacy) and Dust & Scratches take any radius
|
||||
(a sliding 512-bin histogram per row segment above radius 4); CC Power Pin's Perspective
|
||||
below 100 % runs on the GPU. All match the CPU oracle within 1/255 (8 bpc) / 1e-3 (32 bpc) on
|
||||
every tested pixel except: Smear and Reshape allow 0.1 % (f32 point-in-outline tests on
|
||||
boundary pixels; measured 0); Warp's Fisheye and Twist bent past 50 % or with a Horizontal /
|
||||
Vertical Distortion render on the CPU (`gpu_supported`), since their Newton inverse wanders
|
||||
chaotically near the crease and f32 settles on other pixels.
|
||||
|
||||
## Update: M13.11 Advanced 3D on the GPU, Essential Graphics mirrors, ScriptUI paint
|
||||
|
||||
- **Advanced 3D on the GPU (PRV-3, 3D-3)**: a whole Advanced 3D run renders on the device
|
||||
|
|
|
|||
Loading…
Reference in New Issue