From 7efbe7d85c2de21f30c86337f3d07a3d677ddffe Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 11:48:41 -0700 Subject: [PATCH] Interpret Footage: the Footage panel, thumbnails and tracking show Cineon Settings The Footage panel, the Project panel thumbnail and the native-size source frames that tracking, mask tracking, scene detection and auto-trace read took DPX and Cineon frames straight from the file, so they showed the log code values while a composition showed the converted ones. Each now applies the footage's Cineon Settings, as the composition renderer does. --- crates/engine/src/commands/footage_panel.rs | 6 +++++ crates/engine/src/lib.rs | 2 +- crates/engine/src/tests_proxy.rs | 29 +++++++++++++++++++++ crates/engine/src/tracking.rs | 5 ++-- crates/ui-egui/src/panels/footage_panel.rs | 2 +- 5 files changed, 40 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/commands/footage_panel.rs b/crates/engine/src/commands/footage_panel.rs index 1bd0d2236..26cc8e5c9 100644 --- a/crates/engine/src/commands/footage_panel.rs +++ b/crates/engine/src/commands/footage_panel.rs @@ -56,6 +56,12 @@ pub fn timecode(item: Option<&effectcraft_project::Item>, fr: effectcraft_time:: } } +/// The frame the Footage panel shows: footage `f` at source time `t`, with its Cineon Settings +/// applied. +pub fn frame(s: &Session, item: ItemId, f: &effectcraft_project::Footage, t: Tick) -> Option> { + s.footage.frame(item, f, t).map(|img| effectcraft_render::cineon_frame(img, f)) +} + fn source_time(s: &Session, p: &Value, item: ItemId) -> Option { let (_, fr) = source_timing(s, item); if let Some(t) = f_p(p, "time") { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 382534a44..da96af6e0 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -1131,7 +1131,7 @@ impl Session { effectcraft_project::ItemKind::Footage(f) if f.has_video => { let f = f.clone(); Some(Box::new(move || { - let img = footage.frame(item, &f, Tick(0))?; + let img = effectcraft_render::cineon_frame(footage.frame(item, &f, Tick(0))?, &f); // Point-sample down to at most `max_side` on the long side. let step = (img.width.max(img.height) as f32 / max_side.max(1) as f32).ceil().max(1.0) as u32; let (w, h) = (img.width.div_ceil(step), img.height.div_ceil(step)); diff --git a/crates/engine/src/tests_proxy.rs b/crates/engine/src/tests_proxy.rs index 2480030ca..d7e6e658c 100644 --- a/crates/engine/src/tests_proxy.rs +++ b/crates/engine/src/tests_proxy.rs @@ -385,3 +385,32 @@ fn cineon_settings_convert_dpx_code_values() { s.execute_checked("file.interpretProxy", json!({"items": [inner.0], "logConversion": true, "highlightRolloff": 0})).unwrap(); assert!(near(proxied(&s, 0), 0.0) && near(proxied(&s, 1), 1.0), "{} {}", proxied(&s, 0), proxied(&s, 1)); } + +/// The Footage panel, the Project panel thumbnail and the tracker's source frames show DPX +/// footage with its Cineon Settings, as a composition renders it. +#[test] +fn cineon_settings_apply_to_footage_panel_thumbnail_and_tracking() { + let mut s = session(); + let d = import(&mut s, "/m/scan.dpx"); + s.execute("comp.new", json!({"name": "C", "width": 4, "height": 4, "frameRate": 10, "duration": 1})).unwrap(); + let c = s.active_comp_id().unwrap(); + s.execute("layer.addItem", json!({"item": d.0})).unwrap(); + s.execute("file.interpretFootage", json!({"item": d.0, "logConversion": true, "highlightRolloff": 0})).unwrap(); + let near = |a: f32, b: f32| (a - b).abs() < 1.5 / 255.0; + let rendered = px(&s, c, 0.0, RenderOpts::default(), 2, 0)[0]; + assert!(near(rendered, 0.26190), "code 400 at gamma 1.7: {rendered}"); + let f = match &s.project.item(d).unwrap().kind { + ItemKind::Footage(f) => f.clone(), + _ => panic!("not footage"), + }; + let panel = crate::commands::footage_panel::frame(&s, d, &f, Tick::ZERO).unwrap().get(2, 0)[0]; + assert!(near(panel, rendered), "Footage panel: {panel}"); + let (w, _, rgba) = (s.thumbnail_job(d, Tick::ZERO, 64).unwrap())().unwrap(); + assert_eq!(w, 4); + let thumb = rgba[8] as f32 / 255.0; + assert!(near(thumb, rendered), "Project panel thumbnail: {thumb}"); + let comp = s.project.comp(c).unwrap(); + let r = crate::render::Renderer::new(&s.project, s.footage.as_ref(), RenderOpts::default()); + let (img, _) = crate::tracking::source_frame(&r, &s.project, c, comp, &comp.layers[0], Tick::ZERO, None).unwrap(); + assert!(near(img.get(2, 0)[0], rendered), "tracker source frame: {}", img.get(2, 0)[0]); +} diff --git a/crates/engine/src/tracking.rs b/crates/engine/src/tracking.rs index cfdd12441..dca6eb6cc 100644 --- a/crates/engine/src/tracking.rs +++ b/crates/engine/src/tracking.rs @@ -197,7 +197,8 @@ pub(crate) struct Work { } /// The layer's source frame at comp time `t` at 100 % (layer pixels + offset = image pixels). -/// Footage at its native size is used straight from the footage source (no copy). +/// Footage at its native size is used straight from the footage source (no copy unless its +/// Cineon Settings convert it). pub(crate) fn source_frame( r: &Renderer, project: &Project, @@ -215,7 +216,7 @@ pub(crate) fn source_frame( && img.width == f.width && img.height == f.height { - return Some((img, [0.0; 2])); + return Some((effectcraft_render::cineon_frame(img, f), [0.0; 2])); } let buf = r.layer_source(&ctx, layer)?; if (buf.scale - 1.0).abs() > 1e-9 && buf.scale > 0.0 { diff --git a/crates/ui-egui/src/panels/footage_panel.rs b/crates/ui-egui/src/panels/footage_panel.rs index a9e7cefb2..92a515995 100644 --- a/crates/ui-egui/src/panels/footage_panel.rs +++ b/crates/ui-egui/src/panels/footage_panel.rs @@ -62,7 +62,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let cached: Option<((u64, i64, u64), egui::TextureHandle, [u32; 2])> = ctx.data(|d| d.get_temp(tid)); let tex = match cached { Some((k, tex, size)) if k == key => Some((tex, size)), - _ => app.session.footage.frame(view.item, f, view.time).map(|img| { + _ => effectcraft_engine::commands::footage_panel::frame(&app.session, view.item, f, view.time).map(|img| { let ci = egui::ColorImage::from_rgba_unmultiplied([img.width as usize, img.height as usize], &img.to_rgba8()); let tex = crate::frames::load_fitted(&ctx, "footage-panel", ci, egui::TextureOptions::LINEAR); ctx.data_mut(|d| d.insert_temp(tid, (key, tex.clone(), [img.width, img.height])));