Refactor image loading

Start separating image loading logic from wl_shm buffer handling
Add fast path decoding directly to wl_shm buffers if possible
Add auto-vectorized RGB to BGRA swizzling
This commit is contained in:
Gergő Sályi 2025-04-15 23:02:38 +02:00
parent a2f9ad2b83
commit 0972f05b6c
2 changed files with 245 additions and 201 deletions

View file

@ -1,15 +1,19 @@
#![allow(clippy::too_many_arguments)]
use std::{
fs::read_dir,
fs::{DirEntry, read_dir},
io,
path::Path,
};
use anyhow::{bail, Context};
use fast_image_resize::{
FilterType, PixelType, Resizer, ResizeAlg, ResizeOptions,
images::Image,
};
use image::{ImageBuffer, ImageError, ImageReader, Rgb};
use log::{debug, error};
use smithay_client_toolkit::shm::slot::{Buffer, SlotPool};
use image::{ColorType, DynamicImage, ImageBuffer, ImageDecoder, ImageReader};
use log::{debug, error, warn};
use smithay_client_toolkit::shm::slot::SlotPool;
use smithay_client_toolkit::reexports::client::protocol::wl_shm;
use crate::wayland::WorkspaceBackground;
@ -20,218 +24,258 @@ pub fn workspace_bgs_from_output_image_dir(
format: wl_shm::Format,
brightness: i32,
contrast: f32,
width: u32,
height: u32,
) -> anyhow::Result<Vec<WorkspaceBackground>> {
let mut buffers = Vec::new();
let mut resizer = Resizer::new();
let stride = match format {
wl_shm::Format::Xrgb8888 => width as usize * 4,
wl_shm::Format::Bgr888 => {
// Align buffer stride to both 4 and pixel format block size
// Not being aligned to 4 caused
// https://github.com/gergo-salyi/multibg-sway/issues/6
(width as usize * 3).next_multiple_of(4)
},
_ => unreachable!()
};
let dir = read_dir(&dir_path).context("Failed to open directory")?;
for entry_result in dir {
match workspace_bg_from_file(
entry_result,
slot_pool,
format,
brightness,
contrast,
width,
height,
stride,
&mut resizer
) {
Ok(Some(workspace_bg)) => buffers.push(workspace_bg),
Ok(None) => continue,
Err(e) => {
error!("Skipping a directory entry in {:?} \
due to an error: {:#}", dir_path.as_ref(), e);
continue;
}
}
}
if buffers.is_empty() {
bail!("Found no suitable images in the directory")
}
Ok(buffers)
}
fn workspace_bg_from_file(
dir_entry_result: io::Result<DirEntry>,
slot_pool: &mut SlotPool,
format: wl_shm::Format,
brightness: i32,
contrast: f32,
width: u32,
height: u32,
stride: usize,
resizer: &mut Resizer,
) -> anyhow::Result<Option<WorkspaceBackground>> {
let entry = dir_entry_result.context("Failed to read direectory")?;
let path = entry.path();
// Skip dirs
if path.is_dir() { return Ok(None) }
// Use the file stem as the name of the workspace for this wallpaper
let workspace_name = path.file_stem().unwrap()
.to_string_lossy().into_owned();
let (buffer, canvas) = slot_pool.create_buffer(
width.try_into().unwrap(),
height.try_into().unwrap(),
stride.try_into().unwrap(),
format,
).context("Failed to create Wayland shared memory buffer")?;
let color_transform = if brightness == 0 && contrast == 0.0 {
ColorTransform::None
} else {
ColorTransform::Legacy { brightness, contrast }
};
load_wallpaper(
&path,
&mut canvas[..stride * height as usize],
width,
height,
stride,
format,
color_transform,
resizer
).context("Failed to load wallpaper")?;
Ok(Some(WorkspaceBackground { workspace_name, buffer }))
}
#[derive(Clone, Copy, PartialEq)]
pub enum ColorTransform {
// Levels { input_max: u8, input_min: u8, output_max: u8, output_min: u8 },
Legacy { brightness: i32, contrast: f32 },
None,
}
fn load_wallpaper(
path: &Path,
dst: &mut [u8],
surface_width: u32,
surface_height: u32,
)
-> Result<Vec<WorkspaceBackground>, String>
{
let mut buffers = Vec::new();
let dir = read_dir(&dir_path)
.map_err(|e| format!("Failed to open directory: {}", e))?;
for entry_result in dir {
let entry = match entry_result {
Ok(entry) => entry,
Err(e) => {
error!(
"Skipping a directory entry in '{:?}' due to an error: {}",
dir_path.as_ref(), e
);
continue;
}
};
let path = entry.path();
// Skip dirs
if path.is_dir() { continue }
// Use the file stem as the name of the workspace for this wallpaper
let workspace_name = path.file_stem().unwrap()
.to_string_lossy().into_owned();
let raw_image = match ImageReader::open(&path)
.map_err(ImageError::IoError)
.and_then(|r| r.with_guessed_format()
.map_err(ImageError::IoError)
)
.and_then(|r| r.decode())
{
Ok(raw_image) => raw_image,
Err(e) => {
error!(
"Failed to open image '{:?}': {}",
path, e
);
continue;
}
};
// It is possible to adjust the contrast and brightness here
let mut image = raw_image;
surface_stride: usize,
surface_format: wl_shm::Format,
color_transform: ColorTransform,
resizer: &mut Resizer,
) -> anyhow::Result<()> {
let reader = ImageReader::open(path)
.context("Failed to open image file")?
.with_guessed_format()
.context("Failed to read image file format")?;
let file_format = reader.format()
.context("Failed to determine image file format")?;
if !file_format.can_read() {
bail!("Unsupported image file format {file_format:?}")
} else if !file_format.reading_enabled() {
bail!("Application was compiled with support \
for image file format {file_format:?} disabled")
}
let mut decoder = reader.into_decoder()
.context("Failed to initialize image decoder")?;
let (image_width, image_height) = decoder.dimensions();
let image_size = decoder.total_bytes();
let image_color_type = decoder.color_type();
if image_width == 0 || image_height == 0 || image_size > isize::MAX as u64 {
bail!("Image has invalid dimensions {image_width}x{image_height}")
};
let image_size = image_size as usize;
debug!("Image {image_width}x{image_height} {image_color_type:?}");
if image_color_type.has_alpha() {
warn!("Image has alpha channel which will be ignored");
}
if let Ok(Some(_)) = decoder.icc_profile() {
debug!("Image has an embedded ICC color profile \
but ICC color profile handling is not yet implemented");
}
let needs_resize = image_width != surface_width
|| image_height != surface_height;
let surface_row_len = surface_width as usize * 3;
if !needs_resize
&& image_color_type == ColorType::Rgb8
&& surface_format == wl_shm::Format::Bgr888
&& color_transform == ColorTransform::None
&& surface_row_len == surface_stride
{
debug!("Decoding image directly to destination buffer");
decoder.read_image(&mut dst[..image_size])
.context("Failed to decode image")?;
return Ok(());
}
let mut image = DynamicImage::from_decoder(decoder)
.context("Failed to decode image")?;
if let ColorTransform::Legacy { brightness, contrast } = color_transform {
if contrast != 0.0 {
image = image.adjust_contrast(contrast)
}
if brightness != 0 {
image = image.brighten(brightness)
}
}
let mut image = image.into_rgb8();
if needs_resize {
debug!("Resizing image from {}x{} to {}x{}",
image_width, image_height,
surface_width, surface_height
);
let src_image = Image::from_vec_u8(
image_width,
image_height,
image.into_raw(),
PixelType::U8x3,
).unwrap();
let mut dst_image = Image::new(
surface_width,
surface_height,
PixelType::U8x3,
);
resizer.resize(
&src_image,
&mut dst_image,
&ResizeOptions::new()
.fit_into_destination(None)
.resize_alg(ResizeAlg::Convolution(FilterType::Lanczos3))
).context("Failed to resize image")?;
image = ImageBuffer::from_raw(
surface_width,
surface_height,
dst_image.into_vec()
).unwrap();
}
match surface_format {
wl_shm::Format::Bgr888 => {
if surface_row_len == surface_stride {
dst.copy_from_slice(&image);
} else {
copy_pad_stride(
&image,
dst,
surface_row_len,
surface_stride,
surface_height as usize,
);
}
},
wl_shm::Format::Xrgb8888 => {
swizzle_bgra_from_rgb(&image, dst);
},
_ => unreachable!(),
}
Ok(())
}
let mut image = image.into_rgb8();
let image_width = image.width();
let image_height = image.height();
fn copy_pad_stride(
src: &[u8],
dst: &mut [u8],
src_stride: usize,
dst_stride: usize,
height: usize,
) {
for row in 0..height {
dst[row * dst_stride..][..src_stride].copy_from_slice(
&src[row * src_stride..][..src_stride]
);
}
}
if image_width == 0 {
error!(
"Image '{}' has zero width, skipping", workspace_name
);
continue;
};
if image_height == 0 {
error!(
"Image '{}' has zero height, skipping", workspace_name
);
continue;
};
if image_width != surface_width || image_height != surface_height
{
debug!("Resizing image '{}' from {}x{} to {}x{}",
workspace_name,
image_width, image_height,
surface_width, surface_height
);
let src_image = Image::from_vec_u8(
image_width,
image_height,
image.into_raw(),
PixelType::U8x3,
).unwrap();
let mut dst_image = Image::new(
surface_width,
surface_height,
PixelType::U8x3,
);
let mut resizer = Resizer::new();
resizer.resize(
&src_image,
&mut dst_image,
&ResizeOptions::new()
.fit_into_destination(None)
.resize_alg(ResizeAlg::Convolution(FilterType::Lanczos3))
).unwrap();
image = ImageBuffer::from_raw(
surface_width,
surface_height,
dst_image.into_vec()
).unwrap();
fn swizzle_bgra_from_rgb(src: &[u8], dst: &mut [u8]) {
let pixel_count = dst.len() / 4;
assert_eq!(src.len(), pixel_count * 3);
assert_eq!(dst.len(), pixel_count * 4);
unsafe {
#[cfg(target_arch = "x86_64")]
if is_x86_feature_detected!("avx2") {
bgra_from_rgb_avx2(src, dst, pixel_count);
return
}
let buffer = match format {
wl_shm::Format::Xrgb8888 =>
buffer_xrgb8888_from_image(image, slot_pool),
wl_shm::Format::Bgr888 =>
buffer_bgr888_from_image(image, slot_pool),
_ => unreachable!()
};
buffers.push(WorkspaceBackground { workspace_name, buffer });
}
if buffers.is_empty() {
Err("Found 0 suitable images in the directory".to_string())
}
else {
Ok(buffers)
bgra_from_rgb(src, dst, pixel_count)
}
}
fn buffer_xrgb8888_from_image(
image: ImageBuffer<Rgb<u8>, Vec<u8>>,
slot_pool: &mut SlotPool,
)
-> Buffer
{
let (buffer, canvas) = slot_pool
.create_buffer(
image.width() as i32,
image.height() as i32,
image.width() as i32 * 4,
wl_shm::Format::Xrgb8888
)
.unwrap();
let canvas_len = image.len() / 3 * 4;
let image_pixels = image.pixels();
let canvas_pixels = canvas[..canvas_len].chunks_exact_mut(4);
for (image_pixel, canvas_pixel) in image_pixels.zip(canvas_pixels) {
canvas_pixel[0] = image_pixel.0[2];
canvas_pixel[1] = image_pixel.0[1];
canvas_pixel[2] = image_pixel.0[0];
}
buffer
#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn bgra_from_rgb_avx2(src: &[u8], dst: &mut [u8], pixel_count: usize) {
unsafe { bgra_from_rgb(src, dst, pixel_count) }
}
fn buffer_bgr888_from_image(
image: ImageBuffer<Rgb<u8>, Vec<u8>>,
slot_pool: &mut SlotPool,
)
-> Buffer
{
// Align buffer stride to both 4 and pixel format block size
// Not being aligned to 4 caused
// https://github.com/gergo-salyi/multibg-sway/issues/6
const BUFFER_STRIDE_ALIGNEMENT: u32 = 4 * 3;
let width = image.width();
let height = image.height();
let image_stride = width * 3;
let unaligned_bytes = image_stride % BUFFER_STRIDE_ALIGNEMENT;
let buffer_stride =
if unaligned_bytes == 0 {
image_stride
} else {
let padding = BUFFER_STRIDE_ALIGNEMENT - unaligned_bytes;
image_stride + padding
};
let (buffer, canvas) = slot_pool
.create_buffer(
width.try_into().unwrap(),
height.try_into().unwrap(),
buffer_stride.try_into().unwrap(),
wl_shm::Format::Bgr888
)
.unwrap();
if unaligned_bytes == 0 {
canvas[..image.len()].copy_from_slice(&image);
}
else {
let height: usize = height.try_into().unwrap();
let buffer_stride: usize = buffer_stride.try_into().unwrap();
let image_stride: usize = image_stride.try_into().unwrap();
for row in 0..height {
let canvas_start = row * buffer_stride;
let image_start = row * image_stride;
let len = image_stride;
canvas[canvas_start..(canvas_start + len)].copy_from_slice(
&image.as_raw()[image_start..(image_start + len)]
);
unsafe fn bgra_from_rgb(src: &[u8], dst: &mut [u8], pixel_count: usize) {
unsafe {
let mut src = src.as_ptr();
let mut dst = dst.as_mut_ptr();
for _ in 0..pixel_count {
*dst.add(0) = *src.add(2); // B
*dst.add(1) = *src.add(1); // G
*dst.add(2) = *src.add(0); // R
*dst.add(3) = u8::MAX; // A
src = src.add(3);
dst = dst.add(4);
}
}
buffer
}

View file

@ -293,7 +293,7 @@ logical size: {}x{}, transform: {:?}",
},
Err(e) => {
error!(
"Failed to get wallpapers for new output '{}' form '{:?}': {}",
"Failed to get wallpapers for new output '{}' form '{:?}': {:#}",
output_name, output_wallpaper_dir, e
);
return;