M5.1: cache-friendly Gaussian blur (no transposes; in-row horizontal passes, banded running-sum vertical passes) + per-stage pipeline profiling
3–10x faster blurs on a 3000x2400 plane (min-of-9; machine under load, see blur_bench example). LIGHTCRAFT_PROFILE=1 now also times log-lum, base, clarity, texture, dehaze, masks and NR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
17057f7bbc
commit
3cc97c58d1
|
|
@ -139,6 +139,23 @@ pub(crate) fn is_bw(s: &DevelopSettings) -> bool {
|
|||
}
|
||||
|
||||
/// Parallel map over output rows with index.
|
||||
/// `LIGHTCRAFT_PROFILE` is set: print per-stage timings to stderr.
|
||||
pub(crate) fn profiling() -> bool {
|
||||
static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ON.get_or_init(|| std::env::var_os("LIGHTCRAFT_PROFILE").is_some())
|
||||
}
|
||||
|
||||
/// Run `f`, printing its duration under `LIGHTCRAFT_PROFILE`.
|
||||
pub(crate) fn timed<R>(what: &str, f: impl FnOnce() -> R) -> R {
|
||||
if !profiling() {
|
||||
return f();
|
||||
}
|
||||
let t = std::time::Instant::now();
|
||||
let r = f();
|
||||
eprintln!(" {what}: {:.1} ms", t.elapsed().as_secs_f64() * 1e3);
|
||||
r
|
||||
}
|
||||
|
||||
pub(crate) fn for_rows<T: Send>(data: &mut [T], w: usize, f: impl Fn(usize, &mut [T]) + Sync + Send) {
|
||||
par_rows(data, w, f)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use lightcraft_raster::blur::gaussian;
|
|||
use lightcraft_raster::{Plane, Rgb32f};
|
||||
|
||||
use crate::geometry::Frame;
|
||||
use crate::{Prepared, Quality, SourceInfo, for_rows, masks};
|
||||
use crate::{Prepared, Quality, SourceInfo, for_rows, masks, timed};
|
||||
|
||||
/// White balance (relative to the source's as-shot white) and exposure, in place.
|
||||
pub fn scene_linear_pre(img: &mut Rgb32f, info: &SourceInfo, s: &DevelopSettings) {
|
||||
|
|
@ -120,6 +120,7 @@ pub fn denoise(img: &mut Rgb32f, s: &DevelopSettings, src_long: usize, out_long:
|
|||
let scale = (out_long as f32 / src_long.max(1) as f32).clamp(0.05, 1.0);
|
||||
let w = img.width;
|
||||
if lum > 0.0 {
|
||||
let _t = crate::profiling().then(std::time::Instant::now);
|
||||
let l = img.map(log_lum);
|
||||
let detail = (s.detail.nr_detail / 100.0) as f32;
|
||||
let eps = 0.002 + lum * lum * 0.25 * (1.0 - 0.7 * detail);
|
||||
|
|
@ -133,8 +134,12 @@ pub fn denoise(img: &mut Rgb32f, s: &DevelopSettings, src_long: usize, out_long:
|
|||
*p = p.map(|v| v * g);
|
||||
}
|
||||
});
|
||||
if let Some(t) = _t {
|
||||
eprintln!(" nr luminance: {:.1} ms", t.elapsed().as_secs_f64() * 1e3);
|
||||
}
|
||||
}
|
||||
if col > 0.0 {
|
||||
let _t = crate::profiling().then(std::time::Instant::now);
|
||||
let chroma = img.map(|c| {
|
||||
let y = luminance_2020(c).max(1e-6);
|
||||
[c[0] / y, c[1] / y, c[2] / y]
|
||||
|
|
@ -152,6 +157,9 @@ pub fn denoise(img: &mut Rgb32f, s: &DevelopSettings, src_long: usize, out_long:
|
|||
*p = [0, 1, 2].map(|k| ((c0[k] + (cb[k] - c0[k]) * t) * yl).max(0.0));
|
||||
}
|
||||
});
|
||||
if let Some(t) = _t {
|
||||
eprintln!(" nr colour: {:.1} ms", t.elapsed().as_secs_f64() * 1e3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -160,7 +168,7 @@ pub fn log_lum(c: [f32; 3]) -> f32 {
|
|||
}
|
||||
|
||||
pub fn prepare(img: Rgb32f, s: &DevelopSettings, frame: &Frame, px_per_long: f64, q: Quality) -> Prepared {
|
||||
let log_l = img.map(log_lum);
|
||||
let log_l = timed("log_l", || img.map(log_lum));
|
||||
let ppl = px_per_long as f32;
|
||||
let tone_active =
|
||||
s.light.highlights != 0.0 || s.light.shadows != 0.0 || s.masks.iter().any(|m| m.adjust.highlights != 0.0 || m.adjust.shadows != 0.0);
|
||||
|
|
@ -168,19 +176,25 @@ pub fn prepare(img: Rgb32f, s: &DevelopSettings, frame: &Frame, px_per_long: f64
|
|||
// Edge-aware base at ~1.5% of the long edge (EV² epsilon: edges of > ~0.6 EV are preserved).
|
||||
let sigma = (0.015 * ppl).max(1.0);
|
||||
let sigma = if q == Quality::Draft { sigma.min(24.0) } else { sigma };
|
||||
guided_fast(&log_l, sigma, 0.35)
|
||||
timed("base", || guided_fast(&log_l, sigma, 0.35))
|
||||
} else {
|
||||
log_l.clone()
|
||||
};
|
||||
let local_any = |f: fn(&lightcraft_develop::LocalAdjustments) -> f64| s.masks.iter().any(|m| f(&m.adjust) != 0.0);
|
||||
let clarity_blur = (s.effects.clarity != 0.0 || local_any(|a| a.clarity)).then(|| guided_fast(&log_l, (0.012 * ppl).max(1.0), 0.8));
|
||||
let clarity_blur =
|
||||
(s.effects.clarity != 0.0 || local_any(|a| a.clarity)).then(|| timed("clarity", || guided_fast(&log_l, (0.012 * ppl).max(1.0), 0.8)));
|
||||
let texture_blur = (s.effects.texture != 0.0 || s.detail.sharpen_amount != 0.0 || local_any(|a| a.texture) || local_any(|a| a.sharpness))
|
||||
.then(|| gaussian(&log_l, (0.0018 * ppl).max(0.6)));
|
||||
.then(|| timed("texture", || gaussian(&log_l, (0.0018 * ppl).max(0.6))));
|
||||
let dark = (s.effects.dehaze != 0.0 || local_any(|a| a.dehaze)).then(|| {
|
||||
let _t = crate::profiling().then(std::time::Instant::now);
|
||||
let min_c = img.map(|c| c[0].min(c[1]).min(c[2]));
|
||||
gaussian(&min_c, (0.02 * ppl).max(1.0))
|
||||
let d = gaussian(&min_c, (0.02 * ppl).max(1.0));
|
||||
if let Some(t) = _t {
|
||||
eprintln!(" dehaze: {:.1} ms", t.elapsed().as_secs_f64() * 1e3);
|
||||
}
|
||||
d
|
||||
});
|
||||
let masks = masks::evaluate(&s.masks, frame, img.width, img.height, &img, &log_l);
|
||||
let masks = timed("masks", || masks::evaluate(&s.masks, frame, img.width, img.height, &img, &log_l));
|
||||
Prepared { img, log_l, base, clarity_blur, texture_blur, dark, masks, px_per_long }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
//! Micro-benchmark for the blur/map primitives (min of N runs, robust to a loaded machine):
|
||||
//! `RAYON_NUM_THREADS=1 cargo run --release -p lightcraft-raster --example blur_bench`.
|
||||
use lightcraft_raster::{Plane, Rgb32f, blur::gaussian};
|
||||
use std::time::Instant;
|
||||
|
||||
fn best(n: usize, mut f: impl FnMut()) -> f64 {
|
||||
(0..n)
|
||||
.map(|_| {
|
||||
let t = Instant::now();
|
||||
f();
|
||||
t.elapsed().as_secs_f64() * 1e3
|
||||
})
|
||||
.fold(f64::MAX, f64::min)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let (w, h) = (3000, 2400);
|
||||
let p = Plane::from_fn(w, h, |x, y| ((x * 7 + y * 13) % 255) as f32 / 255.0);
|
||||
let c = Rgb32f::from_fn(w, h, |x, y| [x as f32 / w as f32, y as f32 / h as f32, 0.5]);
|
||||
for sigma in [1.0f32, 4.0, 24.0] {
|
||||
println!("plane gaussian σ={sigma}: {:.1} ms", best(9, || drop(gaussian(&p, sigma))));
|
||||
}
|
||||
println!("rgb gaussian σ=4: {:.1} ms", best(9, || drop(gaussian(&c, 4.0))));
|
||||
}
|
||||
|
|
@ -17,37 +17,58 @@ fn box_radii(sigma: f32) -> [usize; 3] {
|
|||
std::array::from_fn(|i| (((if (i as i32) < m { wl } else { wu }) - 1) / 2).max(0) as usize)
|
||||
}
|
||||
|
||||
fn box_h<T: Pixel>(src: &Image<T>, r: usize) -> Image<T> {
|
||||
let w = src.width;
|
||||
let mut out = Image::<T>::new(w, src.height);
|
||||
if r == 0 {
|
||||
return src.clone();
|
||||
/// One horizontal box pass over `row` (clamped edges), using `tmp` as scratch.
|
||||
#[inline]
|
||||
fn box_row<T: Pixel>(row: &mut [T], tmp: &mut Vec<T>, r: usize) {
|
||||
let w = row.len();
|
||||
if r == 0 || w == 0 {
|
||||
return;
|
||||
}
|
||||
tmp.clear();
|
||||
tmp.extend_from_slice(row);
|
||||
let s = &tmp[..];
|
||||
let last = w - 1;
|
||||
let inv = 1.0 / (2 * r + 1) as f32;
|
||||
par_rows(&mut out.data, w, |y, row| {
|
||||
let s = src.row(y);
|
||||
let at = |i: isize| s[i.clamp(0, w as isize - 1) as usize];
|
||||
let mut acc = T::zero();
|
||||
for i in -(r as isize)..=(r as isize) {
|
||||
acc = acc.madd(at(i), 1.0);
|
||||
}
|
||||
for x in 0..w {
|
||||
row[x] = T::zero().madd(acc, inv);
|
||||
acc = acc.madd(at(x as isize + r as isize + 1), 1.0).madd(at(x as isize - r as isize), -1.0);
|
||||
}
|
||||
});
|
||||
out
|
||||
let mut acc = T::zero();
|
||||
for i in 0..=2 * r {
|
||||
acc = acc.madd(s[i.saturating_sub(r).min(last)], 1.0);
|
||||
}
|
||||
for x in 0..w {
|
||||
row[x] = T::zero().madd(acc, inv);
|
||||
acc = acc.madd(s[(x + r + 1).min(last)], 1.0).madd(s[x.saturating_sub(r)], -1.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn transpose<T: Pixel>(src: &Image<T>) -> Image<T> {
|
||||
/// Rows per parallel band in the vertical pass (each band re-primes its running sums).
|
||||
const BAND: usize = 32;
|
||||
|
||||
/// One vertical box pass `src → dst` (clamped edges): a running sum per column, vectorized across
|
||||
/// the row, parallel over horizontal bands. Cache-friendly — no transposes.
|
||||
fn box_v<T: Pixel>(src: &Image<T>, dst: &mut Image<T>, r: usize) {
|
||||
let (w, h) = (src.width, src.height);
|
||||
let mut out = Image::<T>::new(h, w);
|
||||
par_rows(&mut out.data, h, |y, row| {
|
||||
for (x, o) in row.iter_mut().enumerate() {
|
||||
*o = src.data[x * w + y];
|
||||
let inv = 1.0 / (2 * r + 1) as f32;
|
||||
let last = h - 1;
|
||||
let row = |y: usize| &src.data[y * w..(y + 1) * w];
|
||||
par_rows(&mut dst.data, w * BAND, |band, out| {
|
||||
let y0 = band * BAND;
|
||||
let mut acc = vec![T::zero(); w];
|
||||
for i in 0..=2 * r {
|
||||
let yy = (y0 + i).saturating_sub(r).min(last);
|
||||
for (a, v) in acc.iter_mut().zip(row(yy)) {
|
||||
*a = a.madd(*v, 1.0);
|
||||
}
|
||||
}
|
||||
for (k, o) in out.chunks_mut(w).enumerate() {
|
||||
let y = y0 + k;
|
||||
for (d, a) in o.iter_mut().zip(&acc) {
|
||||
*d = T::zero().madd(*a, inv);
|
||||
}
|
||||
let (add, sub) = (row((y + r + 1).min(last)), row(y.saturating_sub(r)));
|
||||
for ((a, p), m) in acc.iter_mut().zip(add).zip(sub) {
|
||||
*a = a.madd(*p, 1.0).madd(*m, -1.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// Blur with Gaussian `sigma` (pixels). `sigma <= 0.3` returns a copy.
|
||||
|
|
@ -57,14 +78,21 @@ pub fn gaussian<T: Pixel>(img: &Image<T>, sigma: f32) -> Image<T> {
|
|||
}
|
||||
let radii = box_radii(sigma);
|
||||
let mut a = img.clone();
|
||||
par_rows(&mut a.data, img.width, |_, row| {
|
||||
let mut tmp = Vec::with_capacity(row.len());
|
||||
for r in radii {
|
||||
box_row(row, &mut tmp, r);
|
||||
}
|
||||
});
|
||||
let mut b = Image::<T>::new(img.width, img.height);
|
||||
for r in radii {
|
||||
a = box_h(&a, r);
|
||||
if r == 0 {
|
||||
continue;
|
||||
}
|
||||
box_v(&a, &mut b, r);
|
||||
std::mem::swap(&mut a, &mut b);
|
||||
}
|
||||
let mut t = transpose(&a);
|
||||
for r in radii {
|
||||
t = box_h(&t, r);
|
||||
}
|
||||
transpose(&t)
|
||||
a
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
Loading…
Reference in New Issue