Merge branch 'worktree-agent-afea1146abae6192c'

# Conflicts:
#	apps/effectcraft-cli/Cargo.toml
#	apps/effectcraft-cli/src/main.rs
#	crates/render/src/lib.rs
This commit is contained in:
Brandon Thomas
2026-10-01 22:35:20 -04:00
15 changed files with 1267 additions and 85 deletions
+5 -1
View File
@@ -17,7 +17,7 @@ use std::sync::Arc;
use effectcraft_project::{Comp, ItemId, Layer, LayerId, Project, Uid};
use effectcraft_raster::Image;
use effectcraft_render::{ExprHost, FootageSource, NoFootage, RenderOpts, Renderer};
use effectcraft_render::{ExprHost, FootageSource, LayerCache, NoFootage, RenderOpts, Renderer};
use effectcraft_time::Tick;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -141,6 +141,8 @@ pub struct Session {
pub events: Vec<Event>,
/// Commands executed: (id, params).
pub journal: Vec<(String, Value)>,
/// Processed-layer pixels reused across frames and edits (content-keyed, never stale).
pub layer_cache: Arc<LayerCache>,
}
impl Default for Session {
@@ -160,6 +162,7 @@ impl Default for Session {
render_job: None,
events: vec![],
journal: vec![],
layer_cache: Arc::new(LayerCache::default()),
}
}
}
@@ -319,6 +322,7 @@ impl Session {
pub fn render(&self, comp: ItemId, t: Tick, opts: RenderOpts) -> Image {
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.comp_frame(comp, t)
}
+29
View File
@@ -18,6 +18,35 @@ fn demo_opens_and_renders() {
assert!(lit > 100_000, "{lit}");
}
/// The session's layer cache never changes pixels: scrubbing the demo (including back and
/// forth, and across an edit made through a command) matches cache-less renders exactly.
#[test]
fn demo_layer_cache_is_transparent() {
let mut s = demo();
let cid = s.active_comp_id().unwrap();
let opts = effectcraft_render::RenderOpts { scale: 0.25, ..Default::default() };
let uncached = |s: &Session, t: f64| {
let mut r = effectcraft_render::Renderer::new(&s.project, s.footage.as_ref(), opts);
r.expr = s.expr.as_deref();
r.comp_frame(cid, effectcraft_time::Tick::from_seconds_f64(t))
};
let check = |s: &Session, t: f64| {
let a = s.render(cid, effectcraft_time::Tick::from_seconds_f64(t), opts);
let b = uncached(s, t);
assert!(a.data.iter().zip(&b.data).all(|(p, q)| (0..4).all(|c| (p[c] - q[c]).abs() < 1e-6)), "t={t}");
};
for t in [0.5, 1.0, 3.0, 3.0334, 6.0, 3.0, 0.5] {
check(&s, t);
}
assert!(s.layer_cache.stats().hits > 0);
// Edit the title's glow through the command layer, then scrub again.
let title = s.project.comp(cid).unwrap().layers.iter().find(|l| l.name == "EFFECTCRAFT").map(|l| l.id.0).unwrap();
s.execute("prop.set", json!({"layer": title, "path": "effects/#1/radius", "value": 5.0})).unwrap();
for t in [3.0, 0.5] {
check(&s, t);
}
}
#[test]
fn every_command_has_unique_id_and_runs_or_reports() {
let mut ids = std::collections::HashSet::new();