Merge pr/1511

# Conflicts:
#	contributors/people.toml
This commit is contained in:
Claude
2026-10-09 02:22:03 +00:00
4 changed files with 168 additions and 20 deletions
+5
View File
@@ -39,3 +39,8 @@ real_name = "Arthur Vinicius"
[people.SckraT]
sync_github_name = true
[people.GamerWil26]
real_name = "Willy Foo"
display_name = "Willy Foo"
sync_github_name = false
@@ -0,0 +1,38 @@
//! Deterministic 24 MP blur/resize benchmark. Run with --release; no private photos needed.
use photocraft_algo::{
FilterParams, apply_in,
resample::{Resample, resize_surface_in_canvas},
};
use photocraft_color::{ColorMode, PixelFormat, SampleType};
use photocraft_geom::Rect;
use photocraft_raster::Surface;
use std::time::Instant;
fn main() {
let bounds = Rect::new(0, 0, 6000, 4000);
let mut src = Surface::new(PixelFormat::new(ColorMode::Rgb, SampleType::U8, true));
for y in (0..4000).step_by(128) {
let r = Rect::new(0, y, 6000, (y + 128).min(4000));
let mut data = Vec::new();
for yy in r.y0..r.y1 {
for x in r.x0..r.x1 {
data.extend_from_slice(&[(x % 251) as f32 / 250.0, (yy % 241) as f32 / 240.0, ((x + yy) % 239) as f32 / 238.0, 1.0]);
}
}
src.write_region(r, &data);
}
for op in ["blur3", "bicubic1791"] {
for rep in 0..4 {
let t = Instant::now();
let out = if op == "blur3" {
apply_in(&src, &FilterParams::GaussianBlur { radius: 3.0 }, bounds, bounds, None, bounds)
} else {
resize_surface_in_canvas(&src, 1791.0 / 6000.0, 1194.0 / 4000.0, Resample::Bicubic, bounds)
};
let elapsed = t.elapsed().as_secs_f64() * 1000.0;
let sample = out.read_region(Rect::new(100, 100, 140, 130));
let checksum = sample.iter().fold(0u64, |s, v| s.wrapping_mul(31).wrapping_add(v.to_bits() as u64));
println!("{op} rep={rep} ms={elapsed:.3} checksum={checksum}");
std::hint::black_box(out);
}
}
}
+58 -6
View File
@@ -55,12 +55,21 @@ pub(crate) fn conv_sep(src: &Image, out: Rect, kx: &[f32], ky: &[f32], alpha: bo
for ty in 0..th {
let row = &p[ty * ww * n..(ty + 1) * ww * n];
let dst = &mut tmp[ty * ow * n..(ty + 1) * ow * n];
for ox in 0..ow {
let d = &mut dst[ox * n..(ox + 1) * n];
for (i, kv) in kx.iter().enumerate() {
let s = &row[(ox + i) * n..(ox + i + 1) * n];
for c in 0..n {
d[c] += s[c] * kv;
match n {
1 => conv_row::<1>(row, dst, kx),
2 => conv_row::<2>(row, dst, kx),
3 => conv_row::<3>(row, dst, kx),
4 => conv_row::<4>(row, dst, kx),
5 => conv_row::<5>(row, dst, kx),
_ => {
for ox in 0..ow {
let d = &mut dst[ox * n..(ox + 1) * n];
for (i, kv) in kx.iter().enumerate() {
let s = &row[(ox + i) * n..(ox + i + 1) * n];
for c in 0..n {
d[c] += s[c] * kv;
}
}
}
}
}
@@ -80,6 +89,21 @@ pub(crate) fn conv_sep(src: &Image, out: Rect, kx: &[f32], ky: &[f32], alpha: bo
res
}
// Fixed channel counts let LLVM keep one pixel's accumulators in registers and vectorize
// channels. The tap order is unchanged; unusual multichannel images keep the generic path.
fn conv_row<const N: usize>(src: &[f32], dst: &mut [f32], kernel: &[f32]) {
let pixels = src.as_chunks::<N>().0;
for (x, out) in dst.as_chunks_mut::<N>().0.iter_mut().enumerate() {
let mut acc = [0.0f32; N];
for (pixel, weight) in pixels.get(x..).unwrap_or(&[]).iter().zip(kernel) {
for c in 0..N {
acc[c] += pixel[c] * weight;
}
}
*out = acc;
}
}
pub(crate) fn gaussian(src: &Image, out: Rect, ctx: &Ctx, radius: f32) -> Vec<f32> {
// Exact kernel for small radii; beyond that three box passes approximate the Gaussian with
// cost independent of the radius (Kovesi, "Fast almost-Gaussian filtering", 2010).
@@ -803,6 +827,34 @@ mod tests {
assert!(coarse < expected * 0.1, "former 64-interval sampling unexpectedly resolved the patch: {coarse} vs {expected}");
}
#[test]
fn fixed_channel_convolution_matches_scalar_tap_order() {
fn check<const N: usize>() {
for width in [1, 7, 256] {
for radius in [0.0, 0.5, 3.0, 4.0] {
let k = gaussian_kernel(radius);
let src: Vec<f32> = (0..(width + k.len() - 1) * N).map(|i| ((i * 73) % 251) as f32 / 250.0).collect();
let mut expected = vec![0.0f32; width * N];
for x in 0..width {
for (i, w) in k.iter().enumerate() {
for c in 0..N {
expected[x * N + c] += src[(x + i) * N + c] * w;
}
}
}
let mut actual = vec![0.0f32; width * N];
conv_row::<N>(&src, &mut actual, &k);
assert_eq!(actual, expected, "channels={N} width={width} radius={radius}");
}
}
}
check::<1>();
check::<2>();
check::<3>();
check::<4>();
check::<5>();
}
#[test]
fn box_gaussian_matches_exact_kernel() {
// A hard vertical edge (premultiplied RGBA-ish, 2 channels) blurred with σ = 12.
+67 -14
View File
@@ -148,6 +148,41 @@ pub fn resize_surface_in_canvas(s: &Surface, sx: f64, sy: f64, filter: Resample,
/// Samples one source read may hold (64 MB of `f32`); larger windows are read in row chunks.
const READ_BUDGET: usize = 1 << 24;
// Preserve tap order and edge weights while exposing the common channel counts to LLVM.
fn horizontal_fixed<const N: usize>(row: &[f32], out: &mut [f32], taps: &[Taps], x_lo: i32, def: &[f32]) {
let pixels = row.as_chunks::<N>().0;
for (t, d) in taps.iter().zip(out.as_chunks_mut::<N>().0) {
let mut acc = [0.0f32; N];
for (k, w) in t.weights.iter().enumerate() {
if let Some(sp) = pixels.get((t.start + k as i32 - x_lo) as usize) {
for c in 0..N {
acc[c] += sp[c] * w;
}
}
}
for (c, v) in acc.iter_mut().enumerate() {
*v += def.get(c).copied().unwrap_or(0.0) * t.outside;
}
*d = acc;
}
}
fn horizontal_dynamic(row: &[f32], out: &mut [f32], taps: &[Taps], x_lo: i32, def: &[f32], n: usize) {
for (t, d) in taps.iter().zip(out.chunks_exact_mut(n)) {
for (k, w) in t.weights.iter().enumerate() {
let sx_ = (t.start + k as i32 - x_lo) as usize;
if let Some(sp) = row.get(sx_ * n..(sx_ + 1) * n) {
for c in 0..n {
d[c] += sp[c] * w;
}
}
}
for c in 0..n {
d[c] += def[c] * t.outside;
}
}
}
fn resize(s: &Surface, sx: f64, sy: f64, filter: Resample, edge: Option<Rect>) -> Surface {
resize_with_budget(s, sx, sy, filter, edge, READ_BUDGET)
}
@@ -174,20 +209,13 @@ fn resize_with_budget(s: &Surface, sx: f64, sy: f64, filter: Resample, edge: Opt
const BAND: usize = 64;
let bands: Vec<(usize, usize)> = (0..vy.len()).step_by(BAND).map(|b| (b, (b + BAND).min(vy.len()))).collect();
// Horizontal pass of one source row (`row` spans `x_lo..x_hi`) into `dw` samples.
let horizontal = |row: &[f32], out: &mut [f32]| {
for (t, d) in hx.iter().zip(out.chunks_exact_mut(n)) {
for (k, w) in t.weights.iter().enumerate() {
let sx_ = (t.start + k as i32 - x_lo) as usize;
if let Some(sp) = row.get(sx_ * n..(sx_ + 1) * n) {
for c in 0..n {
d[c] += sp[c] * w;
}
}
}
for c in 0..n {
d[c] += def[c] * t.outside;
}
}
let horizontal = |row: &[f32], out: &mut [f32]| match n {
1 => horizontal_fixed::<1>(row, out, &hx, x_lo, &def),
2 => horizontal_fixed::<2>(row, out, &hx, x_lo, &def),
3 => horizontal_fixed::<3>(row, out, &hx, x_lo, &def),
4 => horizontal_fixed::<4>(row, out, &hx, x_lo, &def),
5 => horizontal_fixed::<5>(row, out, &hx, x_lo, &def),
_ => horizontal_dynamic(row, out, &hx, x_lo, &def, n),
};
// A source row of nothing but the default pixel, after the horizontal pass.
let def_row = {
@@ -313,6 +341,31 @@ mod tests {
s
}
#[test]
fn fixed_channel_rows_match_dynamic_resampling() {
fn check<const N: usize>() {
for scale in [0.12, 0.5, 1.0, 2.3] {
for filter in [Resample::Nearest, Resample::Bilinear, Resample::Bicubic, Resample::Lanczos] {
for clamp in [None, Some((-3, 31))] {
let ts = taps(-5, 41, scale, filter, clamp, (-3, 31));
let row: Vec<f32> = (0..35 * N).map(|i| ((i * 73) % 251) as f32 / 250.0).collect();
let def: Vec<f32> = (0..N).map(|i| i as f32 / 7.0).collect();
let mut expected = vec![0.0; ts.len() * N];
let mut actual = expected.clone();
horizontal_dynamic(&row, &mut expected, &ts, -3, &def, N);
horizontal_fixed::<N>(&row, &mut actual, &ts, -3, &def);
assert_eq!(actual, expected, "N={N} scale={scale} filter={filter:?} clamp={clamp:?}");
}
}
}
}
check::<1>();
check::<2>();
check::<3>();
check::<4>();
check::<5>();
}
#[test]
fn identity_scale_is_lossless() {
for f in [Resample::Nearest, Resample::Bilinear, Resample::Bicubic, Resample::Lanczos] {