From ddc347cc55cb497b96b1f6a392bb49adc3d7c9ca Mon Sep 17 00:00:00 2001 From: eliets2 <287273517+eliets2@users.noreply.github.com> Date: Wed, 14 Oct 2026 02:11:50 +0300 Subject: [PATCH] Reject non-finite denoise inputs and derived pixels --- crates/denoise-core/README.md | 5 +++ crates/denoise-core/src/run.rs | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/crates/denoise-core/README.md b/crates/denoise-core/README.md index 74d1e3247..4294f9369 100644 --- a/crates/denoise-core/README.md +++ b/crates/denoise-core/README.md @@ -5,3 +5,8 @@ No weights or inference backend. See [the user workflow](../../docs/denoise.md). `lightcraft-denoise` re-exports these types for CPU/GPU users; the engine uses this crate directly when inference is disabled. The default download catalog is empty. Run `cargo test -p lightcraft-denoise-core`; optional catalog checks use `--features rawnind-model`. + +The Bayer runner rejects non-finite source samples before inference and non-finite +results after gain matching, highlight protection and tile blending. Finite negative +and HDR source samples remain supported. These guards do not implement Raw Details +or Super Resolution. diff --git a/crates/denoise-core/src/run.rs b/crates/denoise-core/src/run.rs index 1df96a9e1..987a859b9 100644 --- a/crates/denoise-core/src/run.rs +++ b/crates/denoise-core/src/run.rs @@ -95,6 +95,9 @@ pub fn denoise_bayer( if mosaic.len() != pixels { return Err(Error::Input("the mosaic does not have width × height samples".into())); } + if mosaic.iter().any(|v| !v.is_finite()) { + return Err(Error::Input("the mosaic has samples that are not finite".into())); + } if !(16..=2048).contains(&p.tile) || p.overlap >= p.tile.div_ceil(2) || layout.dx > 1 || layout.dy > 1 { return Err(Error::Input("the tile size and overlap do not fit".into())); } @@ -148,6 +151,9 @@ pub fn denoise_bayer( return Err(Error::Model("numbers that are not finite".into())); } finish_tile(&input, &mut out, p, (bx, ax, by, ay)); + if out.iter().any(|v| !v.is_finite()) { + return Err(Error::Model("numbers that are not finite after scale matching or highlight protection".into())); + } Ok(out) }) .collect() @@ -201,6 +207,9 @@ pub fn denoise_bayer( } } }); + if acc.iter().flatten().any(|v| !v.is_finite()) { + return Err(Error::Model("numbers that are not finite after tile blending".into())); + } if std::env::var_os("LIGHTCRAFT_PROFILE").is_some() { eprintln!( "[denoise] {total} tiles, {} at once: tiles {:.0} ms (blending, {:.0} ms of it, overlaps the model), normalise {:.0} ms", @@ -388,6 +397,53 @@ mod tests { Params { tile, overlap, gain, clip: None, parallel: 2 } } + #[test] + fn nonfinite_mosaics_are_rejected_before_inference() { + struct Counting(std::sync::atomic::AtomicUsize); + impl TileRunner for Counting { + fn run(&self, _: &[f32]) -> Result, Error> { + self.0.fetch_add(1, Ordering::Relaxed); + Ok(vec![0.0; 3 * 32 * 32]) + } + } + let model = Counting(std::sync::atomic::AtomicUsize::new(0)); + let layout = Layout { dx: 0, dy: 0 }; + for sample in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] { + assert!(matches!(denoise_bayer(&[sample], 1, 1, layout, &model, ¶ms(16, 0, Gain::None), &Control::default()), Err(Error::Input(_)))); + } + assert_eq!(model.0.load(Ordering::Relaxed), 0); + // Finite scene-linear samples can be negative or exceed SDR white. + for sample in [-0.25, 2.0] { + assert!(denoise_bayer(&[sample], 1, 1, layout, &model, ¶ms(16, 0, Gain::None), &Control::default()).is_ok()); + } + } + + #[test] + fn finite_model_output_that_overflows_during_gain_matching_is_rejected() { + struct Extreme; + impl TileRunner for Extreme { + fn run(&self, _: &[f32]) -> Result, Error> { + Ok(vec![f32::MAX; 3 * 32 * 32]) + } + } + let p = params(16, 0, Gain::MatchMean { nominal: 1e-6, max_deviation: 0.0 }); + assert!(matches!(denoise_bayer(&[0.5], 1, 1, Layout { dx: 0, dy: 0 }, &Extreme, &p, &Control::default()), Err(Error::Model(_)))); + } + + #[test] + fn overlapping_finite_tiles_that_overflow_the_accumulator_are_rejected() { + struct Extreme; + impl TileRunner for Extreme { + fn run(&self, _: &[f32]) -> Result, Error> { + Ok(vec![f32::MAX; 3 * 32 * 32]) + } + } + assert!(matches!( + denoise_bayer(&vec![0.5; 48 * 48], 48, 48, Layout { dx: 0, dy: 0 }, &Extreme, ¶ms(16, 4, Gain::None), &Control::default()), + Err(Error::Model(_)) + )); + } + #[test] fn overflowing_tiles_and_nan_gain_are_errors_before_the_model_runs() { let layout = Layout::from_cell([0, 1, 1, 2]).unwrap();