Merge pull request #276 from storytold/fix/262-gpu-why

Render: render.backend says why it renders on the CPU; mcp --gpu documented (#262)
This commit is contained in:
BFlat authored and GitHub committed 2026-10-08 18:09:33 +07:00
commit 52ab38fcff
7 files changed
+46 -5

No files matched your search

+4 -2
View File
@@ -322,8 +322,10 @@ fn backend(args: &Args, default_demo: bool) -> Result<Backend, Failure> {
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()))?;
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<Item
/// warm; "up/dn MB" are the GPU's uploads / readbacks per warm GPU frame.
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 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`:
+9
View File
@@ -546,11 +546,20 @@ fn render_backend(s: &mut Session, p: &Value) -> Result<Value> {
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,
})
}
+4
View File
@@ -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<Arc<dyn effectcraft_render::Accelerator>>,
/// 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<String>,
/// 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>>,
@@ -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,
+12
View File
@@ -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"));
}
+7 -1
View File
@@ -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<Gpu> {
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<Gpu, String> {
pollster::block_on(GpuContext::request()).map(Gpu::from_context)
}
/// On an existing device (the desktop app shares egui-wgpu's).
+6 -1
View File
@@ -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::<String>().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}"));
}
}
}
+4 -1
View File
@@ -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.