mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-11 16:33:01 -06:00
Merge remote-tracking branch 'upstream/main' into rq-readback-3335
# Conflicts: # crates/render/src/lib.rs
This commit is contained in:
@@ -375,7 +375,7 @@ pub(crate) fn job_accel() -> Option<Arc<dyn Accelerator>> {
|
||||
}
|
||||
|
||||
/// Render options of an analysis's input frames: 100 %; with an accelerator, the project's
|
||||
/// renderer (Mercury GPU Acceleration: its GPU effects on the device; Software Only: the CPU).
|
||||
/// renderer (GPU Acceleration: its GPU effects on the device; Software Only: the CPU).
|
||||
pub(crate) fn analysis_opts(accel: Option<&dyn Accelerator>) -> RenderOpts {
|
||||
RenderOpts { scale: 1.0, backend: if accel.is_some() { Backend::Auto } else { Backend::Cpu }, ..Default::default() }
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
use effectcraft_color::Label;
|
||||
use effectcraft_keyframe::Value as KV;
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{ItemKind, LayerSource, Solid};
|
||||
use effectcraft_project::{ItemKind, LayerSource, MatteKind, Solid, TrackMatte};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, bad, has_layers, layers_p, str_p};
|
||||
@@ -13,6 +13,8 @@ use crate::{EngineError, Result, Session, cmd};
|
||||
|
||||
const OPS: &str = "editableText|shapesFromText|masksFromText|shapesFromVector|instance3d";
|
||||
|
||||
/// Layer ▸ Create (`layer.create`): the `op` ([`OPS`]) on the given or selected layers; returns
|
||||
/// the created layer ids and what was skipped.
|
||||
fn create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let op = str_p(p, "op").ok_or_else(|| bad("layer.create", format!("missing `op` ({OPS})")))?;
|
||||
let (cid, lids) = layers_p(s, p)?;
|
||||
@@ -99,8 +101,11 @@ fn create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
continue;
|
||||
};
|
||||
let bytes = s.services.read_file(&f.path).map_err(|e| EngineError::Other(format!("cannot read {}: {e}", f.path)))?;
|
||||
let doc = crate::vector::vector_doc(&f.path, &bytes, f.layer.as_ref(), f.page)
|
||||
// In the source layer's pixels, as the footage is drawn (a placed page of an After
|
||||
// Effects project fills the stored size, #3099).
|
||||
let doc = crate::vector::footage_doc(&f, &bytes)
|
||||
.unwrap_or_else(|| Err(format!("{}: not a vector file", f.path)))
|
||||
.map(|d| crate::vector::fitted_to_footage(d, &f))
|
||||
.map_err(EngineError::Other)?;
|
||||
// Images become footage layers (PNG files written next to the source and
|
||||
// imported), parented to the shape layer below it.
|
||||
@@ -123,7 +128,7 @@ fn create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Without images: one shape layer. With images: the shapes between them
|
||||
// in layers of their own, so the stack keeps the document's paint order.
|
||||
let segments = if image_items.is_empty() { vec![doc.clone()] } else { crate::vector::split_at_images(&doc) };
|
||||
let top = crate::vector::shapes_from_vector(proj, &comp, &src, segments.last().unwrap_or(&doc));
|
||||
let mut top = crate::vector::shapes_from_vector(proj, &comp, &src, segments.last().unwrap_or(&doc));
|
||||
// Top to bottom: the last segment, image n, segment n − 1, … image 1, segment 0.
|
||||
let mut stack = vec![];
|
||||
for k in (0..segments.len().saturating_sub(1)).rev() {
|
||||
@@ -140,9 +145,25 @@ fn create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
sl.name = format!("{} {}", top.name, k + 1);
|
||||
stack.push(sl);
|
||||
}
|
||||
// A placed page shows only its frame (the footage's raster ends there): every
|
||||
// layer made from it takes a hidden frame layer as its track matte (#3099).
|
||||
let matte = crate::vector::frame_matte(proj, &comp, &src, &f);
|
||||
if let Some(m) = &matte {
|
||||
let tm = Some(TrackMatte { layer: m.id, kind: MatteKind::Alpha });
|
||||
top.track_matte = tm;
|
||||
for l in &mut stack {
|
||||
l.track_matte = tm;
|
||||
}
|
||||
}
|
||||
let mut ids = vec![top.id];
|
||||
ids.extend(stack.iter().filter(|l| matches!(l.source, LayerSource::Shape)).map(|l| l.id));
|
||||
// Each directly above the source: the top layer first.
|
||||
// The frame matte is made too (last: the selection stays on the top layer).
|
||||
ids.extend(matte.as_ref().map(|m| m.id));
|
||||
// Each directly above the source: the frame matte (when there is one) above
|
||||
// all, then the top layer first.
|
||||
if let Some(m) = matte {
|
||||
insert_above(proj, cid, lid, m, true)?;
|
||||
}
|
||||
insert_above(proj, cid, lid, top, true)?;
|
||||
for l in stack {
|
||||
insert_above(proj, cid, lid, l, true)?;
|
||||
|
||||
@@ -1352,7 +1352,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Load Effect Plug-in...",
|
||||
[],
|
||||
None,
|
||||
"{path (.wasm / .wat plug-in, API v1) | folder (loads every .wasm in it)}",
|
||||
"{path (.wasm / .wat plug-in, WebAssembly ABI 1 or 2) | folder (loads every .wasm in it)}",
|
||||
super::always,
|
||||
plugins_load
|
||||
),
|
||||
|
||||
@@ -719,8 +719,8 @@ 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).
|
||||
/// Video Rendering and Effects: report or set the renderer (GPU Acceleration when a GPU
|
||||
/// adapter exists, else Software Rendering).
|
||||
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 {
|
||||
@@ -748,14 +748,14 @@ pub fn backend_status(s: &Session) -> Value {
|
||||
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())
|
||||
Some("the project's renderer is Software Rendering".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" },
|
||||
"renderer": if gpu { "GPU Acceleration" } else { "Software Rendering" },
|
||||
"gpuAcceleration": gpu,
|
||||
"adapter": adapter,
|
||||
"active": if gpu && adapter.is_some() { "gpu" } else { "cpu" },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Commands behind the Progress, Media Browser, Metadata and Lumetri Scopes panels.
|
||||
//! Commands behind the Progress, Media Browser, Metadata and Video Scopes panels.
|
||||
|
||||
use effectcraft_project::ItemId;
|
||||
use effectcraft_raster::Image;
|
||||
@@ -159,7 +159,7 @@ fn project_comment(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Lumetri Scopes
|
||||
// ---------------------------------------------------------------- Video Scopes
|
||||
|
||||
/// The comp frame the scopes read (the viewer's image: over the comp background), at most
|
||||
/// `max_side` pixels on the long side.
|
||||
@@ -257,7 +257,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
cmd!("project.setProjectComment", "Project Comment", [], None, "{comment}", always, project_comment),
|
||||
query!(
|
||||
"scopes.analyze",
|
||||
"Lumetri Scopes",
|
||||
"Video Scopes",
|
||||
"{comp?, time?|frame?, scope?: waveformRgb|waveformLuma|waveformYc|vectorscopeYuv|vectorscopeHls|histogram|paradeRgb|paradeYuv, standard?: rec601|rec709|rec2020, float?, clamp?, size?}",
|
||||
scopes_analyze
|
||||
),
|
||||
|
||||
@@ -176,9 +176,11 @@ fn psd_layer(s: &Session, path: &str, l: &SourceLayer) -> Option<(u32, Option<(u
|
||||
Some((u32::try_from(found.index).ok()?, size))
|
||||
}
|
||||
|
||||
/// The pixel size of vector footage `f` as its file is now (its page, framing and layer), or
|
||||
/// `None` when the file cannot be read or shows nothing.
|
||||
fn vector_size(s: &Session, f: &Footage) -> Option<(u32, u32)> {
|
||||
let bytes = s.services.read_file(&f.path).ok()?;
|
||||
let doc = crate::vector::vector_doc(&f.path, &bytes, f.layer.as_ref(), f.page)?.ok()?;
|
||||
let doc = crate::vector::footage_doc(f, &bytes)?.ok()?;
|
||||
(!doc.root.children.is_empty()).then(|| doc.pixel_size())
|
||||
}
|
||||
|
||||
@@ -229,6 +231,10 @@ fn refreshed(s: &Session, f: &Footage, probed: Footage) -> Footage {
|
||||
// The layer is gone from the file: keep the item as it was.
|
||||
None => (w, h) = (f.width, f.height),
|
||||
},
|
||||
// A placed page of an After Effects project keeps the size After Effects stored: the
|
||||
// layers' anchors and masks are measured against it, and the page is drawn to fill it
|
||||
// (the probe would measure the page as a viewer frames it, #3099).
|
||||
None if f.placed_page => (w, h) = (f.width, f.height),
|
||||
// A later page of a PDF: the probe measures the first page.
|
||||
None if f.page != 0 => (w, h) = (f.width, f.height),
|
||||
None => {}
|
||||
|
||||
@@ -238,7 +238,7 @@ fn showcase_3d(p: &mut Project, solids: ItemId) -> Comp {
|
||||
anim(&mut cam, "cameraOptions/focusDistance", keys(&[(0.0, Value::Scalar(1700.0)), (5.0, Value::Scalar(1900.0)), (10.0, Value::Scalar(1650.0))]));
|
||||
|
||||
// Key light: warm spot casting soft shadows. Fill: cool ambient.
|
||||
let mut key = build::layer(p, &c, "Key Light", LayerSource::Light { kind: effectcraft_project::LightKind::Spot }, (1920, 1080), None);
|
||||
let mut key = build::layer(p, &c, "Screen Color Key", LayerSource::Light { kind: effectcraft_project::LightKind::Spot }, (1920, 1080), None);
|
||||
set(&mut key, "transform/position", Value::Vec3([500.0, -500.0, -700.0]));
|
||||
set(&mut key, "transform/poi", Value::Vec3([960.0, 600.0, 300.0]));
|
||||
set(&mut key, "lightOptions/intensity", Value::Scalar(150.0));
|
||||
|
||||
@@ -259,7 +259,7 @@ pub fn tutorials() -> Vec<Tutorial> {
|
||||
target: Some("menu.Layer"),
|
||||
accepts: &["layer.newLight"],
|
||||
when: Value::Null,
|
||||
show: vec![("layer.newLight", json!({"kind": "Spot", "name": "Key Light"}))],
|
||||
show: vec![("layer.newLight", json!({"kind": "Spot", "name": "Screen Color Key"}))],
|
||||
}),
|
||||
step(S {
|
||||
title: "Add a camera",
|
||||
|
||||
@@ -261,15 +261,73 @@ pub fn is_vector(f: &effectcraft_project::Footage) -> bool {
|
||||
effectcraft_render::is_vector_footage(f) || f.path.to_ascii_lowercase().ends_with(".svg")
|
||||
}
|
||||
|
||||
/// `doc` (the document of footage `f`) in the footage's layer pixels, as the renderer draws it
|
||||
/// ([`effectcraft_render::vector_fit`]): a placed page of an After Effects project is scaled to
|
||||
/// fill the size stored for it; other footage is unchanged.
|
||||
pub fn fitted_to_footage(mut doc: Doc, f: &effectcraft_project::Footage) -> Doc {
|
||||
let (_, [sx, sy]) = effectcraft_render::vector_fit(f, [doc.width, doc.height], doc.pixel_size(), 1.0);
|
||||
if f.placed_page {
|
||||
doc.root.transform = Affine::scale_non_uniform(sx, sy) * doc.root.transform;
|
||||
(doc.width, doc.height) = (doc.width * sx, doc.height * sy);
|
||||
}
|
||||
doc
|
||||
}
|
||||
|
||||
/// Create Shapes from Vector Layer on a placed page of an After Effects project: a hidden shape
|
||||
/// layer filling the frame the footage is drawn in (its `width` × `height` raster, in the layer
|
||||
/// pixels of [`fitted_to_footage`]'s document), to be the Alpha track matte of every layer made
|
||||
/// from the page, so bleed, art past the artboard and the page's background stay hidden as on
|
||||
/// the footage (#3099). `None` for other footage, whose shapes are converted whole as before.
|
||||
///
|
||||
/// A track matte is drawn at the composition's resolution, so its edge is the footage's raster
|
||||
/// edge on shape and image layers alike; a mask on an image layer would be drawn at the image's
|
||||
/// own resolution and blur its edge when the image is scaled up.
|
||||
pub fn frame_matte(proj: &mut Project, comp: &Comp, src: &Layer, f: &effectcraft_project::Footage) -> Option<Layer> {
|
||||
if !f.placed_page || f.width == 0 || f.height == 0 {
|
||||
return None;
|
||||
}
|
||||
let svg = format!(
|
||||
r##"<svg xmlns="http://www.w3.org/2000/svg" width="{0}" height="{1}"><rect width="{0}" height="{1}" fill="#ffffff"/></svg>"##,
|
||||
f.width, f.height
|
||||
);
|
||||
let doc = effectcraft_svg::parse(svg.as_bytes()).ok()?;
|
||||
let mut l = build::layer(proj, comp, &format!("{} Frame", src.name), LayerSource::Shape, (0, 0), None);
|
||||
add_doc_shapes(proj, &mut l, &doc);
|
||||
copy_transform(proj, src, &mut l);
|
||||
like_source(&mut l, src);
|
||||
// The layers it mattes carry the source's opacity already.
|
||||
if let Some(o) = l.props.prop_mut("transform/opacity") {
|
||||
o.value = Value::Scalar(100.0);
|
||||
o.keys.clear();
|
||||
o.expr = None;
|
||||
}
|
||||
l.name = comp.unique_layer_name(&format!("{} Frame", src.name));
|
||||
l.switches.video = false;
|
||||
Some(l)
|
||||
}
|
||||
|
||||
/// The vector document of footage `f` (its file read as `bytes`; `None` for other formats): its
|
||||
/// page, framed as [`effectcraft_project::Footage::placed_page`] says, restricted to its layer.
|
||||
pub fn footage_doc(f: &effectcraft_project::Footage, bytes: &[u8]) -> Option<Result<Doc, String>> {
|
||||
let framing = if f.placed_page { effectcraft_pdf::Framing::Placed } else { effectcraft_pdf::Framing::Viewer };
|
||||
vector_doc(&f.path, bytes, f.layer.as_ref(), f.page, framing)
|
||||
}
|
||||
|
||||
/// The vector document of an SVG / PDF / AI / EPS file (`None` for other formats): page
|
||||
/// `page` of a PDF, restricted to one layer when `layer` names it.
|
||||
pub fn vector_doc(path: &str, bytes: &[u8], layer: Option<&effectcraft_project::SourceLayer>, page: u32) -> Option<Result<Doc, String>> {
|
||||
/// `page` of a PDF framed as `framing` says, restricted to one layer when `layer` names it.
|
||||
pub fn vector_doc(
|
||||
path: &str,
|
||||
bytes: &[u8],
|
||||
layer: Option<&effectcraft_project::SourceLayer>,
|
||||
page: u32,
|
||||
framing: effectcraft_pdf::Framing,
|
||||
) -> Option<Result<Doc, String>> {
|
||||
if path.to_ascii_lowercase().ends_with(".svg") || effectcraft_svg::looks_like_svg(bytes) {
|
||||
return Some(effectcraft_svg::parse(bytes).map_err(|e| format!("{path}: {e}")));
|
||||
}
|
||||
effectcraft_pdf::sniff(bytes)?;
|
||||
Some(
|
||||
effectcraft_pdf::parse_page(bytes, page as usize)
|
||||
effectcraft_pdf::parse_page_framed(bytes, page as usize, framing)
|
||||
.map(|d| match layer {
|
||||
Some(l) => effectcraft_pdf::layer_doc_with_sizes(&d, l.index as usize, l.layer_size).0,
|
||||
None => d,
|
||||
|
||||
@@ -1263,7 +1263,7 @@ Window
|
||||
Essential Graphics | window.panel {"panel":"essentialGraphics"}
|
||||
Info | window.panel {"panel":"info"} | Cmd+2
|
||||
Learn | help.inAppTutorials
|
||||
Lumetri Scopes | window.panel {"panel":"lumetriScopes"}
|
||||
Video Scopes | window.panel {"panel":"lumetriScopes"}
|
||||
Mask Interpolation | window.panel {"panel":"maskInterpolation"}
|
||||
Media Browser | window.panel {"panel":"mediaBrowser"}
|
||||
Metadata | window.panel {"panel":"metadata"}
|
||||
|
||||
@@ -23,6 +23,25 @@ fn platform_entries(mac: bool) -> Vec<(Vec<String>, MenuEntry)> {
|
||||
out
|
||||
}
|
||||
|
||||
/// Guard the generated display tree, not command IDs, format tokens or dynamic user names.
|
||||
#[test]
|
||||
fn builtin_menu_labels_do_not_reintroduce_vendor_feature_names() {
|
||||
for mac in [true, false] {
|
||||
let entries = platform_entries(mac);
|
||||
for (path, entry) in &entries {
|
||||
for label in path.iter().chain(std::iter::once(&entry.label)) {
|
||||
let folded = label.to_lowercase();
|
||||
assert!(!folded.starts_with("cc "), "{label}");
|
||||
for mark in ["lumetri", "mercury", "mocha", "keylight"] {
|
||||
assert!(!folded.contains(mark), "{label}");
|
||||
}
|
||||
}
|
||||
}
|
||||
let scopes = entries.iter().find(|(_, e)| e.command == "window.panel" && e.params["panel"] == "lumetriScopes").unwrap();
|
||||
assert_eq!(scopes.1.label, "Video Scopes");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_order_matches_after_effects() {
|
||||
let mac: Vec<String> =
|
||||
|
||||
@@ -304,7 +304,8 @@ pub fn parse_catalog(bytes: &[u8]) -> std::result::Result<(Catalog, Vec<String>)
|
||||
fn runs_here(r: &Release) -> bool {
|
||||
// Without a pre-release suffix (`0.18.0-dev` is 0.18.0).
|
||||
let app = env!("CARGO_PKG_VERSION").split(['-', '+']).next().unwrap_or_default();
|
||||
r.api == crate::effects::plugin::WASM_API_VERSION && r.min_app.as_deref().is_none_or(|m| !newer(m, app))
|
||||
(crate::effects::plugin::MIN_WASM_API_VERSION..=crate::effects::plugin::WASM_API_VERSION).contains(&r.api)
|
||||
&& r.min_app.as_deref().is_none_or(|m| !newer(m, app))
|
||||
}
|
||||
|
||||
/// The newest release this build can run (plug-in API and minimum app version).
|
||||
@@ -789,12 +790,13 @@ fn pick(s: &Session, c: &str, p: &Value) -> Result<(String, Package, Release)> {
|
||||
return Err(bad(
|
||||
c,
|
||||
format!(
|
||||
"{} {} needs plug-in API {} and EffectCraft {} or later; this is EffectCraft {} with WebAssembly plug-in API {}",
|
||||
"{} {} needs WebAssembly ABI {} and EffectCraft {} or later; this is EffectCraft {} with WebAssembly ABI {} to {}",
|
||||
pkg.name,
|
||||
rel.version,
|
||||
rel.api,
|
||||
rel.min_app.as_deref().unwrap_or("(any)"),
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
crate::effects::plugin::MIN_WASM_API_VERSION,
|
||||
crate::effects::plugin::WASM_API_VERSION
|
||||
),
|
||||
));
|
||||
@@ -1125,8 +1127,9 @@ mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(latest_compatible(&p).map(|r| r.version.as_str()), Some("1.2.0"));
|
||||
assert!(runs_here(&rel("1.0.0", 1, None)), "API 1 modules still run on a build with a newer plug-in API");
|
||||
assert!(!runs_here(&rel("1.0.0", 2, None)), "WebAssembly modules are API 1: an API 2 release can't load");
|
||||
assert!(runs_here(&rel("1.0.0", 1, None)), "ABI 1 modules still run on a build with a newer ABI");
|
||||
assert!(runs_here(&rel("1.0.0", 2, None)), "ABI 2 modules run on this build");
|
||||
assert!(!runs_here(&rel("1.0.0", 3, None)), "a WebAssembly ABI this build doesn't implement can't load");
|
||||
let j = package_json(&p, None, &Status::default());
|
||||
assert_eq!(j["needsNewerApp"], true);
|
||||
assert!(latest_compatible(&Package::default()).is_none());
|
||||
|
||||
@@ -439,7 +439,7 @@ const RIGGED: &str = r#"{
|
||||
],
|
||||
"cameras": [{"type": "perspective", "name": "Shot", "perspective": {"yfov": 0.6, "znear": 0.1}}],
|
||||
"extensions": {"KHR_lights_punctual": {"lights": [
|
||||
{"type": "spot", "name": "Key Light", "color": [1, 0.8, 0.6], "intensity": 1.5, "spot": {"innerConeAngle": 0.25, "outerConeAngle": 0.5}},
|
||||
{"type": "spot", "name": "Screen Color Key", "color": [1, 0.8, 0.6], "intensity": 1.5, "spot": {"innerConeAngle": 0.25, "outerConeAngle": 0.5}},
|
||||
{"type": "point", "name": "Fill Light", "intensity": 0.5, "range": 4}
|
||||
]}}
|
||||
}"#;
|
||||
@@ -478,7 +478,7 @@ fn cameras_and_lights_from_model() {
|
||||
let r = s.execute("light.fromModel", json!({})).unwrap();
|
||||
let ls: Vec<Layer> = r["layers"].as_array().unwrap().iter().map(|v| layer_in(&s, cid(&s), v.as_u64().unwrap())).collect();
|
||||
assert_eq!(ls.len(), 2);
|
||||
let key = ls.iter().find(|l| l.name == "Key Light").unwrap();
|
||||
let key = ls.iter().find(|l| l.name == "Screen Color Key").unwrap();
|
||||
assert_eq!(key.source, LayerSource::Light { kind: LightKind::Spot });
|
||||
assert!((key.props.prop("lightOptions/coneAngle").unwrap().value.as_f64() - 1.0f64.to_degrees()).abs() < 1e-6);
|
||||
assert!((key.props.prop("lightOptions/coneFeather").unwrap().value.as_f64() - 50.0).abs() < 1e-6);
|
||||
|
||||
@@ -1221,7 +1221,35 @@ fn render_backend_says_why_it_renders_on_the_cpu() {
|
||||
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"));
|
||||
assert_eq!(s.execute("render.backend", json!({"backend": "cpu"})).unwrap()["why"], json!("the project's renderer is Software Rendering"));
|
||||
}
|
||||
|
||||
/// Display labels are descriptive; machine-readable settings remain the same.
|
||||
#[test]
|
||||
fn renderer_labels_keep_their_stable_machine_values() {
|
||||
let mut s = Session::default();
|
||||
for (backend, label, gpu) in [("cpu", "Software Rendering", false), ("gpu", "GPU Acceleration", true)] {
|
||||
let status = s.execute("render.backend", json!({"backend": backend})).unwrap();
|
||||
assert_eq!(status["renderer"], json!(label));
|
||||
assert_eq!(status["gpuAcceleration"], json!(gpu));
|
||||
assert_eq!(status["active"], json!("cpu"), "the headless test has no GPU adapter");
|
||||
}
|
||||
}
|
||||
|
||||
/// The label `render.backend` reports is one it accepts back, so a caller can round-trip the
|
||||
/// status instead of guessing the command's own values.
|
||||
#[test]
|
||||
fn renderer_status_labels_round_trip_through_the_command() {
|
||||
let mut s = Session::default();
|
||||
for (backend, label, gpu) in [("cpu", "Software Rendering", false), ("gpu", "GPU Acceleration", true)] {
|
||||
let reported = s.execute("render.backend", json!({"backend": backend})).unwrap()["renderer"].clone();
|
||||
assert_eq!(reported, json!(label));
|
||||
let parsed = effectcraft_render::Backend::parse(reported.as_str().unwrap()).expect("the reported renderer parses back");
|
||||
assert_eq!(parsed == effectcraft_render::Backend::Cpu, !gpu, "{reported}");
|
||||
let again = s.execute("render.backend", json!({"backend": reported})).unwrap();
|
||||
assert_eq!(again["renderer"], reported);
|
||||
assert_eq!(again["gpuAcceleration"], json!(gpu));
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings ▸ GPU Information names what the GPU compositor runs on, or why it is off, and the
|
||||
|
||||
Reference in New Issue
Block a user