mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 20:47:18 -06:00
Merge pull request #2347 from vangel76/warp-skip-discarded-plan
Reuse the validated Warp Stabilizer plan on first render
This commit is contained in:
@@ -223,6 +223,19 @@ fn plan_cached(akey: u64, a: &WarpAnalysis, s: &StabSettings) -> Arc<Plan> {
|
||||
p
|
||||
}
|
||||
|
||||
/// Validate and cache the default stabilization plan after analysis. Use the serialized
|
||||
/// analysis so the cached plan has exactly the same inputs as the first render, including
|
||||
/// any floating-point round trip through JSON. Other settings still get their own plan.
|
||||
pub fn prepare_default_plan(json: &str) -> bool {
|
||||
let Some((key, a)) = analysis_cached(json) else { return false };
|
||||
if a.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let fps = if a.frame_duration > 0.0 { 1.0 / a.frame_duration } else { 30.0 };
|
||||
let p = plan_cached(key, &a, &StabSettings { fps, ..Default::default() });
|
||||
p.warps.len() == a.frames.len() && p.warps.iter().all(|h| h.0.iter().flatten().all(|v| v.is_finite()))
|
||||
}
|
||||
|
||||
/// The stored analysis of an instance (`None` when not analysed).
|
||||
pub fn analysis(params: &Params) -> Option<Arc<WarpAnalysis>> {
|
||||
let j = s(params, ANALYSIS);
|
||||
@@ -601,6 +614,42 @@ mod tests {
|
||||
assert_eq!(render(&ctx, Buf { img: img.clone(), offset: [0.0; 2], scale: 1.0 }).img.data, img.data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepared_default_plan_preserves_first_render_and_is_reused() {
|
||||
let json = analysis_json(25, 64.0, 48.0, |k| [((k * 7) % 5) as f64 - 2.0, k as f64 * 0.1]);
|
||||
let mut a = WarpAnalysis::from_json(&json).unwrap();
|
||||
a.start = 0.012345678901234567;
|
||||
a.tracks = (0..24)
|
||||
.map(|id| effectcraft_track::camtrack::Track2D {
|
||||
id,
|
||||
start: 0,
|
||||
pts: (0..25).map(|k| [8.0 + (id % 6) as f32 * 8.0 + ((k * 7) % 5) as f32, 8.0 + (id / 6) as f32 * 8.0 + k as f32 * 0.1]).collect(),
|
||||
})
|
||||
.collect();
|
||||
let json = a.to_json();
|
||||
let ps = params(&json, 2, 0);
|
||||
let ctx = EffectCtx { params: &ps, time: a.start, layer_size: a.size, seed: 1, adjustment: false, env: EffectEnv::default() };
|
||||
let input = Buf { img: frame([30.0, 22.0]), offset: [0.0; 2], scale: 1.0 };
|
||||
// Original flow: validate, discard the plan, then render from the stored analysis.
|
||||
let _discarded = plan(&a, &StabSettings { fps: 25.0, ..Default::default() });
|
||||
let before = render(&ctx, input.clone());
|
||||
let key = (fnv(json.as_bytes()), settings_key(&settings(&ps, 25.0)));
|
||||
plans().lock().unwrap().remove(&key);
|
||||
assert!(prepare_default_plan(&json));
|
||||
let prepared = plans().lock().unwrap().get(&key).unwrap().clone();
|
||||
assert!(!prepared.meshes.is_empty(), "exercise the Subspace Warp plan");
|
||||
let after = render(&ctx, input);
|
||||
let (_, _, rendered) = plan_at(&ps, a.start).unwrap();
|
||||
assert!(Arc::ptr_eq(&prepared, &rendered));
|
||||
assert_eq!(before.offset, after.offset);
|
||||
assert_eq!(before.scale.to_bits(), after.scale.to_bits());
|
||||
for (a, b) in before.img.data.iter().flatten().zip(after.img.data.iter().flatten()) {
|
||||
assert_eq!(a.to_bits(), b.to_bits());
|
||||
}
|
||||
assert!(!prepare_default_plan("invalid"));
|
||||
assert!(!prepare_default_plan(&WarpAnalysis::default().to_json()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_param_lookup() {
|
||||
let mut p = Params::default();
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
//! the Warp Stabilizer, `Renderer::layer_input`) for every frame between the layer's In and Out
|
||||
//! points on a background thread, and feeds the frames to `effectcraft_track::stabilize`'s
|
||||
//! analyzer (step 1 of 2, "Analyzing in background"); step 2 ("Stabilizing") derives the
|
||||
//! stabilization plan once to validate the result. When the job finishes, the analysis is
|
||||
//! stabilization plan once to validate and cache the result. When the job finishes, the analysis is
|
||||
//! written into the effect's hidden Analysis parameter as one undo step, together with a key of
|
||||
//! what it was made from (the layer's source, In/Out, start, stretch and Time Remap). Edits that
|
||||
//! change those clear the analysis (`invalidate`, run after every edit) and queue the effect
|
||||
@@ -21,7 +21,7 @@ use effectcraft_project::{GroupKind, ItemId, ItemKind, Layer, LayerId, LayerSour
|
||||
use effectcraft_raster::Image;
|
||||
use effectcraft_render::{EvalCtx, ExprHost, FootageSource, LayerCache, Renderer};
|
||||
use effectcraft_time::Tick;
|
||||
use effectcraft_track::stabilize::{AnalyzeOpts, Analyzer, StabSettings, WarpAnalysis};
|
||||
use effectcraft_track::stabilize::{AnalyzeOpts, Analyzer, WarpAnalysis};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::tracking::lock;
|
||||
@@ -245,9 +245,8 @@ async fn run_work(w: WarpWork, shared: &WarpShared) {
|
||||
let lt0 = layer.layer_time(w.times[0]).seconds();
|
||||
let fd = if w.times.len() > 1 { layer.layer_time(w.times[1]).seconds() - lt0 } else { comp.frame_duration().seconds() };
|
||||
let a = an.finish(lt0, fd);
|
||||
// Step 2: derive a plan once (validates the motion and warms nothing else up).
|
||||
let plan = effectcraft_track::stabilize::plan(&a, &StabSettings { fps: if fd > 0.0 { 1.0 / fd } else { 30.0 }, ..Default::default() });
|
||||
if plan.warps.len() != a.frames.len() || plan.warps.iter().any(|h| h.0.iter().flatten().any(|v| !v.is_finite())) {
|
||||
// Plan from the stored JSON, exactly as rendering does, and retain it for the first render.
|
||||
if !warp_stab::prepare_default_plan(&a.to_json()) {
|
||||
return fail("the stabilization could not be computed");
|
||||
}
|
||||
*lock(&shared.result) = Some(a);
|
||||
|
||||
Reference in New Issue
Block a user