From 4eb69e75770f5f574dadf3b9959fd85ef86d20f8 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 8 Oct 2026 18:09:01 +0700 Subject: [PATCH] Render: render.backend says why it renders on the CPU; mcp --gpu documented (#262) get_project / render.backend reported "gpuAcceleration": true with "active": "cpu" and no reason, which read as a broken GPU. The headless MCP server only attaches the GPU compositor when started with --gpu, which docs/agents.md didn't mention, and a failed --gpu said only "no usable GPU adapter" (the adapter's error went to a log no one sees). The session now keeps the host's reason there is no GPU compositor (accel_note): the CLI's "headless: renders on the CPU unless started with --gpu", or the desktop app's compositor setup or retirement error. The status reports it as `why` whenever it renders on the CPU (or that the project's renderer is Software Only). --gpu failures include the adapter's error (Gpu::try_headless). docs/agents.md documents `mcp --gpu`. Test: render_backend_says_why_it_renders_on_the_cpu (engine). Checked with effectcraft-cli mcp and mcp --gpu. Co-Authored-By: Claude Opus 5.5 --- apps/effectcraft-cli/src/main.rs | 6 ++++-- crates/engine/src/commands/file.rs | 9 +++++++++ crates/engine/src/lib.rs | 4 ++++ crates/engine/src/tests_settings.rs | 12 ++++++++++++ crates/gpu/src/lib.rs | 8 +++++++- crates/ui-egui/src/lib.rs | 7 ++++++- docs/agents.md | 5 ++++- 7 files changed, 46 insertions(+), 5 deletions(-) diff --git a/apps/effectcraft-cli/src/main.rs b/apps/effectcraft-cli/src/main.rs index ec3b0b50..605db2fc 100644 --- a/apps/effectcraft-cli/src/main.rs +++ b/apps/effectcraft-cli/src/main.rs @@ -322,8 +322,10 @@ fn backend(args: &Args, default_demo: bool) -> Result { fn session(args: &Args) -> Result { 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()))?; + let g = effectcraft_gpu::Gpu::try_headless().map_err(|e| Failure::Error(format!("--gpu: no usable GPU adapter ({e})")))?; s.accel = Some(std::sync::Arc::new(g)); + } else { + s.accel_note = Some("headless: renders on the CPU unless started with --gpu".into()); } Ok(s) } @@ -1069,7 +1071,7 @@ fn adv3d_bench_comp(p: &mut effectcraft_engine::project::Project) -> Option Result<(), Failure> { use effectcraft_render::Backend; - let gpu = effectcraft_gpu::Gpu::headless().ok_or_else(|| Failure::Error("--gpu: no usable GPU adapter".into()))?; + let gpu = effectcraft_gpu::Gpu::try_headless().map_err(|e| Failure::Error(format!("--gpu: no usable GPU adapter ({e})")))?; 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) and an Advanced 3D comp (`--adv3d`: diff --git a/crates/engine/src/commands/file.rs b/crates/engine/src/commands/file.rs index fd7a02b3..fe32d28a 100644 --- a/crates/engine/src/commands/file.rs +++ b/crates/engine/src/commands/file.rs @@ -546,11 +546,20 @@ fn render_backend(s: &mut Session, p: &Value) -> Result { pub fn backend_status(s: &Session) -> Value { let adapter = s.accel.as_ref().map(|a| a.name()); let gpu = s.project.settings.gpu_acceleration; + // Why it renders on the CPU (#262). + let why = if !gpu { + Some("the project's renderer is Mercury Software Only".to_string()) + } else if adapter.is_none() { + Some(s.accel_note.clone().unwrap_or_else(|| "no GPU compositor is attached".into())) + } else { + None + }; json!({ "renderer": if gpu { "Mercury GPU Acceleration" } else { "Mercury Software Only" }, "gpuAcceleration": gpu, "adapter": adapter, "active": if gpu && adapter.is_some() { "gpu" } else { "cpu" }, + "why": why, }) } diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index db21709a..93fb4c19 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -340,6 +340,9 @@ pub struct Session { /// that ask for [`effectcraft_render::Backend::Gpu`]/`Auto`, and by Render Queue exports /// when the project's renderer is the GPU. pub accel: Option>, + /// Why there is no [`Session::accel`] (`render.backend`'s `why`): the host's reason, e.g. a + /// headless start without `--gpu` or the compositor's setup error. + pub accel_note: Option, /// Expression syntax checker (set by the host that links the expression engine). pub expr_check: Option std::result::Result<(), String>>, pub importer: Option>, @@ -473,6 +476,7 @@ impl Default for Session { footage: Arc::new(NoFootage), expr: None, accel: None, + accel_note: None, expr_check: None, importer: None, exporter: None, diff --git a/crates/engine/src/tests_settings.rs b/crates/engine/src/tests_settings.rs index 69847ca5..231fe878 100644 --- a/crates/engine/src/tests_settings.rs +++ b/crates/engine/src/tests_settings.rs @@ -536,3 +536,15 @@ fn unicode_label_colors_fall_back_without_panicking() { s.execute("prefs.set", json!({"key": "labels.0.color", "value": "#aBcD09"})).unwrap(); assert_eq!(s.prefs.label_rgb(Label::Red), [0xab, 0xcd, 9]); } + +/// `render.backend` says why it renders on the CPU (#262): the Software Only renderer, or the +/// host's reason there is no GPU compositor. +#[test] +fn render_backend_says_why_it_renders_on_the_cpu() { + let mut s = Session::default(); + let why = |s: &mut Session| s.execute("render.backend", json!({})).unwrap()["why"].clone(); + assert_eq!(why(&mut s), json!("no GPU compositor is attached")); + s.accel_note = Some("headless: renders on the CPU unless started with --gpu".into()); + assert_eq!(why(&mut s), json!("headless: renders on the CPU unless started with --gpu")); + assert_eq!(s.execute("render.backend", json!({"backend": "cpu"})).unwrap()["why"], json!("the project's renderer is Mercury Software Only")); +} diff --git a/crates/gpu/src/lib.rs b/crates/gpu/src/lib.rs index ece74bb1..7d124303 100644 --- a/crates/gpu/src/lib.rs +++ b/crates/gpu/src/lib.rs @@ -117,7 +117,13 @@ impl Gpu { /// On a device of its own (CLI, tests, benchmarks); `None` without a usable adapter. #[cfg(not(target_arch = "wasm32"))] pub fn headless() -> Option { - GpuContext::headless().map(Gpu::from_context) + Gpu::try_headless().map_err(|e| log::info!("gpu: {e}")).ok() + } + + /// [`Gpu::headless`], with why there is none. + #[cfg(not(target_arch = "wasm32"))] + pub fn try_headless() -> Result { + pollster::block_on(GpuContext::request()).map(Gpu::from_context) } /// On an existing device (the desktop app shares egui-wgpu's). diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 699d5893..eba6ba2e 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -503,6 +503,7 @@ impl EffectcraftApp { self.gpu_checked = true; self.frames.retire_gpu(); self.session.accel = None; + self.session.accel_note = Some(format!("GPU preview retired: {}", failure.reason)); self.session.layer_cache.clear(); self.gpu = None; if let Some(rs) = &self.wgpu { @@ -559,10 +560,14 @@ impl EffectcraftApp { self.gpu = Some(g); self.wgpu = Some(rs.clone()); } - Ok(Err(e)) => log::info!("GPU compositor unavailable: {e}"), + Ok(Err(e)) => { + log::info!("GPU compositor unavailable: {e}"); + self.session.accel_note = Some(format!("GPU compositor unavailable: {e}")); + } Err(e) => { let reason = e.downcast_ref::().map(String::as_str).or_else(|| e.downcast_ref::<&str>().copied()).unwrap_or("backend panicked"); log::warn!("GPU compositor setup failed; using CPU compositing: {reason}"); + self.session.accel_note = Some(format!("GPU compositor setup failed: {reason}")); } } } diff --git a/docs/agents.md b/docs/agents.md index 36c3bd62..52c46d3a 100644 --- a/docs/agents.md +++ b/docs/agents.md @@ -36,7 +36,10 @@ You can also register it from the command line: Desktop, Cursor and the like) take the same `command` and `args`. - **Headless** (`["mcp"]`): an in-process session with no window. Add `"--demo"` or - `"--project", "file.ecproj"` to start with content. Startup is instant. + `"--project", "file.ecproj"` to start with content. Startup is instant. It renders on the CPU + unless you add `"--gpu"` (`["mcp", "--gpu"]`), which attaches the GPU compositor on a device of + its own; if no adapter is usable the server says why and exits. `get_project`'s `renderer` + (`render.backend`) reports `active` (`gpu` / `cpu`) and, on the CPU, `why`. - **Bridge** (`["mcp", "--bridge", "9877"]`): drives a running `effectcraft --control 9877`, so you see every change live. Bridge mode adds `screenshot` and the `ui_*` tools.