mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 13:08:07 -06:00
Merge branch 'worktree-agent-a773c6d272696f48a' (M12.2 GPU compositor)
Auto backend keeps comps with 3D layers on the CPU compositor (3D runs are still CPU-composited; the GPU round-trip made them slower). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -89,7 +89,7 @@ fn save_frame(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let t = f_p(p, "time").map(Tick::from_seconds_f64).unwrap_or(s.time());
|
||||
let scale = f_p(p, "scale").unwrap_or(1.0).clamp(0.01, 4.0);
|
||||
let bg = comp.background;
|
||||
let img = s.render(cid, t, effectcraft_render::RenderOpts { scale, ..Default::default() });
|
||||
let img = s.render(cid, t, effectcraft_render::RenderOpts { scale, backend: effectcraft_render::Backend::Auto, ..Default::default() });
|
||||
let rgba = img.to_rgba8_over(bg);
|
||||
let png = encode_png(&rgba, img.width, img.height).map_err(|e| EngineError::Other(format!("PNG encoding failed: {e}")))?;
|
||||
s.services.write_file(&path, &png).map_err(|e| EngineError::Other(format!("cannot write {path}: {e}")))?;
|
||||
|
||||
@@ -152,6 +152,17 @@ fn project_settings(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
),
|
||||
};
|
||||
}
|
||||
if let Some(r) = p.get("renderer").or_else(|| p.get("gpuAcceleration")) {
|
||||
proj.settings.gpu_acceleration = match r {
|
||||
Value::Bool(b) => *b,
|
||||
Value::String(s) => match effectcraft_render::Backend::parse(s) {
|
||||
Some(effectcraft_render::Backend::Cpu) => false,
|
||||
Some(_) => true,
|
||||
None => return Err(bad("file.projectSettings", format!("renderer: gpu|software, not `{s}`"))),
|
||||
},
|
||||
_ => return Err(bad("file.projectSettings", "renderer: gpu|software")),
|
||||
};
|
||||
}
|
||||
if let Some(t) = str_p(p, "timeDisplay") {
|
||||
proj.settings.time_display =
|
||||
if t.eq_ignore_ascii_case("frames") { effectcraft_project::TimeDisplayStyle::Frames } else { effectcraft_project::TimeDisplayStyle::Timecode };
|
||||
@@ -161,6 +172,41 @@ fn project_settings(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(serde_json::to_value(&s.project.settings).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Video Rendering and Effects: report or set the renderer (Mercury GPU Acceleration when a GPU
|
||||
/// adapter exists, else Mercury Software Only).
|
||||
fn render_backend(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(b) = p.get("backend").or_else(|| p.get("renderer")) {
|
||||
let gpu = match b {
|
||||
Value::Bool(b) => *b,
|
||||
Value::String(v) => match effectcraft_render::Backend::parse(v) {
|
||||
Some(effectcraft_render::Backend::Cpu) => false,
|
||||
Some(_) => true,
|
||||
None => return Err(bad("render.backend", format!("backend: gpu|cpu, not `{v}`"))),
|
||||
},
|
||||
_ => return Err(bad("render.backend", "backend: gpu|cpu")),
|
||||
};
|
||||
if gpu != s.project.settings.gpu_acceleration {
|
||||
s.edit("Project Settings", None, |proj, _| {
|
||||
proj.settings.gpu_acceleration = gpu;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(backend_status(s))
|
||||
}
|
||||
|
||||
/// The renderer setting and what renders with it.
|
||||
pub fn backend_status(s: &Session) -> Value {
|
||||
let adapter = s.accel.as_ref().map(|a| a.name());
|
||||
let gpu = s.project.settings.gpu_acceleration;
|
||||
json!({
|
||||
"renderer": if gpu { "Mercury GPU Acceleration" } else { "Mercury Software Only" },
|
||||
"gpuAcceleration": gpu,
|
||||
"adapter": adapter,
|
||||
"active": if gpu && adapter.is_some() { "gpu" } else { "cpu" },
|
||||
})
|
||||
}
|
||||
|
||||
fn cycle_depth(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
s.edit("Project Bit Depth", None, |proj, _| {
|
||||
proj.settings.bit_depth = proj.settings.bit_depth.next();
|
||||
@@ -184,10 +230,11 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Project Settings...",
|
||||
["File"],
|
||||
Some("Cmd+Alt+Shift+K"),
|
||||
"{bitDepth?: 8|16|32, workingSpace?: none|srgb|rec709|rec2020|p3, linearize?, blendLinear?, timeDisplay?: timecode|frames}",
|
||||
"{bitDepth?: 8|16|32, workingSpace?: none|srgb|rec709|rec2020|p3, linearize?, blendLinear?, renderer?: gpu|software, timeDisplay?: timecode|frames}",
|
||||
always,
|
||||
project_settings
|
||||
),
|
||||
cmd!("file.cycleBitDepth", "Cycle Project Bit Depth", [], None, "{}", always, cycle_depth),
|
||||
cmd!("render.backend", "Video Rendering and Effects", [], None, "{backend?: gpu|cpu}", always, render_backend),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ fn summary(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
"path": s.path,
|
||||
"dirty": s.is_dirty(),
|
||||
"bitDepth": s.project.settings.bit_depth.label(),
|
||||
"renderer": super::file::backend_status(s),
|
||||
"items": items,
|
||||
"activeComp": s.state.active_comp.map(|c| c.0),
|
||||
"time": s.time().seconds(),
|
||||
|
||||
@@ -495,7 +495,8 @@ fn save_current_preview(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut item = RenderQueueItem::new(0, cid);
|
||||
item.output = OutputModule::for_format(format);
|
||||
item.output.output = path.clone();
|
||||
let job = crate::ExportJob { project: &s.project, footage: s.footage.as_ref(), expr: s.expr.as_deref(), item: &item, path: &path };
|
||||
let job =
|
||||
crate::ExportJob { project: &s.project, footage: s.footage.as_ref(), expr: s.expr.as_deref(), accel: s.accel.as_deref(), item: &item, path: &path };
|
||||
let r = exporter.export(&job, &mut |_, _| true).map_err(EngineError::Other)?;
|
||||
s.toast(format!("Saved preview to {}", r.path));
|
||||
Ok(serde_json::to_value(&r).unwrap_or_default())
|
||||
|
||||
@@ -236,6 +236,10 @@ pub struct Session {
|
||||
pub services: Arc<dyn Services>,
|
||||
pub footage: Arc<dyn FootageSource>,
|
||||
pub expr: Option<Arc<dyn ExprHost>>,
|
||||
/// GPU compositor (Mercury GPU Acceleration), when the frontend has one: used by renders
|
||||
/// that ask for [`effectcraft_render::Backend::Gpu`]/`Auto`, and by Render Queue exports
|
||||
/// when the project's renderer is the GPU.
|
||||
pub accel: Option<Arc<dyn effectcraft_render::Accelerator>>,
|
||||
/// Expression syntax checker (set by the host that links the expression engine).
|
||||
pub expr_check: Option<fn(&str) -> std::result::Result<(), String>>,
|
||||
pub importer: Option<Arc<dyn Importer>>,
|
||||
@@ -281,6 +285,7 @@ impl Default for Session {
|
||||
services: Arc::new(FsServices),
|
||||
footage: Arc::new(NoFootage),
|
||||
expr: None,
|
||||
accel: None,
|
||||
expr_check: None,
|
||||
importer: None,
|
||||
exporter: None,
|
||||
@@ -484,6 +489,7 @@ impl Session {
|
||||
let mut r = Renderer::new(&self.project, self.footage.as_ref(), opts);
|
||||
r.expr = self.expr.as_deref();
|
||||
r.cache = Some(&self.layer_cache);
|
||||
r.accel = self.accel.as_deref();
|
||||
r.comp_frame(comp, t)
|
||||
}
|
||||
|
||||
@@ -502,7 +508,7 @@ impl Session {
|
||||
let c = self.project.comp(comp).ok_or(EngineError::NoComp)?;
|
||||
let long = c.width.max(c.height).max(1) as f64;
|
||||
let scale = if max_side == 0 { 1.0 } else { (max_side as f64 / long).min(1.0) };
|
||||
let img = self.render(comp, t, RenderOpts { scale, ..Default::default() });
|
||||
let img = self.render(comp, t, RenderOpts { scale, backend: effectcraft_render::Backend::Auto, ..Default::default() });
|
||||
Ok((img.width, img.height, img.to_rgba8_over(c.background)))
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,8 @@ pub struct ExportJob<'a> {
|
||||
pub footage: &'a dyn FootageSource,
|
||||
pub expr: Option<&'a dyn ExprHost>,
|
||||
pub item: &'a RenderQueueItem,
|
||||
/// GPU compositor to render with when the project's renderer is Mercury GPU Acceleration.
|
||||
pub accel: Option<&'a dyn effectcraft_render::Accelerator>,
|
||||
/// Resolved output path (templates expanded except the `#` frame-number run).
|
||||
pub path: &'a str,
|
||||
}
|
||||
@@ -120,6 +122,7 @@ struct Work {
|
||||
project: Arc<Project>,
|
||||
footage: Arc<dyn FootageSource>,
|
||||
expr: Option<Arc<dyn ExprHost>>,
|
||||
accel: Option<Arc<dyn effectcraft_render::Accelerator>>,
|
||||
exporter: Arc<dyn Exporter>,
|
||||
/// Per queue item: one (item with that output module, resolved path) per output module.
|
||||
items: Vec<Vec<(RenderQueueItem, String)>>,
|
||||
@@ -155,7 +158,7 @@ fn run_work(w: Work, shared: &JobShared) {
|
||||
// first module's file is the item's output.
|
||||
let mut r: Result<ExportResult, String> = Err("no output module".into());
|
||||
for (mi, (mitem, path)) in modules.iter().enumerate() {
|
||||
let job = ExportJob { project: &w.project, footage: w.footage.as_ref(), expr: w.expr.as_deref(), item: mitem, path };
|
||||
let job = ExportJob { project: &w.project, footage: w.footage.as_ref(), expr: w.expr.as_deref(), accel: w.accel.as_deref(), item: mitem, path };
|
||||
let rr = w.exporter.export(&job, &mut |done, total| {
|
||||
let mut s = lock(&shared.state);
|
||||
s.done = done;
|
||||
@@ -288,7 +291,7 @@ impl Session {
|
||||
return Err("nothing is queued: add a composition (Composition ▸ Add to Render Queue) and tick Render".into());
|
||||
}
|
||||
let ids: Vec<u64> = items.iter().filter_map(|m| m.first().map(|(i, _)| i.id)).collect();
|
||||
let work = Work { project: self.project.clone(), footage: self.footage.clone(), expr: self.expr.clone(), exporter, items };
|
||||
let work = Work { project: self.project.clone(), footage: self.footage.clone(), expr: self.expr.clone(), accel: self.accel.clone(), exporter, items };
|
||||
let shared = Arc::new(JobShared::default());
|
||||
if wait {
|
||||
run_work(work, &shared);
|
||||
|
||||
Reference in New Issue
Block a user