mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 12:07:48 -06:00
G2: improve editing reliability and Windows rendering fidelity
This commit is contained in:
@@ -58,7 +58,7 @@ pub(crate) fn label_keys(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
pub fn amplitudes(buf: &[f32]) -> (f64, f64, f64) {
|
||||
let n = (buf.len() / 2).max(1) as f64;
|
||||
let (mut l, mut r) = (0.0f64, 0.0f64);
|
||||
for c in buf.chunks_exact(2) {
|
||||
for c in buf.as_chunks::<2>().0 {
|
||||
l += c[0].abs() as f64;
|
||||
r += c[1].abs() as f64;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ pub fn load_peaks(file: &Path) -> Option<Vec<[f32; 4]>> {
|
||||
return None;
|
||||
}
|
||||
let f = |c: &[u8]| f32::from_le_bytes([c[0], c[1], c[2], c[3]]);
|
||||
Some(b.chunks_exact(16).map(|c| [f(&c[0..4]), f(&c[4..8]), f(&c[8..12]), f(&c[12..16])]).collect())
|
||||
Some(b.as_chunks::<16>().0.iter().map(|c| [f(&c[0..4]), f(&c[4..8]), f(&c[8..12]), f(&c[12..16])]).collect())
|
||||
}
|
||||
|
||||
/// Store a summary (atomically: written beside, then renamed).
|
||||
|
||||
@@ -69,7 +69,7 @@ fn add_configure_render() {
|
||||
assert!(s.execute("renderQueue.setOutputModule", json!({"item": id, "format": "jpeg", "channels": "rgba"})).is_err());
|
||||
let r = s.execute("renderQueue.setOutput", json!({"item": id, "path": "/tmp/rq/out.mov"})).unwrap();
|
||||
assert_eq!(r["output"]["format"], "ProRes", "Output To with .mov switches to ProRes");
|
||||
assert_eq!(r["outputPath"], "/tmp/rq/out.mov");
|
||||
assert_eq!(std::path::Path::new(r["outputPath"].as_str().unwrap()), std::path::absolute("/tmp/rq/out.mov").unwrap());
|
||||
// A second item that fails, a third that is unqueued.
|
||||
let b = s.execute("renderQueue.add", json!({"format": "gif", "output": "/tmp/rq/fail.gif"})).unwrap();
|
||||
let c = s.execute("renderQueue.add", json!({"output": "/tmp/rq/skip.mp4"})).unwrap();
|
||||
@@ -77,11 +77,14 @@ fn add_configure_render() {
|
||||
assert_eq!(s.project.render_queue[2].status.label(), "Unqueued");
|
||||
let r = s.execute("renderQueue.render", json!({})).unwrap();
|
||||
assert_eq!(r["items"].as_array().unwrap().len(), 2);
|
||||
assert_eq!(*ex.log.lock().unwrap(), vec!["/tmp/rq/out.mov".to_string(), "/tmp/rq/fail.gif".to_string()]);
|
||||
assert_eq!(
|
||||
ex.log.lock().unwrap().iter().map(std::path::PathBuf::from).collect::<Vec<_>>(),
|
||||
[std::path::absolute("/tmp/rq/out.mov").unwrap(), std::path::absolute("/tmp/rq/fail.gif").unwrap()]
|
||||
);
|
||||
let q = &s.project.render_queue;
|
||||
assert_eq!(q[0].status.label(), "Done");
|
||||
assert!(q[0].started.is_some() && q[0].render_time.is_some());
|
||||
assert_eq!(q[0].last_output.as_deref(), Some("/tmp/rq/out.mov"));
|
||||
assert_eq!(std::path::Path::new(q[0].last_output.as_deref().unwrap()), std::path::absolute("/tmp/rq/out.mov").unwrap());
|
||||
assert_eq!(q[1].status, RenderStatus::Failed("boom".into()));
|
||||
assert_eq!(q[2].status.label(), "Unqueued");
|
||||
assert!(s.drain_events().iter().any(|e| matches!(e, Event::Toast { error: true, .. })));
|
||||
|
||||
@@ -716,15 +716,14 @@ fn footage_dir(s: &Session, name: &str, footage_dir: Option<&str>, project_path:
|
||||
}
|
||||
let safe: String = name.chars().map(|c| if c.is_alphanumeric() || " -_()".contains(c) { c } else { '_' }).collect();
|
||||
let safe = if safe.trim().is_empty() { "Template".to_string() } else { safe.trim().to_string() };
|
||||
let base = match s.config.as_ref().and_then(|c| c.dir()) {
|
||||
Some(d) => d.join(FOOTAGE_DIR),
|
||||
None => PathBuf::from(format!("/{FOOTAGE_DIR}")),
|
||||
};
|
||||
let base = s.config.as_ref().and_then(|c| c.dir()).map(|d| d.join(FOOTAGE_DIR));
|
||||
// A fresh folder per created project.
|
||||
let mut k = 1;
|
||||
loop {
|
||||
let d = base.join(if k == 1 { safe.clone() } else { format!("{safe} {k}") });
|
||||
let ds = d.to_string_lossy().into_owned();
|
||||
let folder = if k == 1 { safe.clone() } else { format!("{safe} {k}") };
|
||||
// Virtual storage keys use '/' independently of the host OS. Native settings
|
||||
// folders retain their platform path semantics.
|
||||
let ds = base.as_ref().map(|d| d.join(&folder).to_string_lossy().into_owned()).unwrap_or_else(|| format!("/{FOOTAGE_DIR}/{folder}"));
|
||||
if !s.services.exists(&ds) || k > 999 {
|
||||
return ds;
|
||||
}
|
||||
@@ -955,7 +954,7 @@ mod tests {
|
||||
assert!(t.width > 0 && t.duration > 0.0, "{t:?}");
|
||||
let th = decode_thumb(&thumbnail(&s, &t.id).unwrap()).unwrap();
|
||||
// Not a blank frame: some variety in the pixels.
|
||||
let distinct: std::collections::BTreeSet<[u8; 3]> = th.chunks_exact(3).map(|c| [c[0] / 16, c[1] / 16, c[2] / 16]).collect();
|
||||
let distinct: std::collections::BTreeSet<[u8; 3]> = th.as_chunks::<3>().0.iter().map(|c| [c[0] / 16, c[1] / 16, c[2] / 16]).collect();
|
||||
assert!(distinct.len() > 3, "{}: thumbnail looks blank", t.id);
|
||||
}
|
||||
let sq = l.iter().find(|t| t.id == "social-square").unwrap();
|
||||
@@ -1121,7 +1120,7 @@ mod tests {
|
||||
let dir = root.join("work").join("Promo Footage");
|
||||
let fp = footage_paths(&s);
|
||||
let get = |n: &str| fp.iter().find(|f| f.0 == n).unwrap().clone();
|
||||
assert_eq!(get("Still").1, dir.join("still.png").to_string_lossy());
|
||||
assert_eq!(std::path::Path::new(&get("Still").1), dir.join("still.png"));
|
||||
assert_eq!(std::fs::read(dir.join("still.png")).unwrap(), vec![1u8; 1000]);
|
||||
let shot = get("Shot");
|
||||
assert_eq!(shot.2.len(), 3);
|
||||
|
||||
@@ -30,6 +30,7 @@ fn project(s: &mut Session) -> effectcraft_project::ItemId {
|
||||
|
||||
#[test]
|
||||
fn disk_cache_settings_persistence_and_invalidation() {
|
||||
crate::tests_roto::hold_roto_cache_test_lock();
|
||||
let dir = folder("persist");
|
||||
let mut s = session(&dir);
|
||||
let st = s.execute_checked("cache.diskStats", json!({})).unwrap();
|
||||
|
||||
@@ -244,7 +244,7 @@ fn import_export_helpers() {
|
||||
// Append Bit Depth to File Name.
|
||||
assert_eq!(p.output_name("/out/Comp 1.png", 16), "/out/Comp 1.png");
|
||||
p.export.append_bits_to_name = true;
|
||||
assert_eq!(p.output_name("/out/Comp 1.png", 16), "/out/Comp 1_16bpc.png");
|
||||
assert_eq!(PathBuf::from(p.output_name("/out/Comp 1.png", 16)), PathBuf::from("/out/Comp 1_16bpc.png"));
|
||||
assert_eq!(p.output_name("Comp_[#####].exr", 32), "Comp_[#####]_32bpc.exr");
|
||||
// Segments: sequences by file count, movies by size at the data rate.
|
||||
assert_eq!(p.segment_frames(100, true, 0.0), None);
|
||||
@@ -299,7 +299,7 @@ fn render_queue_uses_default_folder_bit_depth_names_and_segments() {
|
||||
assert_eq!(segs.len(), 1, "20 frames × 50 kB fit in 1 MB");
|
||||
item.output.bitrate_kbps = 8000; // 100 kB per frame: 10 frames per MB
|
||||
let segs = s.segments(&item, "/o/Main.mp4");
|
||||
assert_eq!(segs.iter().map(|(_, p)| p.as_str()).collect::<Vec<_>>(), ["/o/Main_001.mp4", "/o/Main_002.mp4"]);
|
||||
assert_eq!(segs.iter().map(|(_, p)| PathBuf::from(p)).collect::<Vec<_>>(), [PathBuf::from("/o/Main_001.mp4"), PathBuf::from("/o/Main_002.mp4")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -255,6 +255,7 @@ fn lift_and_extract_work_area() {
|
||||
|
||||
#[test]
|
||||
fn label_group_purge_and_edit_original() {
|
||||
crate::tests_roto::hold_roto_cache_test_lock();
|
||||
let mut s = comp();
|
||||
let a = solid(&mut s, "#ff0000");
|
||||
let b = solid(&mut s, "#00ff00");
|
||||
|
||||
@@ -28,6 +28,7 @@ fn wait_loaded(s: &mut Session, task: Task) {
|
||||
|
||||
#[test]
|
||||
fn models_are_listed_chosen_and_verified() {
|
||||
crate::tests_roto::hold_roto_cache_test_lock();
|
||||
let (mut s, d) = session("list");
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
let bogus = d.join("bogus.bin");
|
||||
|
||||
@@ -10,6 +10,21 @@ use serde_json::{Value, json};
|
||||
use crate::Session;
|
||||
use crate::tests_track::{H, W, frame_time, setup};
|
||||
|
||||
/// Keep cache/model mutation tests from clearing segmentations under another fixture.
|
||||
/// Libtest runs each test on its own thread; the guard lives until that thread exits.
|
||||
pub(crate) fn hold_roto_cache_test_lock() {
|
||||
use std::cell::RefCell;
|
||||
use std::sync::{Mutex, MutexGuard, PoisonError};
|
||||
static LOCK: Mutex<()> = Mutex::new(());
|
||||
thread_local! {
|
||||
static HELD: RefCell<Option<MutexGuard<'static, ()>>> = const { RefCell::new(None) };
|
||||
}
|
||||
HELD.with(|h| {
|
||||
if h.borrow().is_none() {
|
||||
*h.borrow_mut() = Some(LOCK.lock().unwrap_or_else(PoisonError::into_inner));
|
||||
}
|
||||
});
|
||||
}
|
||||
const R: f64 = 40.0;
|
||||
|
||||
fn noise(x: f64, y: f64, seed: u32) -> f64 {
|
||||
@@ -74,6 +89,7 @@ fn line(a: [f64; 2], b: [f64; 2]) -> Value {
|
||||
/// frame 0. `name`: the footage file name (segmentations are cached process-wide by content key,
|
||||
/// which includes the footage's identity: different clips need different names).
|
||||
fn painted(path: fn(u32) -> [f64; 2], name: &str) -> (Session, LayerId, u64) {
|
||||
hold_roto_cache_test_lock();
|
||||
let (mut s, clip, solid) = setup(move |f| draw(path(f)));
|
||||
let cid = s.active_comp_id().unwrap();
|
||||
s.edit("remove solid", None, |p, _| {
|
||||
|
||||
@@ -460,8 +460,8 @@ fn open_recent_increment_and_revert() {
|
||||
assert!(s.prefs.recent_projects[0].ends_with("A.ecproj"));
|
||||
s.execute("file.clearRecent", json!({})).unwrap();
|
||||
assert!(!s.is_enabled("file.openRecent"));
|
||||
assert_eq!(autosave::increment_path("/x/Intro 9.ecproj", |_| false), "/x/Intro 10.ecproj");
|
||||
assert_eq!(autosave::increment_path("/x/v1.ecproj", |p| p.ends_with("v1 2.ecproj")), "/x/v1 3.ecproj");
|
||||
assert_eq!(std::path::PathBuf::from(autosave::increment_path("/x/Intro 9.ecproj", |_| false)), std::path::PathBuf::from("/x/Intro 10.ecproj"));
|
||||
assert_eq!(std::path::PathBuf::from(autosave::increment_path("/x/v1.ecproj", |p| p.ends_with("v1 2.ecproj"))), std::path::PathBuf::from("/x/v1 3.ecproj"));
|
||||
let _ = std::fs::remove_dir_all(d);
|
||||
}
|
||||
|
||||
|
||||
@@ -284,9 +284,20 @@ fn add_output_module_encodes_twice() {
|
||||
s.execute("renderQueue.setOutput", json!({"index": 1, "module": 2, "path": "/tmp/om/second.mov"})).unwrap();
|
||||
assert!(s.execute("renderQueue.setOutput", json!({"index": 1, "module": 3, "path": "/tmp/x.mov"})).is_err());
|
||||
s.execute("renderQueue.render", json!({})).unwrap();
|
||||
assert_eq!(*ex.log.lock().unwrap(), vec!["H264:/tmp/om/main.mp4".to_string(), "ProRes:/tmp/om/second.mov".to_string()]);
|
||||
assert_eq!(
|
||||
ex.log
|
||||
.lock()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let (format, path) = entry.split_once(':').unwrap();
|
||||
(format.to_string(), std::path::PathBuf::from(path))
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
[("H264".to_string(), std::path::absolute("/tmp/om/main.mp4").unwrap()), ("ProRes".to_string(), std::path::absolute("/tmp/om/second.mov").unwrap())]
|
||||
);
|
||||
assert_eq!(s.project.render_queue[0].status.label(), "Done");
|
||||
assert_eq!(s.project.render_queue[0].last_output.as_deref(), Some("/tmp/om/main.mp4"));
|
||||
assert_eq!(std::path::Path::new(s.project.render_queue[0].last_output.as_deref().unwrap()), std::path::absolute("/tmp/om/main.mp4").unwrap());
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(s.project.render_queue[0].extra_outputs.len(), 1, "undo the Output To edit");
|
||||
}
|
||||
@@ -307,7 +318,9 @@ fn pre_render_imports_and_replaces() {
|
||||
assert_eq!(ex.log.lock().unwrap().len(), 1);
|
||||
let l = &s.project.comp(outer).unwrap().layers[0];
|
||||
let LayerSource::Footage { item } = l.source else { panic!("not replaced: {:?}", l.source) };
|
||||
assert!(matches!(&s.project.item(item).unwrap().kind, effectcraft_project::ItemKind::Footage(f) if f.path == "/tmp/pre/inner.mov"));
|
||||
assert!(
|
||||
matches!(&s.project.item(item).unwrap().kind, effectcraft_project::ItemKind::Footage(f) if std::path::Path::new(&f.path) == std::path::absolute("/tmp/pre/inner.mov").unwrap())
|
||||
);
|
||||
s.execute("edit.undo", json!({})).unwrap();
|
||||
assert!(matches!(s.project.comp(outer).unwrap().layers[0].source, LayerSource::Comp { .. }));
|
||||
}
|
||||
@@ -323,13 +336,15 @@ fn post_render_actions_import_and_set_proxy() {
|
||||
assert_eq!(s.project.render_queue[0].post_render, PostRenderAction::Import);
|
||||
s.execute("renderQueue.render", json!({})).unwrap();
|
||||
assert_eq!(s.project.items.len(), items_before + 1);
|
||||
assert!(s.project.items.values().any(|i| matches!(&i.kind, effectcraft_project::ItemKind::Footage(f) if f.path == "/tmp/post/a.mov")));
|
||||
assert!(s.project.items.values().any(
|
||||
|i| matches!(&i.kind, effectcraft_project::ItemKind::Footage(f) if std::path::Path::new(&f.path) == std::path::absolute("/tmp/post/a.mov").unwrap())
|
||||
));
|
||||
// Set Proxy: the comp gets the render as its proxy.
|
||||
let b = s.execute("renderQueue.add", json!({"output": "/tmp/post/b.mov"})).unwrap();
|
||||
s.execute("renderQueue.setOutputModule", json!({"item": b["item"], "postRenderAction": "Set Proxy"})).unwrap();
|
||||
s.execute("renderQueue.render", json!({})).unwrap();
|
||||
let px = s.project.item(cid).unwrap().proxy.clone().expect("proxy set");
|
||||
assert_eq!(px.footage.path, "/tmp/post/b.mov");
|
||||
assert_eq!(std::path::Path::new(&px.footage.path), std::path::absolute("/tmp/post/b.mov").unwrap());
|
||||
// None: nothing happens.
|
||||
let n = s.project.items.len();
|
||||
let c = s.execute("renderQueue.add", json!({"output": "/tmp/post/c.mov"})).unwrap();
|
||||
@@ -413,7 +428,7 @@ fn log_notify_and_overflow_settings() {
|
||||
let (mut s, _) = rq();
|
||||
let a = s.execute("renderQueue.add", json!({"output": "/tmp/n/a.mp4", "log": "plusPerFrameInfo"})).unwrap();
|
||||
assert_eq!(a["logLabel"], "Plus Per Frame Info");
|
||||
assert_eq!(a["logPath"], "/tmp/n/a_RenderLog.txt");
|
||||
assert_eq!(std::path::Path::new(a["logPath"].as_str().unwrap()), std::path::absolute("/tmp/n/a_RenderLog.txt").unwrap());
|
||||
s.execute("renderQueue.setLog", json!({"item": a["item"], "log": "plusSettings"})).unwrap();
|
||||
assert_eq!(s.project.render_queue[0].log, effectcraft_project::render_queue::RenderLog::PlusSettings);
|
||||
s.execute("renderQueue.setOverflowFolders", json!({"folders": ["/tmp/o1", " ", "/tmp/o2"]})).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user