diff --git a/crates/engine/src/tests_track.rs b/crates/engine/src/tests_track.rs index eeb7f95a..b3c2a174 100644 --- a/crates/engine/src/tests_track.rs +++ b/crates/engine/src/tests_track.rs @@ -354,3 +354,78 @@ fn backward_frame_steps_options_and_reset() { assert!(s.active_comp().unwrap().layer(clip).unwrap().motion_trackers().is_none()); assert!(s.execute("track.analyze", json!({})).is_err()); } + +/// End-to-end analysis speed at 1080p for one track point: footage frames through the renderer's +/// layer source and the tracker (frames are prepared before timing). +#[test] +fn analyze_1080p_single_point_speed() { + let (w, h) = (1920u32, 1080u32); + let mut base = Image::new(w + 64, h + 64); + for y in 0..base.height { + for x in 0..base.width { + let v = 0.5 + 0.25 * ((x as f32 * 0.21).sin() * (y as f32 * 0.13).cos()) + 0.15 * ((x + 2 * y) as f32 * 0.07).sin(); + base.set(x, y, [v, v, v, 1.0]); + } + } + let frames: Vec> = (0..31u32) + .map(|f| { + let (dx, dy) = (f as i64, (f / 2) as i64); + let mut img = Image::new(w, h); + for y in 0..h { + for x in 0..w { + img.set(x, y, base.get(x as i64 + 32 - dx, y as i64 + 32 - dy)); + } + } + Arc::new(img) + }) + .collect(); + struct Pre(Vec>); + impl FootageSource for Pre { + fn frame(&self, _: ItemId, _: &Footage, t: Tick) -> Option> { + let f = (t.seconds() * FPS as f64).round().max(0.0) as usize; + self.0.get(f.min(self.0.len() - 1)).cloned() + } + } + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "HD", "width": w, "height": h, "frameRate": FPS, "duration": 2})).unwrap(); + let cid = s.active_comp_id().unwrap(); + let clip = s + .edit("clip", None, |p, _| { + let footage = Footage { + path: "hd.mov".into(), + kind: FootageKind::Video, + width: w, + height: h, + pixel_aspect: 1.0, + frame_rate: FrameRate::new(FPS as i64, 1), + native_rate: None, + duration: Tick::from_seconds_f64(2.0), + has_video: true, + has_audio: false, + alpha: Default::default(), + premul_color: [0.0; 3], + loop_count: 1, + codec: String::new(), + missing: false, + sequence: vec![], + }; + let item = p.add_item("hd", Label::Aqua, None, ItemKind::Footage(footage)); + let comp = p.comp(cid).unwrap().clone(); + let l = build::layer(p, &comp, "HD", LayerSource::Footage { item }, (w, h), None); + let id = l.id; + p.comp_mut(cid).unwrap().layers.insert(0, l); + Ok(id) + }) + .unwrap(); + s.footage = Arc::new(Pre(frames)); + s.execute("track.motion", json!({"layer": clip.0})).unwrap(); + s.execute("track.setPoint", json!({"point": 1, "center": [960, 540], "featureSize": [48, 48], "searchSize": [128, 128]})).unwrap(); + let t0 = std::time::Instant::now(); + s.execute("track.analyze", json!({"wait": true, "end": 30.0 / FPS as f64})).unwrap(); + let secs = t0.elapsed().as_secs_f64(); + let fps = 30.0 / secs; + eprintln!("1080p single-point analysis: {fps:.0} frames/s end to end ({:.1} ms/frame)", secs * 1e3 / 30.0); + let fc = tracker_points(&s, clip)[0].get("featureCenter").unwrap().clone(); + assert!(dist(fc.value_at(frame_time(30)).as_vec2(), [990.0, 555.0]) < 0.25); + assert!(fps > 30.0, "{fps} frames/s"); +} diff --git a/crates/engine/src/tracking.rs b/crates/engine/src/tracking.rs index 12d1a1e9..43e24cb8 100644 --- a/crates/engine/src/tracking.rs +++ b/crates/engine/src/tracking.rs @@ -196,17 +196,35 @@ pub(crate) struct Work { } /// The layer's source frame at comp time `t` at 100 % (layer pixels + offset = image pixels). -fn source_frame(r: &Renderer, project: &Project, cid: ItemId, comp: &Comp, layer: &Layer, t: Tick, expr: Option<&dyn ExprHost>) -> Option<(Image, [f64; 2])> { +/// Footage at its native size is used straight from the footage source (no copy). +fn source_frame( + r: &Renderer, + project: &Project, + cid: ItemId, + comp: &Comp, + layer: &Layer, + t: Tick, + expr: Option<&dyn ExprHost>, +) -> Option<(Arc, [f64; 2])> { let ctx = EvalCtx { project, comp_id: cid, comp, time: t, expr }; + if let effectcraft_project::LayerSource::Footage { item } = &layer.source + && let Some(effectcraft_project::ItemKind::Footage(f)) = project.item(*item).map(|i| &i.kind) + && f.has_video + && let Some(img) = r.footage.frame(*item, f, ctx.source_time(layer)) + && img.width == f.width + && img.height == f.height + { + return Some((img, [0.0; 2])); + } let buf = r.layer_source(&ctx, layer)?; if (buf.scale - 1.0).abs() > 1e-9 && buf.scale > 0.0 { // Footage decoded at another size: bring it to layer pixels. let w = (buf.img.width as f64 / buf.scale).round().max(1.0) as u32; let h = (buf.img.height as f64 / buf.scale).round().max(1.0) as u32; let off = [buf.offset[0] / buf.scale, buf.offset[1] / buf.scale]; - return Some((effectcraft_raster::resample(&buf.img, w, h), off)); + return Some((Arc::new(effectcraft_raster::resample(&buf.img, w, h)), off)); } - Some((buf.img, buf.offset)) + Some((Arc::new(buf.img), buf.offset)) } pub(crate) fn run_work(w: Work, shared: &TrackShared) {