mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 19:18:14 -06:00
M12.1: render cache + parallel compositing fast paths
- render: LayerCache (content-keyed processed-layer cache: source -> masks -> effects, keyed by a hash of the layer's non-transform props evaluated at the frame time, solid item, switches, scale; time folded in for clock-driven effects / wiggle paths). Static and transform-only layers render once; edits only re-render the edited layer. Session + UI frame jobs share it. - effects: TIME_DEPENDENT list + test that every clock-reading effect is listed. - raster: affine composite fast path (per-row span clipping, unchecked interior bilinear, integer-translation copies, inlined Normal blend), checked against composite_warp_reference; box/Gaussian blur without transposes, two ping-pong buffers; accumulate_warp so motion blur sums into one buffer instead of one full-frame image per sub-sample. - ui: viewer frame is urgent and jumps the prefetch queue; stale-revision jobs are dropped; paused prefetch waits for the viewer frame; "Frame Render Time" reports viewer frames only; parallel f32 -> Color32 conversion. - cli: `frame --bench N` (per-layer/per-effect timings, wall + CPU time) and `frame --bench-play N` (consecutive frames with/without the layer cache). - tests: cache transparency (scrub, edits of values/keys/effects/masks/ solids/text/timing), hit/miss accounting, time-dependent effects, demo cached == uncached, fast warp/blur/accumulate == reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
dee16181fe
commit
a2ff724ac2
@@ -1,13 +1,18 @@
|
||||
//! EffectCraft headless CLI.
|
||||
//!
|
||||
//! ```text
|
||||
//! effectcraft-cli frame [project.ecproj | --demo] [--comp NAME] [--time S | --frame N] [--scale K] --out out.png
|
||||
//! effectcraft-cli frame [project.ecproj | --demo] [--comp NAME] [--time S | --frame N] [--scale K]
|
||||
//! [--bench N] [--bench-play N] --out out.png
|
||||
//! effectcraft-cli run [project.ecproj | --demo] '<command-id>' '<json params>' … (prints results)
|
||||
//! effectcraft-cli commands (lists commands)
|
||||
//! ```
|
||||
//!
|
||||
//! `--bench N` renders the frame N times without the layer cache and prints per-layer and
|
||||
//! per-effect timings; `--bench-play N` renders N consecutive frames with and without the cache.
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_engine::project::ItemId;
|
||||
use effectcraft_render::{LayerCache, LayerTiming, RenderOpts, Renderer};
|
||||
use effectcraft_time::Tick;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
@@ -22,7 +27,8 @@ fn open(s: &mut Session, args: &[String]) {
|
||||
eprintln!("effectcraft: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
} else if args.iter().any(|a| a == "--demo") || true {
|
||||
} else {
|
||||
// `--demo` or no project: the demo project.
|
||||
let _ = s.execute("file.openDemoProject", json!({}));
|
||||
}
|
||||
}
|
||||
@@ -31,6 +37,105 @@ fn opt<'a>(args: &'a [String], k: &str) -> Option<&'a str> {
|
||||
args.iter().position(|a| a == k).and_then(|i| args.get(i + 1)).map(String::as_str)
|
||||
}
|
||||
|
||||
fn median(v: &mut [f64]) -> f64 {
|
||||
v.sort_by(f64::total_cmp);
|
||||
if v.is_empty() { 0.0 } else { v[v.len() / 2] }
|
||||
}
|
||||
|
||||
fn min(v: &[f64]) -> f64 {
|
||||
v.iter().copied().fold(f64::INFINITY, f64::min)
|
||||
}
|
||||
|
||||
/// CPU time of the whole process (all threads) in ms: a load-independent measure of work.
|
||||
fn cpu_ms() -> f64 {
|
||||
cpu_time::ProcessTime::try_now().map(|t| t.as_duration().as_secs_f64() * 1e3).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
/// One frame: (wall ms, CPU ms, per-layer timings).
|
||||
fn render_timed(s: &Session, cid: ItemId, t: Tick, opts: RenderOpts, cache: Option<&LayerCache>) -> (f64, f64, Vec<LayerTiming>) {
|
||||
let prof = std::sync::Mutex::new(Vec::<LayerTiming>::new());
|
||||
let mut r = Renderer::new(&s.project, s.footage.as_ref(), opts);
|
||||
r.expr = s.expr.as_deref();
|
||||
r.cache = cache;
|
||||
r.profile = Some(&prof);
|
||||
let c0 = cpu_ms();
|
||||
let t0 = std::time::Instant::now();
|
||||
let img = r.comp_frame(cid, t);
|
||||
let ms = t0.elapsed().as_secs_f64() * 1e3;
|
||||
let cpu = cpu_ms() - c0;
|
||||
std::hint::black_box(&img);
|
||||
(ms, cpu, prof.into_inner().unwrap_or_default())
|
||||
}
|
||||
|
||||
type LayerRow = (usize, String, Vec<f64>, Vec<f64>, Vec<(String, Vec<f64>)>);
|
||||
|
||||
/// `frame --bench N`: render the same frame N times without the layer cache (the full cost of a
|
||||
/// frame) and print min/median frame time plus min per-layer and per-effect times.
|
||||
fn bench(s: &Session, cid: ItemId, t: Tick, opts: RenderOpts, n: usize) {
|
||||
let mut totals = Vec::with_capacity(n);
|
||||
let mut cpus = Vec::with_capacity(n);
|
||||
let mut per_layer: Vec<LayerRow> = Vec::new();
|
||||
for _ in 0..n {
|
||||
let (ms, cpu, prof) = render_timed(s, cid, t, opts, None);
|
||||
totals.push(ms);
|
||||
cpus.push(cpu);
|
||||
for (i, lt) in prof.into_iter().enumerate() {
|
||||
if per_layer.len() <= i {
|
||||
per_layer.push((lt.depth, lt.layer.clone(), vec![], vec![], vec![]));
|
||||
}
|
||||
let e = &mut per_layer[i];
|
||||
e.2.push(lt.process_ms);
|
||||
e.3.push(lt.composite_ms);
|
||||
for (j, (id, ms)) in lt.effects.into_iter().enumerate() {
|
||||
if e.4.len() <= j {
|
||||
e.4.push((id, vec![]));
|
||||
}
|
||||
e.4[j].1.push(ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!(
|
||||
"bench (no cache): {n} runs at scale {}: wall min {:.2} ms, median {:.2} ms; CPU (all threads) min {:.2} ms",
|
||||
opts.scale,
|
||||
min(&totals),
|
||||
median(&mut totals),
|
||||
min(&cpus)
|
||||
);
|
||||
for (d, name, p, c, fx) in per_layer {
|
||||
eprintln!(" {:indent$}{name:<28} process {:8.2} ms composite {:8.2} ms (min)", "", min(&p), min(&c), indent = d * 2);
|
||||
for (id, v) in fx {
|
||||
eprintln!(" {:indent$} fx {id:<30} {:8.2} ms", "", min(&v), indent = d * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `frame --bench-play N`: render N consecutive frames from `t` as playback does, without and
|
||||
/// with the layer cache, and print per-frame times.
|
||||
fn bench_play(s: &Session, cid: ItemId, t: Tick, opts: RenderOpts, n: usize) {
|
||||
let Some(comp) = s.project.comp(cid) else { return };
|
||||
let f0 = comp.frame_rate.frame_at(t);
|
||||
let cache = LayerCache::default();
|
||||
for (label, c) in [("no cache", None), ("layer cache", Some(&cache))] {
|
||||
let runs: Vec<(f64, f64)> = (0..n as i64)
|
||||
.map(|i| {
|
||||
let (ms, cpu, _) = render_timed(s, cid, comp.frame_rate.tick_of(f0 + i), opts, c);
|
||||
(ms, cpu)
|
||||
})
|
||||
.collect();
|
||||
let mut v: Vec<f64> = runs.iter().map(|r| r.0).collect();
|
||||
let mean = v.iter().sum::<f64>() / n as f64;
|
||||
let cpu = runs.iter().map(|r| r.1).sum::<f64>() / n as f64;
|
||||
eprintln!(
|
||||
"bench-play ({label}): {n} frames at scale {}: wall mean {mean:.2} ms, median {:.2} ms, min {:.2} ms; CPU mean {cpu:.2} ms",
|
||||
opts.scale,
|
||||
median(&mut v),
|
||||
min(&v)
|
||||
);
|
||||
}
|
||||
let st = cache.stats();
|
||||
eprintln!(" layer cache: {} hits, {} misses, {} entries, {:.1} MB", st.hits, st.misses, st.entries, st.bytes as f64 / 1e6);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let Some(cmd) = args.first().cloned() else { usage() };
|
||||
@@ -54,6 +159,12 @@ fn main() {
|
||||
};
|
||||
let scale = opt(rest, "--scale").and_then(|v| v.parse().ok()).unwrap_or(1.0);
|
||||
let out = opt(rest, "--out").unwrap_or("frame.png");
|
||||
if let Some(n) = opt(rest, "--bench").and_then(|v| v.parse::<usize>().ok()) {
|
||||
bench(&s, cid, t, RenderOpts { scale, ..Default::default() }, n.max(1));
|
||||
}
|
||||
if let Some(n) = opt(rest, "--bench-play").and_then(|v| v.parse::<usize>().ok()) {
|
||||
bench_play(&s, cid, t, RenderOpts { scale, ..Default::default() }, n.max(1));
|
||||
}
|
||||
let t0 = std::time::Instant::now();
|
||||
let img = s.render(cid, t, RenderOpts { scale, ..Default::default() });
|
||||
let el = t0.elapsed();
|
||||
|
||||
Reference in New Issue
Block a user