diff --git a/crates/ui-egui/src/panels/viewer_tools.rs b/crates/ui-egui/src/panels/viewer_tools.rs index e704e0a6..c94f94f5 100644 --- a/crates/ui-egui/src/panels/viewer_tools.rs +++ b/crates/ui-egui/src/panels/viewer_tools.rs @@ -349,6 +349,14 @@ fn transformed(img: &egui::ColorImage, ch: Channel, colorized: bool, stops: f32, egui::ColorImage::new(img.size, px.into_iter().map(|a| Color32::from_rgba_premultiplied(a[0], a[1], a[2], a[3])).collect()) } +/// The display conversion's inputs, for the texture caches. +pub(crate) fn display_key(app: &EffectcraftApp, has_conversion: bool) -> String { + let opts = &app.session.state.viewer; + let custom = if opts.simulation.profile == vw::SimProfile::MyCustom { format!("{:?}", app.session.prefs.custom_rgb) } else { String::new() }; + format!("{:?}{:?}{}{custom}", has_conversion, opts.simulation, app.session.prefs.previews.display_profile) + + &format!("{:?}{:?}{}", app.session.project.settings.working_space, app.session.project.settings.output_space, opts.display_color_management) +} + /// Draw the frame into `comp_rect`: the rendered frame (covering the region of interest when one /// is set), or the snapshot while Show Snapshot is on / F5 is held, through Show Channel and the /// exposure. Fast Previews ▸ Wireframe draws layer outlines instead. @@ -378,10 +386,7 @@ pub(crate) fn draw_frame(app: &mut EffectcraftApp, ctx: &egui::Context, painter: return; } let dc = vw::DisplayColor::of(&app.session); - // The display conversion's inputs, for the texture caches. - let custom = if opts.simulation.profile == vw::SimProfile::MyCustom { format!("{:?}", app.session.prefs.custom_rgb) } else { String::new() }; - let dc_key = format!("{:?}{:?}{}{custom}", dc.is_some(), opts.simulation, app.session.prefs.previews.display_profile) - + &format!("{:?}{:?}{}", app.session.project.settings.working_space, app.session.project.settings.output_space, opts.display_color_management); + let dc_key = display_key(app, dc.is_some()); let plain = opts.channel == Channel::Rgb && opts.exposure == 0.0 && dc.is_none(); let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)); if snap { diff --git a/crates/ui-egui/src/panels/viewers.rs b/crates/ui-egui/src/panels/viewers.rs index f23fd511..8d235133 100644 --- a/crates/ui-egui/src/panels/viewers.rs +++ b/crates/ui-egui/src/panels/viewers.rs @@ -102,7 +102,9 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: u32, rect: Rect) { app.request_frame(cid, frame, scale); let key = app.frame_key(cid, frame, scale); if let Some((key, img)) = app.frames.get_for_viewer(&key, app.passive_tex.get(&id).map(|(k, _)| k)) { - set_texture(app, &ctx, id, key, img); + let dc = effectcraft_engine::viewer::DisplayColor::of(&app.session); + let dkey = display_hash(&super::viewer_tools::display_key(app, dc.is_some())); + set_texture(app, &ctx, id, key, img, dc, dkey); } match app.passive_tex.get(&id).filter(|(k, _)| k.comp == cid.0) { Some((_, tex)) => { @@ -126,16 +128,38 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: u32, rect: Rect) { } /// Keep the frame viewer `id` shows in its texture. -fn set_texture(app: &mut EffectcraftApp, ctx: &egui::Context, id: u32, key: FrameKey, img: FrameImage) { +fn set_texture( + app: &mut EffectcraftApp, + ctx: &egui::Context, + id: u32, + key: FrameKey, + img: FrameImage, + dc: Option, + dkey: u64, +) { + let dc_id = egui::Id::new(("passive-viewer-display", id)); + let same_display = ctx.data(|d| d.get_temp::(dc_id)) == Some(dkey); if let Some((k, _)) = app.passive_tex.get_mut(&id) && *k == key + && same_display { // An undo or an edit elsewhere can reuse this content at a newer revision. Keep that // revision as the lower bound for subsequent intermediate edit frames. k.revision = key.revision; return; } + ctx.data_mut(|d| d.insert_temp(dc_id, dkey)); let opts = super::viewer::zoom_texture_options(app.session.prefs.viewer_zoom_smooth()); + // The monitor conversion the active viewer applies (Use Display Color Management). + let img = match (img, &dc) { + (FrameImage::Cpu(img), Some(dc)) => { + let mut px: Vec<[u8; 4]> = img.pixels.iter().map(|c| c.to_array()).collect(); + dc.apply(&mut px); + let pixels = px.into_iter().map(|a| Color32::from_rgba_premultiplied(a[0], a[1], a[2], a[3])).collect(); + FrameImage::Cpu(Arc::new(egui::ColorImage::new(img.size, pixels))) + } + (img, _) => img, + }; let tex = match (img, app.passive_tex.remove(&id)) { (FrameImage::Cpu(img), Some((_, PassiveTexture::Cpu(mut h)))) if h.size() == crate::frames::fitted_size(img.size, crate::frames::max_texture_side(ctx)) => @@ -165,6 +189,14 @@ fn set_texture(app: &mut EffectcraftApp, ctx: &egui::Context, id: u32, key: Fram app.passive_tex.insert(id, (key, tex)); } +/// A hash of the display conversion's inputs (see `viewer_tools::display_key`). +fn display_hash(key: &str) -> u64 { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + key.hash(&mut h); + h.finish() +} + /// Give a passive viewer's texture back (GPU textures are registered with the renderer). fn free(app: &EffectcraftApp, tex: Option) { if let (Some(PassiveTexture::Gpu(id, _)), Some(rs)) = (tex, &app.wgpu) { diff --git a/crates/ui-egui/tests/viewer_passive_display.rs b/crates/ui-egui/tests/viewer_passive_display.rs new file mode 100644 index 00000000..797689af --- /dev/null +++ b/crates/ui-egui/tests/viewer_passive_display.rs @@ -0,0 +1,58 @@ +//! A passive Composition viewer applies the same monitor conversion as the active one (#547). +use effectcraft_engine::Session; +use effectcraft_ui_egui::{EffectcraftApp, menus, panels::viewers}; +use egui::{Color32, Context, ImageData, RawInput, Rect, pos2, vec2}; +use serde_json::json; +use std::collections::HashMap; + +fn frames(app: &mut EffectcraftApp, ctx: &Context, textures: &mut HashMap>) { + for _ in 0..4 { + let mut output = ctx.run_ui(RawInput { screen_rect: Some(Rect::from_min_size(pos2(0.0, 0.0), vec2(256.0, 256.0))), ..Default::default() }, |ui| { + viewers::show(app, ui, 0, Rect::from_min_size(pos2(0.0, 0.0), vec2(128.0, 256.0))); + viewers::show(app, ui, 1, Rect::from_min_size(pos2(128.0, 0.0), vec2(128.0, 256.0))); + }); + for (&id, deltas) in &output.textures_delta.set { + let Some(name) = ctx.tex_manager().read().meta(id).map(|m| m.name.clone()) else { continue }; + for delta in deltas { + let ImageData::Color(image) = &delta.image; + if delta.pos.is_none() { + textures.insert(name.clone(), image.pixels.clone()); + } + } + } + output.textures_delta.clear(); + std::thread::sleep(std::time::Duration::from_millis(100)); + } +} + +fn pixel(textures: &HashMap>, name: &str) -> [u8; 4] { + let p = &textures[name]; + p[p.len() / 2].to_array() +} + +#[test] +fn passive_viewer_matches_the_monitor_conversion() { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "Display test", "width": 16, "height": 16, "duration": 1})).unwrap(); + s.execute("layer.newSolid", json!({"name": "Color", "color": "#406080", "width": 16, "height": 16})).unwrap(); + s.execute("edit.deselectAll", json!({})).unwrap(); + let mut app = EffectcraftApp::new(s); + let ctx = Context::default(); + menus::invoke(&mut app, &ctx, "view.newViewer", json!({})).unwrap(); + let mut textures = HashMap::new(); + app.session.execute("file.projectSettings", json!({"workingSpace": "rec2020"})).unwrap(); + app.session.execute("prefs.set", json!({"key": "previews.displayProfile", "value": "p3"})).unwrap(); + frames(&mut app, &ctx, &mut textures); + assert_eq!(pixel(&textures, "viewer-display"), [37, 89, 122, 255]); + assert_eq!(pixel(&textures, "viewer-0"), [37, 89, 122, 255]); + // Changing which viewer is active keeps the conversion in both. + viewers::activate(&mut app, &ctx, 0); + frames(&mut app, &ctx, &mut textures); + assert_eq!(pixel(&textures, "viewer-display"), [37, 89, 122, 255]); + assert_eq!(pixel(&textures, "viewer-1"), [37, 89, 122, 255]); + // No output-to-monitor conversion needed: both show the raw frame. + app.session.execute("prefs.set", json!({"key": "previews.displayProfile", "value": "srgb"})).unwrap(); + frames(&mut app, &ctx, &mut textures); + assert_eq!(pixel(&textures, "viewer-frame"), [0, 90, 125, 255]); + assert_eq!(pixel(&textures, "viewer-1"), [0, 90, 125, 255]); +}