From a4b7ff419bd9f801e97a89319e8809518ef6e1a2 Mon Sep 17 00:00:00 2001 From: Theophilus2003 <110519630+Theophilus2003@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:43:37 +0000 Subject: [PATCH] perf(desktop-gpui): resize picker thumbnails up to 4.3x faster with identical output --- .../kernels/benches/thumbnail_fit.rs | 5 + apps/desktop-gpui/kernels/src/thumbnail.rs | 188 +++++++++++++++++- 2 files changed, 191 insertions(+), 2 deletions(-) diff --git a/apps/desktop-gpui/kernels/benches/thumbnail_fit.rs b/apps/desktop-gpui/kernels/benches/thumbnail_fit.rs index 4fc40b7cfdf..80b66d2472b 100644 --- a/apps/desktop-gpui/kernels/benches/thumbnail_fit.rs +++ b/apps/desktop-gpui/kernels/benches/thumbnail_fit.rs @@ -25,6 +25,11 @@ fn main() { ("upscale", 160, 90, false), ("downscale", 640, 360, false), ("downscale-1080p", 1920, 1080, false), + ("window-1600x1000", 1600, 1000, false), + ("display-1440p", 2560, 1440, false), + ("display-ultrawide", 3440, 1440, false), + ("display-4k", 3840, 2160, false), + ("display-4k-alpha", 3840, 2160, true), ] { let rgba = RgbaImage::from_fn(width, height, |x, y| { image::Rgba([ diff --git a/apps/desktop-gpui/kernels/src/thumbnail.rs b/apps/desktop-gpui/kernels/src/thumbnail.rs index af46b0790e6..2212ed259bc 100644 --- a/apps/desktop-gpui/kernels/src/thumbnail.rs +++ b/apps/desktop-gpui/kernels/src/thumbnail.rs @@ -27,11 +27,10 @@ pub fn fit_centered(image: &RgbaImage, target_width: u32, target_height: u32) -> let resized = if (scaled_width, scaled_height) == image.dimensions() { Cow::Borrowed(image) } else { - Cow::Owned(image::imageops::resize( + Cow::Owned(resize_lanczos3( image, scaled_width.max(1), scaled_height.max(1), - image::imageops::FilterType::Lanczos3, )) }; @@ -45,6 +44,133 @@ pub fn fit_centered(image: &RgbaImage, target_width: u32, target_height: u32) -> canvas } +// Same weights and per-pixel f32 accumulation order as +// `image::imageops::resize(.., FilterType::Lanczos3)`, so the output is +// byte-identical; only the loop order changes, so whole blocks of a row stay in +// registers and vectorize instead of one generic `get_pixel` per tap. +fn resize_lanczos3(image: &RgbaImage, width: u32, height: u32) -> RgbaImage { + let source_row = image.width() as usize * 4; + let mut vertical = vec![0f32; source_row * height as usize]; + vertical_pass( + &mut vertical, + image.as_raw(), + &lanczos3_taps(image.height(), height), + source_row, + ); + + let columns = lanczos3_taps(image.width(), width); + let mut resized = RgbaImage::new(width, height); + for (output, row) in resized + .chunks_exact_mut(width as usize * 4) + .zip(vertical.chunks_exact(source_row)) + { + for (pixel, taps) in output.chunks_exact_mut(4).zip(&columns) { + let mut sum = [0f32; 4]; + for (index, weight) in taps.weights.iter().enumerate() { + let start = (taps.start + index) * 4; + for (channel, &value) in sum.iter_mut().zip(&row[start..start + 4]) { + *channel += value * weight; + } + } + for (channel, value) in pixel.iter_mut().zip(sum) { + *channel = value.clamp(0.0, 255.0).round() as u8; + } + } + } + resized +} + +fn vertical_pass(vertical: &mut [f32], pixels: &[u8], rows: &[Taps], source_row: usize) { + #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] + if is_x86_feature_detected!("avx2") { + unsafe { vertical_pass_avx2(vertical, pixels, rows, source_row) }; + return; + } + vertical_pass_portable(vertical, pixels, rows, source_row); +} + +#[cfg(any(target_arch = "x86", target_arch = "x86_64"))] +#[target_feature(enable = "avx2")] +fn vertical_pass_avx2(vertical: &mut [f32], pixels: &[u8], rows: &[Taps], source_row: usize) { + vertical_pass_portable(vertical, pixels, rows, source_row); +} + +#[inline(always)] +fn vertical_pass_portable(vertical: &mut [f32], pixels: &[u8], rows: &[Taps], source_row: usize) { + for (row, taps) in vertical.chunks_exact_mut(source_row).zip(rows) { + let mut blocks = row.chunks_exact_mut(VERTICAL_BLOCK); + for (block_index, block) in blocks.by_ref().enumerate() { + let offset = block_index * VERTICAL_BLOCK; + let mut sums = [0f32; VERTICAL_BLOCK]; + for (index, &weight) in taps.weights.iter().enumerate() { + let start = (taps.start + index) * source_row + offset; + let values: &[u8; VERTICAL_BLOCK] = + pixels[start..start + VERTICAL_BLOCK].try_into().unwrap(); + for (sum, &value) in sums.iter_mut().zip(values) { + *sum += f32::from(value) * weight; + } + } + block.copy_from_slice(&sums); + } + let remainder = blocks.into_remainder(); + let offset = source_row - remainder.len(); + for (index, &weight) in taps.weights.iter().enumerate() { + let start = (taps.start + index) * source_row + offset; + for (sum, &value) in remainder.iter_mut().zip(&pixels[start..]) { + *sum += f32::from(value) * weight; + } + } + } +} + +const VERTICAL_BLOCK: usize = 32; + +struct Taps { + start: usize, + weights: Vec, +} + +fn lanczos3_taps(source: u32, target: u32) -> Vec { + let ratio = source as f32 / target as f32; + let scale = if ratio < 1.0 { 1.0 } else { ratio }; + let support = 3.0 * scale; + (0..target) + .map(|index| { + let center = (index as f32 + 0.5) * ratio; + let start = ((center - support).floor() as i64).clamp(0, i64::from(source) - 1); + let end = ((center + support).ceil() as i64).clamp(start + 1, i64::from(source)); + let center = center - 0.5; + let mut weights = (start..end) + .map(|tap| lanczos3((tap as f32 - center) / scale)) + .collect::>(); + let mut total = 0.0; + for weight in &weights { + total += weight; + } + for weight in &mut weights { + *weight /= total; + } + Taps { + start: start as usize, + weights, + } + }) + .collect() +} + +fn lanczos3(x: f32) -> f32 { + fn sinc(t: f32) -> f32 { + let a = t * std::f32::consts::PI; + if t == 0.0 { 1.0 } else { a.sin() / a } + } + + if x.abs() < 3.0 { + sinc(x) * sinc(x / 3.0) + } else { + 0.0 + } +} + #[cfg(test)] mod tests { use super::*; @@ -186,4 +312,62 @@ mod tests { } } } + + fn noise(width: u32, height: u32) -> RgbaImage { + let mut state = 0x9e37_79b9_7f4a_7c15_u64 ^ u64::from(width * 31 + height); + RgbaImage::from_fn(width, height, |_, _| { + state = state + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1_442_695_040_888_963_407); + image::Rgba((state >> 32).to_le_bytes()[..4].try_into().unwrap()) + }) + } + + #[test] + fn lanczos3_matches_image_resize_byte_for_byte() { + for (width, height, target_width, target_height) in [ + (3840, 2160, 320, 180), + (2560, 1600, 288, 180), + (1920, 1080, 320, 180), + (1366, 768, 320, 180), + (777, 1333, 104, 180), + (321, 181, 320, 180), + (320, 400, 320, 180), + (100, 60, 320, 180), + (1, 999, 1, 180), + (999, 1, 320, 1), + ] { + let source = noise(width, height); + let expected = image::imageops::resize( + &source, + target_width, + target_height, + image::imageops::FilterType::Lanczos3, + ); + assert!( + resize_lanczos3(&source, target_width, target_height) == expected, + "{width}x{height} into {target_width}x{target_height}" + ); + } + } + + #[test] + fn portable_vertical_pass_matches_the_dispatched_one() { + for (width, height, target_height) in [(3840, 2160, 180), (1001, 777, 180), (7, 9, 16)] { + let source = noise(width, height); + let source_row = width as usize * 4; + let rows = lanczos3_taps(height, target_height); + let mut dispatched = vec![0f32; source_row * target_height as usize]; + let mut portable = dispatched.clone(); + vertical_pass(&mut dispatched, source.as_raw(), &rows, source_row); + vertical_pass_portable(&mut portable, source.as_raw(), &rows, source_row); + assert!( + dispatched + .iter() + .zip(&portable) + .all(|(a, b)| a.to_bits() == b.to_bits()), + "{width}x{height}" + ); + } + } }