Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions apps/desktop-gpui/kernels/benches/thumbnail_fit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
188 changes: 186 additions & 2 deletions apps/desktop-gpui/kernels/src/thumbnail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
))
};

Expand All @@ -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<f32>,
}

fn lanczos3_taps(source: u32, target: u32) -> Vec<Taps> {
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::<Vec<_>>();
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::*;
Expand Down Expand Up @@ -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}"
);
}
}
}
Loading