mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
M12.2: gpu compositor and gpu effects
New L3 crate effectcraft-gpu: a wgpu compute backend for the per-frame compositing walk (layer transforms with the CPU's nearest/bilinear/bicubic sampling and minification pre-filter, motion-blur accumulation, track mattes, Preserve Transparency, layer style passes and knockout, all 38 blend modes as a WGSL port of color::blend, 8/16 bpc clamp+quantise, colour-space conversions). 3D runs, adjustment and wireframe layers run on the CPU between GPU steps. GPU effects with the CPU semantics: Gaussian, Fast Box and Directional Blur, Glow, Levels, Curves, Hue/Saturation, Tint, Fill, Gradient Ramp, Fractal Noise, Drop Shadow, Brightness & Contrast, Exposure, Invert, Transform (registry gpu badge from effects::GPU_EFFECTS). render: Backend (Cpu/Gpu/Auto) in RenderOpts, the Accelerator trait and Renderer::accel, GPU effect chains in the effect stack, hooks for the GPU walk. Project setting gpu_acceleration (Mercury GPU Acceleration / Software Only), render.backend command, file.projectSettings renderer. Session and Render Queue exports carry the accelerator. CLI --gpu and bench --gpu. Tests compare GPU vs CPU renders (blend modes x bit depths, transforms, masks, mattes, motion blur, colour management, styles, collapse, adjustment fallback, every GPU effect) and skip without an adapter. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ab3b4cfb70
commit
485f562e19
30 files changed
+3625
-62
No files matched your search
@@ -17,6 +17,7 @@ effectcraft-engine = { workspace = true }
|
||||
effectcraft-host = { workspace = true }
|
||||
effectcraft-automation = { workspace = true }
|
||||
effectcraft-render = { workspace = true }
|
||||
effectcraft-gpu = { workspace = true }
|
||||
effectcraft-time = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
image = { workspace = true }
|
||||
|
||||
@@ -13,11 +13,14 @@
|
||||
//! [--work-area] [--fps N] [--resolution full|half|third|quarter|K] [--quality best|draft] [--channels rgb|rgba]
|
||||
//! [--jpeg-quality N] [--bitrate KBPS] [--prores proxy|lt|standard|hq|4444|4444xq] [--audio auto|on|off]
|
||||
//! effectcraft-cli render F.ecproj --queue render the project's Render Queue
|
||||
//! effectcraft-cli bench [--comp C] [--time S] [--scale K] [--n N] [--play N] render timings
|
||||
//! effectcraft-cli bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu] render timings
|
||||
//! (--gpu: CPU vs GPU ms/frame for every comp at Full and Half)
|
||||
//! effectcraft-cli mcp [--bridge PORT] MCP server on stdio
|
||||
//!
|
||||
//! Project: --project F.ecproj (or a positional *.ecproj) | --demo | --empty (default: demo; mcp: empty)
|
||||
//! Saving: --save (back to --project) | --save-as F.ecproj
|
||||
//! GPU: --gpu renders on the GPU compositor (Mercury GPU Acceleration) when an adapter exists;
|
||||
//! the default is the CPU (Mercury Software Only)
|
||||
//! Bridge: --bridge PORT drives a running `effectcraft --control PORT` instead of a headless session
|
||||
//! Output: --json for one compact JSON document on stdout (errors: {"error": …}, exit 1)
|
||||
//! ```
|
||||
@@ -41,10 +44,11 @@ const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|se
|
||||
render [--comp C] --out FILE [--format F] [--start S] [--end S] [--work-area] [--fps N]
|
||||
[--resolution full|half|third|quarter|K] [--quality best|draft] [--channels rgb|rgba]
|
||||
[--jpeg-quality N] [--bitrate KBPS] [--prores PROFILE] [--audio auto|on|off] | --queue
|
||||
bench [--comp C] [--time S] [--scale K] [--n N] [--play N] per-layer/effect render timings;
|
||||
--play N renders N consecutive frames with/without the layer cache
|
||||
bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu] per-layer/effect render timings;
|
||||
--play N renders N consecutive frames with/without the layer cache;
|
||||
--gpu compares CPU and GPU ms/frame for every comp at Full and Half
|
||||
mcp [--bridge PORT] MCP server (JSON-RPC over stdio)
|
||||
options: --project F.ecproj | --demo | --empty --save | --save-as F --bridge PORT --json
|
||||
options: --project F.ecproj | --demo | --empty --save | --save-as F --bridge PORT --json --gpu
|
||||
<comp>: id or name, '-' = active comp; <layer>: id, '#n' or name; <value>: JSON or bare string";
|
||||
|
||||
/// Options that take a value.
|
||||
@@ -196,7 +200,7 @@ fn backend(args: &Args, default_demo: bool) -> Result<Backend, Failure> {
|
||||
}
|
||||
return Ok(Backend::bridge(addr)?);
|
||||
}
|
||||
let mut b = Backend::headless(effectcraft_host::session());
|
||||
let mut b = Backend::headless(session(args)?);
|
||||
if let Some(p) = &args.project {
|
||||
b.exec("file.open", json!({"path": p}))?;
|
||||
} else if args.flag("--demo") || (default_demo && !args.flag("--empty")) {
|
||||
@@ -205,6 +209,17 @@ fn backend(args: &Args, default_demo: bool) -> Result<Backend, Failure> {
|
||||
Ok(b)
|
||||
}
|
||||
|
||||
/// A wired session; `--gpu` attaches the GPU compositor (renders then follow the project's
|
||||
/// renderer setting, Mercury GPU Acceleration by default).
|
||||
fn session(args: &Args) -> Result<effectcraft_engine::Session, Failure> {
|
||||
let mut s = effectcraft_host::session();
|
||||
if args.flag("--gpu") {
|
||||
let g = effectcraft_gpu::Gpu::headless().ok_or_else(|| Failure::Error("--gpu: no usable GPU adapter".into()))?;
|
||||
s.accel = Some(std::sync::Arc::new(g));
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// `--save` / `--save-as` after a mutating command.
|
||||
fn maybe_save(b: &mut Backend, args: &Args) -> Result<Option<String>, Failure> {
|
||||
let r = if let Some(p) = args.opt("--save-as") {
|
||||
@@ -436,7 +451,7 @@ fn render(args: &Args, json_out: bool) -> Result<(), Failure> {
|
||||
if args.opt("--bridge").is_some() {
|
||||
return usage_err("render runs headless; use `exec renderQueue.add` / `renderQueue.render` with --bridge");
|
||||
}
|
||||
let mut s = effectcraft_host::session();
|
||||
let mut s = session(args)?;
|
||||
match &args.project {
|
||||
Some(p) => s.execute("file.open", json!({"path": p})).map_err(|e| Failure::Error(e.to_string()))?,
|
||||
None => s.execute("file.openDemoProject", json!({})).map_err(|e| Failure::Error(e.to_string()))?,
|
||||
@@ -549,6 +564,9 @@ fn bench_cmd(args: &Args) -> Result<(), Failure> {
|
||||
if let Some(c) = args.opt("--comp") {
|
||||
s.execute("comp.open", json!({"comp": c})).map_err(|e| Failure::Error(e.to_string()))?;
|
||||
}
|
||||
if args.flag("--gpu") {
|
||||
return bench_gpu(&s, args);
|
||||
}
|
||||
let cid = s.active_comp_id().ok_or_else(|| Failure::Error("no composition".into()))?;
|
||||
let t = Tick::from_seconds_f64(args.num("--time")?.unwrap_or(3.0));
|
||||
let opts = RenderOpts { scale: args.num("--scale")?.unwrap_or(1.0), ..Default::default() };
|
||||
@@ -664,3 +682,70 @@ fn bench_play(s: &Session, cid: ItemId, t: Tick, opts: RenderOpts, n: usize) {
|
||||
let st = cache.stats();
|
||||
eprintln!(" layer cache: {} hits, {} misses, {} entries, {:.1} MB", st.hits, st.misses, st.entries, st.bytes as f64 / 1e6);
|
||||
}
|
||||
|
||||
/// Median wall time (ms) of `n` runs of `f` after one warm-up run.
|
||||
fn time_ms(n: usize, mut f: impl FnMut()) -> f64 {
|
||||
f();
|
||||
let mut v: Vec<f64> = (0..n)
|
||||
.map(|_| {
|
||||
let t0 = std::time::Instant::now();
|
||||
f();
|
||||
t0.elapsed().as_secs_f64() * 1e3
|
||||
})
|
||||
.collect();
|
||||
median(&mut v)
|
||||
}
|
||||
|
||||
/// `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.
|
||||
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()))?;
|
||||
let n = args.num("--n")?.unwrap_or(10.0).max(1.0) as usize;
|
||||
let comps: Vec<ItemId> = match args.opt("--comp") {
|
||||
Some(_) => s.active_comp_id().into_iter().collect(),
|
||||
None => s.project.comps().map(|(id, _)| *id).collect(),
|
||||
};
|
||||
eprintln!("GPU: {} — median of {n} runs (ms/frame)", effectcraft_render::Accelerator::name(&gpu));
|
||||
eprintln!("{:<28} {:>5} {:>10} {:>9} {:>9} {:>9} {:>9} {:>9}", "comp", "res", "size", "cpu cold", "gpu cold", "cpu warm", "gpu warm", "gpu view");
|
||||
for cid in comps {
|
||||
let Some(comp) = s.project.comp(cid) else { continue };
|
||||
let name = s.project.item(cid).map(|i| i.name.clone()).unwrap_or_default();
|
||||
let t = Tick::from_seconds_f64(args.num("--time")?.unwrap_or(3.0).min(comp.duration.seconds() * 0.5));
|
||||
for (label, scale) in [("Full", 1.0), ("Half", 0.5)] {
|
||||
let mk = |backend: Backend, cache: Option<&LayerCache>| {
|
||||
let mut r = Renderer::new(&s.project, s.footage.as_ref(), RenderOpts { scale, backend, ..Default::default() });
|
||||
r.expr = s.expr.as_deref();
|
||||
r.cache = cache;
|
||||
r.accel = Some(&gpu);
|
||||
r.comp_frame(cid, t)
|
||||
};
|
||||
let cpu_cold = time_ms(n, || {
|
||||
std::hint::black_box(mk(Backend::Cpu, None));
|
||||
});
|
||||
let gpu_cold = time_ms(n, || {
|
||||
std::hint::black_box(mk(Backend::Gpu, None));
|
||||
});
|
||||
let cache = LayerCache::default();
|
||||
let cpu_warm = time_ms(n, || {
|
||||
std::hint::black_box(mk(Backend::Cpu, Some(&cache)));
|
||||
});
|
||||
let gcache = LayerCache::default();
|
||||
let gpu_warm = time_ms(n, || {
|
||||
std::hint::black_box(mk(Backend::Gpu, Some(&gcache)));
|
||||
});
|
||||
let view = time_ms(n, || {
|
||||
let mut r = Renderer::new(&s.project, s.footage.as_ref(), RenderOpts { scale, backend: Backend::Gpu, ..Default::default() });
|
||||
r.expr = s.expr.as_deref();
|
||||
r.cache = Some(&gcache);
|
||||
r.accel = Some(&gpu);
|
||||
std::hint::black_box(gpu.render_display(&r, cid, t));
|
||||
gpu.wait();
|
||||
});
|
||||
let size = format!("{}x{}", (comp.width as f64 * scale).round(), (comp.height as f64 * scale).round());
|
||||
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}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user