diff --git a/crates/pipeline/src/lib.rs b/crates/pipeline/src/lib.rs index f7c33ee9e..05f0ff8a5 100644 --- a/crates/pipeline/src/lib.rs +++ b/crates/pipeline/src/lib.rs @@ -358,6 +358,9 @@ pub struct Plan<'a> { pub geo: u64, /// Key of the white-balanced, retouched, denoised image (and of the planes computed from it). pub lin_key: u64, + /// [`Self::lin_key`] without the noise-reduction settings: draft renders reuse the smooth + /// planes of an image that differs from this one only in its noise reduction. + pub pre_nr_key: u64, /// Red eye / pet eye corrections with their detected pupils, in output pixels. pub eyes: Vec, /// The whole frame's dehaze airlight, for a windowed render (which can't see the whole frame). @@ -418,7 +421,7 @@ pub fn plan<'a>(src: &Rgb32f, info: &SourceInfo, s: &'a DevelopSettings, req: &R let (wb_t, wb_tint) = local::effective_wb(info, s); let eyes = redeye::resolve(src, &s.red_eye, s.orientation, &frame, w, h, px_per_long); let d = &s.detail; - let lin_key = hash_of(( + let pre_nr_key = hash_of(( geo, [wb_t, wb_tint, info.as_shot_temp, info.as_shot_tint].map(f64::to_bits), format!("{:?}", s.spots), @@ -427,10 +430,10 @@ pub fn plan<'a>(src: &Rgb32f, info: &SourceInfo, s: &'a DevelopSettings, req: &R format!("{:?}", s.optics), // and the local tone mapping local_tone::enabled(info, s), - [d.nr_luminance, d.nr_detail, d.nr_color, d.nr_color_detail, d.nr_color_smoothness].map(f64::to_bits), src_long, )); - Plan { settings, frame, w, h, px_per_long, src_long, geo, lin_key, eyes, fixed_air, keep, mattes: info.mattes.clone() } + let lin_key = hash_of((pre_nr_key, [d.nr_luminance, d.nr_detail, d.nr_color, d.nr_color_detail, d.nr_color_smoothness].map(f64::to_bits))); + Plan { settings, frame, w, h, px_per_long, src_long, geo, lin_key, pre_nr_key, eyes, fixed_air, keep, mattes: info.mattes.clone() } } /// Whether the scene-linear stage needs work only the CPU does (defringe, spot removal). @@ -521,7 +524,12 @@ fn render_impl(src: Src<'_>, info: &SourceInfo, s: &DevelopSettings, req: &Rende lap("wb/spots/nr", &mut t); let mut planes = match cached.map(|e| e.planes) { Some(p) if p.key == lin_key => p, - _ => local::Planes { key: lin_key, ..Default::default() }, + // A draft while noise reduction changes (a slider drag): the wide, edge-aware planes of the + // image before the change stand in (noise reduction barely moves a blur that wide); the + // per-pixel log luminance and the fine texture band are redone. Keyed so a full-quality + // render never takes them. + Some(p) if req.quality == Quality::Draft && p.pre_nr == plan.pre_nr_key => p.for_other_nr(), + _ => local::Planes { key: lin_key, pre_nr: plan.pre_nr_key, ..Default::default() }, }; let mut prep = local::prepare(lin.clone(), s, frame, px_per_long, req.quality, &mut planes, plan.mattes.as_deref()); if let Some(a) = plan.fixed_air { diff --git a/crates/pipeline/src/local.rs b/crates/pipeline/src/local.rs index fd7fefd34..c74d992f1 100644 --- a/crates/pipeline/src/local.rs +++ b/crates/pipeline/src/local.rs @@ -210,6 +210,8 @@ pub fn log_lum(c: [f32; 3]) -> f32 { #[derive(Clone, Default)] pub(crate) struct Planes { pub key: u64, + /// The image's key without its noise reduction (`Plan::pre_nr_key`). + pub pre_nr: u64, pub log_l: Option>, pub base: Option<(u32, Arc)>, pub clarity: Option<(u32, Arc)>, @@ -220,6 +222,15 @@ pub(crate) struct Planes { pub chroma: Option<(u32, Arc)>, } +impl Planes { + /// The planes that still fit an image differing from this one only in noise reduction, close + /// enough for a draft: the wide blurs (base, clarity, dehaze, chromaticity). Its `key` matches + /// no image, so only another draft takes them. + pub fn for_other_nr(self) -> Planes { + Planes { key: 0, pre_nr: self.pre_nr, log_l: None, texture: None, ..self } + } +} + /// Reuse `slot` if it was computed at `sigma`, else compute and store it. fn plane_at(slot: &mut Option<(u32, Arc)>, sigma: f32, f: impl FnOnce() -> Plane) -> Arc { match slot { diff --git a/crates/pipeline/src/tests.rs b/crates/pipeline/src/tests.rs index 185d96b31..54a75ff8d 100644 --- a/crates/pipeline/src/tests.rs +++ b/crates/pipeline/src/tests.rs @@ -206,7 +206,13 @@ fn stage_cache_matches_uncached_render_through_a_slider_session() { for req in [&full, &draft] { let a = render_cached(&src, &info, &s, req, &cache).image; let b = render(&src, &info, &s, req).image; - assert_eq!(a, b, "step {i} {:?}", req.quality); + if req.quality == Quality::Draft && i == 6 { + // a draft while noise reduction changes keeps the wide planes of the image before it + let d = max_diff(&a, &b); + assert!(d <= 3, "step {i} draft: {d}"); + } else { + assert_eq!(a, b, "step {i} {:?}", req.quality); + } } } assert_eq!(cache.len(), 2); @@ -220,6 +226,35 @@ fn stage_cache_matches_uncached_render_through_a_slider_session() { assert_eq!(render_cached(&other, &info, &s, &full, &cache).image, render(&other, &info, &s, &full).image); } +#[test] +fn drafts_keep_wide_planes_across_a_noise_reduction_change_and_full_renders_do_not() { + use crate::{Quality, StageCache, render_cached}; + use std::sync::Arc; + let src = Arc::new(scene()); + let info = SourceInfo { raw: true, ..Default::default() }; + let cache = StageCache::default(); + let draft = RenderRequest { quality: Quality::Draft, ..RenderRequest::fit(160, 160) }; + let full = RenderRequest { quality: Quality::Full, ..draft }; + let mut s = typical_edits(); + s.effects.clarity = 60.0; + render_cached(&src, &info, &s, &draft, &cache); + let clarity_plane = |c: &StageCache| c.entries.lock().unwrap().first().and_then(|e| e.planes.clarity.clone()).unwrap().1; + let first = clarity_plane(&cache); + s.detail.nr_luminance = 50.0; + render_cached(&src, &info, &s, &draft, &cache); + assert!(Arc::ptr_eq(&first, &clarity_plane(&cache)), "a draft redid the clarity plane for a noise-reduction change"); + for v in [10.0, 45.0, 80.0] { + s.detail.nr_luminance = v; + s.detail.nr_color = v / 2.0; + let a = render_cached(&src, &info, &s, &draft, &cache).image; + let d = max_diff(&a, &render(&src, &info, &s, &draft).image); + // close to the exact draft (a few levels at edges, where clarity is strong) + assert!(d <= 8, "nr {v}: {d}"); + // a full-quality render after the drafts is exact (it never takes the stand-in planes) + assert_eq!(render_cached(&src, &info, &s, &full, &cache).image, render(&src, &info, &s, &full).image, "nr {v}"); + } +} + #[test] fn exposure_after_spatial_filters_equals_exposing_the_source() { // The pipeline filters the image before exposure and applies exposure per pixel; filters on