M12.3: track footage frames without copies; 1080p analysis speed test

Analysis reads native-size footage frames straight from the footage source
(124 frames/s end to end for a 1080p single-point track in the test build).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-02 09:11:08 -04:00
parent e57e6a5120
commit ac533168eb
2 changed files with 96 additions and 3 deletions

View File

@ -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<Arc<Image>> = (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<Arc<Image>>);
impl FootageSource for Pre {
fn frame(&self, _: ItemId, _: &Footage, t: Tick) -> Option<Arc<Image>> {
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");
}

View File

@ -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<Image>, [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) {