Merge branch 'main' into worktree-agent-a4b0610bd14276508

This commit is contained in:
Brandon Thomas committed 2026-10-03 10:53:00 -04:00
commit a4f4d72f27
360 files changed
+28430 -578

No files matched your search

+152 -6
View File
@@ -9,11 +9,13 @@
//! effectcraft-cli get <comp> <layer> <path> [--time S] read a property
//! effectcraft-cli set <comp> <layer> <path> <value> [--time S] [--expression E]
//! effectcraft-cli render-frame [--comp C] [--time S|--frame N] [--max-side PX|--scale K] [--out F.png]
//! effectcraft-cli render [--comp C] --out FILE [--format h264|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff] [--start S] [--end S]
//! effectcraft-cli render [--comp C] --out FILE [--format h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff] [--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 proxy|lt|standard|hq|4444|4444xq] [--audio auto|on|off]
//! [--profile main|main10] [--level auto|4.1] [--rate-control bitrate|quality] [--video-quality 1-100]
//! [--keyint FRAMES] [--webm-codec vp9|av1] [--audio-bitrate KBPS] [--opus-app audio|voice]
//! 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] [--gpu] render timings
//! effectcraft-cli bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu [--adv3d]] render timings
//! (--gpu: CPU vs GPU ms/frame for every comp at Full and Half)
//! effectcraft-cli script FILE.jsx [F.ecproj] | --eval CODE run an After Effects-style script
//! effectcraft-cli mcp [--bridge PORT] MCP server on stdio
@@ -44,8 +46,12 @@ const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|se
render-frame [--comp C] [--time S | --frame N] [--max-side PX | --scale K] [--out F.png]
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] [--gpu] per-layer/effect render timings;
[--jpeg-quality N] [--bitrate KBPS] [--prores PROFILE] [--audio auto|on|off]
[--profile main|main10] [--level auto|4.1] [--rate-control bitrate|quality] [--video-quality N]
[--keyint FRAMES] [--webm-codec vp9|av1] [--audio-bitrate KBPS] [--opus-app audio|voice] | --queue
(formats h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff;
--profile..--keyint: HEVC / AV1, --audio-bitrate/--opus-app: WebM Opus)
bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu [--adv3d]] 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
script FILE.jsx [F.ecproj] | --eval CODE run JavaScript with the After Effects-style object model
@@ -85,6 +91,14 @@ const VALUED: &[&str] = &[
"--bitrate",
"--prores",
"--audio",
"--profile",
"--level",
"--rate-control",
"--video-quality",
"--keyint",
"--webm-codec",
"--audio-bitrate",
"--opus-app",
];
struct Args {
@@ -546,6 +560,22 @@ fn render(args: &Args, json_out: bool) -> Result<(), Failure> {
if let Some(v) = args.num("--bitrate")? {
p["bitrate"] = json!(v);
}
for (flag, key) in [
("--profile", "profile"),
("--level", "level"),
("--rate-control", "rateControl"),
("--webm-codec", "webmCodec"),
("--opus-app", "opusApplication"),
] {
if let Some(v) = args.opt(flag) {
p[key] = json!(v);
}
}
for (flag, key) in [("--video-quality", "quality"), ("--keyint", "keyframeInterval"), ("--audio-bitrate", "audioBitrate")] {
if let Some(v) = args.num(flag)? {
p[key] = json!(v);
}
}
// Render exactly this item: unqueue whatever the project's queue already holds.
for k in 0..s.project.render_queue.len() {
let _ = s.execute("renderQueue.setRender", json!({"index": k + 1, "render": false}));
@@ -786,6 +816,109 @@ fn adjustment_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option
Some(p.add_item("Adjustment Layers (bench)", effectcraft_engine::color::Label::None, None, ItemKind::Comp(comp.into())))
}
/// `bench --gpu`'s Advanced 3D comp (1920×1080): a ground plane, PBR primitives, extruded
/// bevelled text, a shadow-casting spot light, a point light and an environment light, seen
/// through a depth-of-field camera (hexagonal iris, highlights) with a motion-blurred sphere.
fn adv3d_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option<ItemId> {
use effectcraft_engine::keyframe::{Keyframe, TextDoc, Value};
use effectcraft_engine::project::build::{self, Ids};
use effectcraft_engine::project::{Comp, ItemKind, Layer, LayerSource, LightKind, PrimitiveKind, Renderer as R3, Solid};
let (w, h) = (1920u32, 1080u32);
let mut comp = Comp::new(w, h, effectcraft_time::FrameRate::FPS_30, Tick::from_seconds_f64(10.0));
comp.renderer = R3::Advanced3D;
comp.enable_motion_blur = true;
comp.motion_blur_samples = 8;
let mut layers: Vec<Layer> = vec![];
let mut add = |p: &mut effectcraft_engine::project::Project, comp: &Comp, src: LayerSource, edit: &dyn Fn(&mut Layer)| {
let mut l = build::layer(p, comp, "L", src, (w, h), None);
edit(&mut l);
layers.insert(0, l);
};
fn set(l: &mut Layer, path: &str, v: Value) {
if let Some(pr) = l.props.prop_mut(path) {
pr.value = v;
}
}
add(p, &comp, LayerSource::Primitive { kind: PrimitiveKind::Plane }, &|l| {
set(l, "geometryOptions/width", Value::Scalar(6000.0));
set(l, "geometryOptions/height", Value::Scalar(6000.0));
set(l, "transform/position", Value::Vec3([960.0, 800.0, 0.0]));
set(l, "transform/rotationX", Value::Scalar(90.0));
});
for (i, kind) in [PrimitiveKind::Sphere, PrimitiveKind::Torus, PrimitiveKind::Cube, PrimitiveKind::Sphere].into_iter().enumerate() {
add(p, &comp, LayerSource::Primitive { kind }, &|l| {
let x = 400.0 + 380.0 * i as f64;
set(l, "geometryOptions/radius", Value::Scalar(140.0));
set(l, "geometryOptions/tubeRadius", Value::Scalar(45.0));
for k in ["width", "height", "depth"] {
set(l, &format!("geometryOptions/{k}"), Value::Scalar(220.0));
}
set(l, "transform/position", Value::Vec3([x, 640.0, 300.0 * i as f64 - 300.0]));
set(l, "transform/rotationX", Value::Scalar(35.0));
set(l, "transform/rotationY", Value::Scalar(25.0));
set(l, "materialOptions/metallic", Value::Scalar(if i % 2 == 1 { 100.0 } else { 0.0 }));
set(l, "materialOptions/roughness", Value::Scalar(30.0));
set(l, "materialOptions/castsShadows", Value::Enum(1));
if i == 0 {
l.switches.motion_blur = true;
if let Some(pr) = l.props.prop_mut("transform/position") {
pr.keys = vec![
Keyframe::new(Tick::ZERO, Value::Vec3([x, 640.0, -300.0])),
Keyframe::new(Tick::from_seconds_f64(10.0), Value::Vec3([x + 300.0 * 60.0, 640.0, -300.0])),
];
}
}
});
}
add(p, &comp, LayerSource::Text, &|l| {
l.switches.three_d = true;
let doc = TextDoc { text: "EffectCraft".into(), size: 180.0, fill: [0.95, 0.75, 0.2, 1.0], apply_fill: true, ..Default::default() };
set(l, "text/sourceText", Value::Text(Box::new(doc)));
let mut next = 900_000u64;
l.props.children.push(build::extrusion_geometry_options(&mut Ids(&mut next)).into());
set(l, "geometryOptions/extrusionDepth", Value::Scalar(40.0));
set(l, "geometryOptions/bevelStyle", Value::Enum(1));
set(l, "transform/position", Value::Vec3([960.0, 260.0, 200.0]));
set(l, "transform/rotationY", Value::Scalar(-15.0));
});
let env = p.add_item(
"Environment (bench)",
effectcraft_engine::color::Label::None,
None,
ItemKind::Solid(Solid { color: [0.55, 0.65, 0.9], width: 64, height: 32, pixel_aspect: 1.0 }),
);
add(p, &comp, LayerSource::Solid { item: env }, &|l| {
l.environment = true;
l.switches.three_d = true;
});
add(p, &comp, LayerSource::Light { kind: LightKind::Environment }, &|l| set(l, "lightOptions/intensity", Value::Scalar(40.0)));
add(p, &comp, LayerSource::Light { kind: LightKind::Spot }, &|l| {
set(l, "transform/position", Value::Vec3([500.0, -600.0, -900.0]));
set(l, "transform/poi", Value::Vec3([960.0, 640.0, 0.0]));
set(l, "lightOptions/castsShadows", Value::Bool(true));
set(l, "lightOptions/coneAngle", Value::Scalar(100.0));
set(l, "lightOptions/shadowDiffusion", Value::Scalar(8.0));
});
add(p, &comp, LayerSource::Light { kind: LightKind::Point }, &|l| {
set(l, "transform/position", Value::Vec3([1500.0, 200.0, -500.0]));
set(l, "lightOptions/intensity", Value::Scalar(50.0));
});
add(p, &comp, LayerSource::Camera, &|l| {
let zoom = match l.props.prop_mut("cameraOptions/zoom").map(|pr| pr.value.clone()) {
Some(Value::Scalar(z)) => z,
_ => 2666.7,
};
set(l, "cameraOptions/dof", Value::Bool(true));
set(l, "cameraOptions/focusDistance", Value::Scalar(zoom));
set(l, "cameraOptions/aperture", Value::Scalar(120.0));
set(l, "cameraOptions/irisShape", Value::Enum(4));
set(l, "cameraOptions/highlightGain", Value::Scalar(20.0));
set(l, "cameraOptions/highlightThreshold", Value::Scalar(200.0));
});
comp.layers = layers;
Some(p.add_item("Advanced 3D (bench)", effectcraft_engine::color::Label::None, None, ItemKind::Comp(comp.into())))
}
/// `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;
@@ -796,14 +929,17 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
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;
// 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).
// full-frame adjustment layer with a GPU effect stack) and an Advanced 3D comp (`--adv3d`:
// that one only).
let mut project = (*s.project).clone();
project.settings.gpu_acceleration = true;
let comps: Vec<ItemId> = match args.opt("--comp") {
_ if args.flag("--adv3d") => adv3d_bench_comp(&mut project).into_iter().collect(),
Some(_) => s.active_comp_id().into_iter().collect(),
None => {
let mut v: Vec<ItemId> = s.project.comps().map(|(id, _)| *id).collect();
v.extend(adjustment_bench_comp(&mut project));
v.extend(adv3d_bench_comp(&mut project));
v
}
};
@@ -864,7 +1000,17 @@ fn bench_gpu(s: &Session, args: &Args) -> Result<(), Failure> {
// Agreement: share of pixels where the GPU frame differs from the CPU reference by
// more than 1/255 on any channel.
let (a, b) = (mk(Backend::Cpu, Some(&cache)), mk(Backend::Gpu, Some(&gcache)));
let off = a.data.iter().zip(&b.data).filter(|(p, q)| (0..4).any(|c| (p[c] - q[c]).abs() > 1.0 / 255.0 + 1e-6)).count();
let off = a
.data
.iter()
.zip(&b.data)
.filter(|(p, q)| {
(0..4).any(|c| {
let d = (p[c] - q[c]).abs();
d.is_nan() || d > 1.0 / 255.0 + 1e-6
})
})
.count();
let pct = 100.0 * off as f64 / a.data.len().max(1) as f64;
let speedup = cpu_warm / gpu_warm.max(1e-9);
eprintln!(