mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
M13.13: gpu texture pool, Auto per-comp backend choice, warp-family GPU ports
- gpu: pooled working textures (released once every encoder that could read them is submitted), shared zero images, reused readback staging buffers, direct readback into Image; NaN-poisoned scratch images under test and NaN-aware diffs; validation errors panic under test - render: AutoPick (per comp / scale / path EWMA of CPU vs GPU frame times, warm-up and periodic re-probe) behind Backend::Auto; viewer uses it too; cached adjustment footprints (uploaded once) - gpu fx_warp: Warp, Bezier Warp, CC Bend It, CC Page Turn, Smear, Reshape, Color Emboss, Cartoon; sliding-histogram median for large radii; CC Power Pin perspective < 100 - cli: bench --gpu reports Auto and per-frame upload / readback MB Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f5a136e346
commit
9ff0e5aa3c
@@ -788,7 +788,9 @@ fn adjustment_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option
|
||||
|
||||
/// `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()))?;
|
||||
@@ -796,6 +798,7 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
|
||||
// Every comp, plus an adjustment-layer comp built for the benchmark (the main comp under a
|
||||
// full-frame adjustment layer with a GPU effect stack).
|
||||
let mut project = (*s.project).clone();
|
||||
project.settings.gpu_acceleration = true;
|
||||
let comps: Vec<ItemId> = match args.opt("--comp") {
|
||||
Some(_) => s.active_comp_id().into_iter().collect(),
|
||||
None => {
|
||||
@@ -807,8 +810,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 };
|
||||
@@ -833,9 +836,20 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
|
||||
std::hint::black_box(mk(Backend::Cpu, Some(&cache)));
|
||||
});
|
||||
let gcache = LayerCache::default();
|
||||
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();
|
||||
@@ -852,7 +866,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)
|
||||
|
||||
@@ -72,7 +72,8 @@ pub use color_fx::{
|
||||
};
|
||||
pub use color2::{Curve, auto_correct_settings, equalize_tables, luma_clip_points, shadow_highlight_amounts};
|
||||
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;
|
||||
@@ -912,6 +913,15 @@ 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",
|
||||
];
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
+304
-31
@@ -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,154 @@ 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>>,
|
||||
}
|
||||
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 +243,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 +309,7 @@ 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"),
|
||||
]
|
||||
.concat();
|
||||
let module =
|
||||
@@ -215,6 +367,7 @@ impl GpuContext {
|
||||
.chain(crate::fx_stylize::KERNELS)
|
||||
.chain(crate::fx_noise::KERNELS)
|
||||
.chain(crate::fx_tone::KERNELS)
|
||||
.chain(crate::fx_warp::KERNELS)
|
||||
.map(|e| (e, &layout))
|
||||
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
||||
.map(|(e, layout)| {
|
||||
@@ -274,8 +427,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 +458,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 +477,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 +492,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) }, 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 +528,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 +586,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 +621,50 @@ impl<'g> Enc<'g> {
|
||||
|
||||
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 +761,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 +817,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,7 @@ 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),
|
||||
_ => pointwise(e, id, ctx, b),
|
||||
}
|
||||
}
|
||||
@@ -162,7 +163,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 +226,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 +259,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 +286,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 +339,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 +465,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)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+21
-12
@@ -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)
|
||||
}
|
||||
+11
-2
@@ -42,13 +42,14 @@ 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;
|
||||
@@ -62,6 +63,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`),
|
||||
@@ -86,7 +89,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 {
|
||||
@@ -158,6 +161,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 {
|
||||
@@ -189,4 +196,6 @@ mod tests_fx_stylize;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_tone;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_warp;
|
||||
#[cfg(test)]
|
||||
mod tests_particles;
|
||||
|
||||
+10
-10
@@ -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
|
||||
}
|
||||
@@ -81,7 +81,7 @@ pub fn warp(
|
||||
p.f[2] = r[2];
|
||||
p.f[3] = [opacity, accumulate.unwrap_or(1.0), 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 out = e.scratch(dst.width, dst.height);
|
||||
e.pixels("warp_blend", &p, &img, Some(dst), &out, None);
|
||||
out
|
||||
}
|
||||
@@ -91,7 +91,7 @@ pub fn blend_full(e: &mut Enc, dst: &GpuImage, src: &GpuImage, mode: BlendMode,
|
||||
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);
|
||||
let out = e.scratch(dst.width, dst.height);
|
||||
e.pixels("blend_full", &p, src, Some(dst), &out, None);
|
||||
out
|
||||
}
|
||||
@@ -105,21 +105,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 +129,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,7 +140,7 @@ 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
|
||||
}
|
||||
@@ -149,7 +149,7 @@ pub fn adjust_mix(e: &mut Enc, canvas: &GpuImage, adjusted: &GpuImage, matte: &G
|
||||
pub fn quantize(e: &mut Enc, img: &GpuImage, levels: f32) -> GpuImage {
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = levels;
|
||||
let out = e.image(img.width, img.height);
|
||||
let out = e.scratch(img.width, img.height);
|
||||
e.pixels("quantize", &p, img, None, &out, None);
|
||||
out
|
||||
}
|
||||
@@ -174,7 +174,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
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -959,3 +959,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]);
|
||||
}
|
||||
|
||||
+10
-3
@@ -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);
|
||||
|
||||
@@ -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() {
|
||||
@@ -127,7 +127,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)
|
||||
}
|
||||
@@ -156,7 +156,7 @@ 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)?;
|
||||
}
|
||||
@@ -165,7 +165,7 @@ fn draw_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &
|
||||
}
|
||||
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)?;
|
||||
tmp = ops::knockout(e, &tmp, &k);
|
||||
}
|
||||
for (b, m) in &st.passes {
|
||||
@@ -183,7 +183,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)?;
|
||||
let pipe = r.pipe();
|
||||
// Effects run in the working space, not the linear blending space.
|
||||
let below = match pipe.from_blend() {
|
||||
@@ -210,14 +210,14 @@ fn composite_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, buf:
|
||||
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)?;
|
||||
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)
|
||||
{
|
||||
@@ -243,7 +243,7 @@ fn place(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, buf: &Arc<Buf>
|
||||
if let [m] = pl.matrices.as_slice() {
|
||||
return Some(ops::warp(e, target, &src, m, pl.sampling, mode, opacity, pl.seed, None));
|
||||
}
|
||||
let mut acc = e.image(target.width, target.height);
|
||||
let 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));
|
||||
|
||||
@@ -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};
|
||||
@@ -283,6 +285,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
|
||||
@@ -627,16 +634,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) };
|
||||
@@ -1580,9 +1614,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)
|
||||
}
|
||||
|
||||
@@ -1735,6 +1780,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());
|
||||
}
|
||||
@@ -437,14 +437,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))))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-9
@@ -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**: 160 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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user