From 3ef5bf883c7f01c2e7c35c3864d25a16b0b29ba5 Mon Sep 17 00:00:00 2001 From: Radzivon Bartoshyk Date: Wed, 8 Apr 2026 19:21:16 +0100 Subject: [PATCH 1/9] API improvements --- Cargo.toml | 1 + src/avx/image_to_oklab.rs | 22 +- src/avx/mod.rs | 6 +- src/avx/oklab_to_image.rs | 18 +- src/avx/to_sigmoidal.rs | 40 +- src/buffer.rs | 305 ++++++++++++++ src/concat_alpha.rs | 302 ++++++++------ src/err.rs | 64 +++ src/gamma_curves.rs | 2 +- src/hsv_to_image.rs | 8 +- src/image.rs | 8 +- src/image_to_hsv.rs | 8 +- src/image_to_jzazbz.rs | 784 +++++++++++++++++++++++++++--------- src/image_to_lalphabeta.rs | 24 +- src/image_to_linear.rs | 393 +++++++++++------- src/image_to_linear_u8.rs | 474 ++++++++++++++++------ src/image_to_oklab.rs | 585 ++++++++++++++++++--------- src/image_to_sigmoidal.rs | 419 ++++++++++++++----- src/image_to_xyz_lab.rs | 44 +- src/image_xyza_laba.rs | 24 +- src/jzazbz_to_image.rs | 45 +-- src/lalphabeta_to_image.rs | 24 +- src/lib.rs | 4 + src/linear_to_image.rs | 16 +- src/linear_to_image_u8.rs | 16 +- src/neon/image_to_jzazbz.rs | 15 +- src/neon/image_to_oklab.rs | 15 +- src/neon/mod.rs | 6 +- src/neon/oklab_to_image.rs | 22 +- src/neon/sigmoidal.rs | 24 +- src/neon/to_sigmoidal.rs | 134 ++++-- src/oklab_to_image.rs | 551 ++++++++++++++++--------- src/sigmoidal_to_image.rs | 8 +- src/sse/image_to_jzazbz.rs | 16 +- src/sse/image_to_oklab.rs | 28 +- src/sse/mod.rs | 8 +- src/sse/oklab_to_image.rs | 14 +- src/sse/to_sigmoidal.rs | 54 ++- src/xyz_lab_to_image.rs | 42 +- src/xyza_laba_to_image.rs | 24 +- 40 files changed, 3239 insertions(+), 1358 deletions(-) create mode 100644 src/buffer.rs create mode 100644 src/err.rs diff --git a/Cargo.toml b/Cargo.toml index d6f487f..2dd46a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ categories = ["multimedia::images", "multimedia::video"] homepage = "https://github.com/awxkee/colorutils-rs" repository = "https://github.com/awxkee/colorutils-rs" exclude = ["*.jpg"] +rust-version = "1.89.0" [dependencies] erydanos = "0.2" diff --git a/src/avx/image_to_oklab.rs b/src/avx/image_to_oklab.rs index 06f3060..c4ee0f4 100644 --- a/src/avx/image_to_oklab.rs +++ b/src/avx/image_to_oklab.rs @@ -42,19 +42,16 @@ macro_rules! triple_to_oklab { } #[target_feature(enable = "avx2")] -pub unsafe fn avx_image_to_oklab( +pub(crate) unsafe fn avx_image_to_oklab( start_cx: usize, width: u32, - dst: *mut f32, - dst_offset: usize, + dst: &mut [f32], ) -> usize { let target: OklabTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); let channels = image_configuration.get_channels_count(); let mut cx = start_cx; - let dst_ptr = (dst as *mut u8).add(dst_offset) as *mut f32; - let (c0, c1, c2, c3, c4, c5, c6, c7, c8) = ( _mm256_set1_ps(0.4122214708f32), _mm256_set1_ps(0.5363325363f32), @@ -79,10 +76,10 @@ pub unsafe fn avx_image_to_oklab(in_place_ptr); + avx_vld_f32_and_deinterleave::(in_place_ptr.as_ptr()); let (x_low_low, y_low_low, z_low_low) = triple_to_oklab!( r_chan, g_chan, b_chan, target, c0, c1, c2, c3, c4, c5, c6, c7, c8, m0, m1, m2, m3, m4, @@ -91,14 +88,19 @@ pub unsafe fn avx_image_to_oklab( - src: *const f32, + src: &[f32], oklab_target: OklabTarget, m0: __m256, m1: __m256, @@ -42,7 +42,7 @@ unsafe fn avx_oklab_vld( c8: __m256, ) -> (__m256, __m256, __m256, __m256) { let (l, mut a, mut b, a_f32) = - avx_vld_f32_and_deinterleave_direct::(src); + avx_vld_f32_and_deinterleave_direct::(src.as_ptr()); if oklab_target == OklabTarget::Oklch { let a0 = _mm256_mul_ps(a, _mm256_cos_ps(b)); @@ -63,12 +63,10 @@ unsafe fn avx_oklab_vld( } #[target_feature(enable = "avx2")] -pub unsafe fn avx_oklab_to_image( +pub(crate) unsafe fn avx_oklab_to_image( start_cx: usize, - src: *const f32, - src_offset: usize, - dst: *mut f32, - dst_offset: u32, + src: &[f32], + dst: &mut [f32], width: u32, ) -> usize { let target: OklabTarget = TARGET.into(); @@ -100,8 +98,8 @@ pub unsafe fn avx_oklab_to_image( start_cx: usize, - src: *const u8, + src: &[u8], + dst: &mut [f32], width: u32, - dst: *mut f32, ) -> usize { let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); let mut cx = start_cx; @@ -34,10 +34,8 @@ pub unsafe fn avx_image_to_sigmoidal_row< let channels = image_configuration.get_channels_count(); - let dst_ptr = (dst as *mut u8) as *mut f32; - - while cx + 32 < width as usize { - let src_ptr = src.add(cx * channels); + while cx + 32 <= width as usize { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); let (r_chan, g_chan, b_chan, a_chan) = avx_vld_u8_and_deinterleave::(src_ptr); @@ -61,7 +59,7 @@ pub unsafe fn avx_image_to_sigmoidal_row< u8_scale, ); - let ptr = dst_ptr.add(cx * channels); + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); avx_store_and_interleave_v4_f32!( ptr, image_configuration, @@ -71,7 +69,7 @@ pub unsafe fn avx_image_to_sigmoidal_row< a_low_low ); } else { - let ptr = dst_ptr.add(cx * channels); + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); avx_store_and_interleave_v3_f32!( ptr, image_configuration, @@ -94,7 +92,9 @@ pub unsafe fn avx_image_to_sigmoidal_row< u8_scale, ); - let ptr = dst_ptr.add(cx * channels + 8 * channels); + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels..) + .as_mut_ptr(); avx_store_and_interleave_v4_f32!( ptr, image_configuration, @@ -104,7 +104,9 @@ pub unsafe fn avx_image_to_sigmoidal_row< a_low_high ); } else { - let ptr = dst_ptr.add(cx * channels + 8 * channels); + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels..) + .as_mut_ptr(); avx_store_and_interleave_v3_f32!( ptr, image_configuration, @@ -133,7 +135,9 @@ pub unsafe fn avx_image_to_sigmoidal_row< u8_scale, ); - let ptr = dst_ptr.add(cx * channels + 8 * channels * 2); + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels * 2..) + .as_mut_ptr(); avx_store_and_interleave_v4_f32!( ptr, image_configuration, @@ -143,7 +147,9 @@ pub unsafe fn avx_image_to_sigmoidal_row< a_high_low ); } else { - let ptr = dst_ptr.add(cx * channels + 8 * channels * 2); + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels * 2..) + .as_mut_ptr(); avx_store_and_interleave_v3_f32!( ptr, image_configuration, @@ -166,7 +172,9 @@ pub unsafe fn avx_image_to_sigmoidal_row< u8_scale, ); - let ptr = dst_ptr.add(cx * channels + 8 * channels * 3); + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels * 3..) + .as_mut_ptr(); avx_store_and_interleave_v4_f32!( ptr, image_configuration, @@ -176,7 +184,9 @@ pub unsafe fn avx_image_to_sigmoidal_row< a_high_high ); } else { - let ptr = dst_ptr.add(cx * channels + 8 * channels * 3); + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels * 3..) + .as_mut_ptr(); avx_store_and_interleave_v3_f32!( ptr, image_configuration, diff --git a/src/buffer.rs b/src/buffer.rs new file mode 100644 index 0000000..528140c --- /dev/null +++ b/src/buffer.rs @@ -0,0 +1,305 @@ +/* + * // Copyright 2026 (c) the Radzivon Bartoshyk. All rights reserved. + * // + * // Use of this source code is governed by a BSD-style + * // license that can be found in the LICENSE file. + */ +use crate::err::ColorError; +use std::borrow::Cow; +use std::fmt::Debug; +use std::ops::{Index, Range, RangeFrom}; + +pub struct ImageBuffer<'a, F> +where + [F]: ToOwned>, +{ + pub data: std::borrow::Cow<'a, [F]>, + pub width: u32, + pub height: u32, + pub stride: u32, + pub channels: u32, +} + +impl<'a, F: Sized> ImageBuffer<'a, F> +where + [F]: ToOwned>, +{ + pub fn new( + data: &'a [F], + width: u32, + height: u32, + stride: u32, + channels: u32, + ) -> Result { + let min_stride = width * channels; + if stride < min_stride { + return Err(ColorError::StrideTooNarrow { + min: min_stride, + given: stride, + }); + } + let required = stride as usize * height as usize; + if data.len() < required { + return Err(ColorError::BufferTooSmall { + expected: required, + got: data.len(), + }); + } + Ok(Self { + data: Cow::Borrowed(data), + width, + height, + stride, + channels, + }) + } + + pub fn from_vec( + data: Vec, + width: u32, + height: u32, + stride: u32, + channels: u32, + ) -> Result { + let min_stride = width * channels; + if stride < min_stride { + return Err(ColorError::StrideTooNarrow { + min: min_stride, + given: stride, + }); + } + let required = stride as usize * height as usize; + if data.len() < required { + return Err(ColorError::BufferTooSmall { + expected: required, + got: data.len(), + }); + } + Ok(Self { + data: Cow::Owned(data), + width, + height, + stride, + channels, + }) + } + + pub fn try_match(&self, other: &ImageBuffer<'_, F>) -> Result<(), ColorError> { + if self.width != other.width || self.height != other.height { + return Err(ColorError::DimensionMismatch { + expected: (self.width, self.height), + got: (other.width, other.height), + }); + } + Ok(()) + } + + pub fn validate(&self) -> Result<(), ColorError> { + let min_stride = self + .width + .checked_mul(self.channels) + .ok_or(ColorError::DimensionOverflow)?; + + if self.stride < min_stride { + return Err(ColorError::StrideTooNarrow { + min: min_stride, + given: self.stride, + }); + } + + let required = self + .stride() + .checked_mul(self.height as usize) + .ok_or(ColorError::DimensionOverflow)?; + + if required == 0 { + return Err(ColorError::ZeroImageSize); + } + + if self.data.len() < required { + return Err(ColorError::BufferTooSmall { + expected: required, + got: self.data.len(), + }); + } + + Ok(()) + } + + pub fn stride(&self) -> usize { + if self.stride == 0 { + return self.width as usize * self.channels as usize; + } + self.stride as usize + } +} + +#[derive(Debug)] +pub enum BufferStore<'a, T: Debug> { + Borrowed(&'a mut [T]), + Owned(Vec), +} + +impl BufferStore<'_, T> { + #[allow(clippy::should_implement_trait)] + pub fn borrow(&self) -> &[T] { + match self { + Self::Borrowed(p_ref) => p_ref, + Self::Owned(vec) => vec, + } + } + + #[allow(clippy::should_implement_trait)] + pub fn borrow_mut(&mut self) -> &mut [T] { + match self { + Self::Borrowed(p_ref) => p_ref, + Self::Owned(vec) => vec, + } + } +} + +impl Index for BufferStore<'_, T> { + type Output = T; + + fn index(&self, index: usize) -> &Self::Output { + match self { + Self::Borrowed(p_ref) => &p_ref[index], + Self::Owned(vec) => &vec[index], + } + } +} + +impl Index> for BufferStore<'_, T> { + type Output = [T]; + + fn index(&self, index: Range) -> &Self::Output { + match self { + Self::Borrowed(p_ref) => &p_ref[index], + Self::Owned(vec) => &vec[index], + } + } +} + +impl Index> for BufferStore<'_, T> { + type Output = [T]; + + fn index(&self, index: RangeFrom) -> &Self::Output { + match self { + Self::Borrowed(p_ref) => &p_ref[index], + Self::Owned(vec) => &vec[index], + } + } +} + +#[derive(Debug)] +pub struct ImageBufferMut<'a, F: Copy + Debug> { + pub data: BufferStore<'a, F>, + pub width: u32, + pub height: u32, + pub stride: u32, + pub channels: u32, +} + +impl<'a, F: Copy + Debug + Sized> ImageBufferMut<'a, F> { + pub fn new( + data: BufferStore<'a, F>, + width: u32, + height: u32, + stride: u32, + channels: u32, + ) -> Result { + let buf = Self { + data, + width, + height, + stride, + channels, + }; + buf.validate()?; + Ok(buf) + } + + pub fn validate(&self) -> Result<(), ColorError> { + let min_stride = self + .width + .checked_mul(self.channels) + .ok_or(ColorError::DimensionOverflow)?; + + if self.stride < min_stride { + return Err(ColorError::StrideTooNarrow { + min: min_stride, + given: self.stride, + }); + } + + let required = self + .stride() + .checked_mul(self.height as usize) + .ok_or(ColorError::DimensionOverflow)?; + + if required == 0 { + return Err(ColorError::ZeroImageSize); + } + + if self.data.borrow().len() < required { + return Err(ColorError::BufferTooSmall { + expected: required, + got: self.data.borrow().len(), + }); + } + + Ok(()) + } + + pub fn try_match(&self, other: &ImageBufferMut<'_, F>) -> Result<(), ColorError> { + if self.width != other.width || self.height != other.height { + return Err(ColorError::DimensionMismatch { + expected: (self.width, self.height), + got: (other.width, other.height), + }); + } + Ok(()) + } + + pub fn try_match_immutable(&self, other: &ImageBuffer<'_, Z>) -> Result<(), ColorError> + where + [Z]: ToOwned>, + { + if self.width != other.width || self.height != other.height { + return Err(ColorError::DimensionMismatch { + expected: (self.width, self.height), + got: (other.width, other.height), + }); + } + Ok(()) + } + + pub fn try_match_immutable_with_channels( + &self, + other: &ImageBuffer<'_, Z>, + ) -> Result<(), ColorError> + where + [Z]: ToOwned>, + { + if self.width != other.width || self.height != other.height { + return Err(ColorError::DimensionMismatch { + expected: (self.width, self.height), + got: (other.width, other.height), + }); + } + if self.channels != other.channels { + return Err(ColorError::ChannelCountMismatch { + expected: self.channels, + got: other.channels, + }); + } + Ok(()) + } + + pub fn stride(&self) -> usize { + if self.stride == 0 { + return self.width as usize * self.channels as usize; + } + self.stride as usize + } +} diff --git a/src/concat_alpha.rs b/src/concat_alpha.rs index 0b34ef1..ee75fe1 100644 --- a/src/concat_alpha.rs +++ b/src/concat_alpha.rs @@ -5,145 +5,193 @@ * // license that can be found in the LICENSE file. */ -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -use crate::avx::*; -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -use crate::sse::*; -#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -use std::arch::aarch64::*; -#[cfg(target_arch = "x86")] -#[allow(unused_imports)] -use std::arch::x86::*; -#[cfg(target_arch = "x86_64")] -#[allow(unused_imports)] -use std::arch::x86_64::*; +use crate::{ColorError, ImageBuffer, ImageBufferMut}; /// Adds alpha plane into an existing RGB/XYZ/LAB or other 3 plane image. Image will become RGBA, XYZa, LABa etc. pub fn append_alpha( - dst: &mut [f32], - dst_stride: u32, - src: &[f32], - src_stride: u32, - a_plane: &[f32], - a_stride: u32, - width: u32, - height: u32, -) { - let mut dst_offset = 0usize; - let mut src_offset = 0usize; - let mut a_offset = 0usize; - - #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] - let mut _use_sse = std::arch::is_x86_feature_detected!("sse4.1"); - - #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] - let mut _use_avx = std::arch::is_x86_feature_detected!("avx2"); - - for _ in 0..height { - let mut _cx = 0usize; - - let src_ptr = unsafe { (src.as_ptr() as *const u8).add(src_offset) as *const f32 }; - let a_ptr = unsafe { (a_plane.as_ptr() as *const u8).add(a_offset) as *const f32 }; - let dst_ptr = unsafe { (dst.as_mut_ptr() as *mut u8).add(dst_offset) as *mut f32 }; - - #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] - unsafe { - if _use_avx { - concat_alpha_avx(width, _cx, src_ptr, a_ptr, dst_ptr); - } - } + src: &ImageBuffer, + dst: &mut ImageBufferMut, + a_plane: &ImageBuffer, +) -> Result<(), ColorError> { + src.validate()?; + dst.validate()?; + a_plane.validate()?; + if src.channels != 3 { + return Err(ColorError::Generic( + "We can add alpha only to 3 channels image".to_string(), + )); + } + if dst.channels != 4 { + return Err(ColorError::Generic( + "Dst image should be 4 channels".to_string(), + )); + } + if a_plane.channels != 1 { + return Err(ColorError::Generic( + "Alpha plane must have 1 channels".to_string(), + )); + } + dst.try_match_immutable(src)?; + dst.try_match_immutable(a_plane)?; - #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] - unsafe { - if _use_sse { - concat_alpha_sse(width, _cx, src_ptr, a_ptr, dst_ptr); - } + let a_working_width = a_plane.width * a_plane.channels; + let src_working_width = src.width * src.channels; + let dst_working_width = dst.width * dst.channels; + let dst_stride = dst.stride(); + for ((src, dst), a_plane) in src + .data + .chunks(src.stride()) + .zip(dst.data.borrow_mut().chunks_mut(dst_stride)) + .zip(a_plane.data.chunks(a_plane.stride())) + { + let src = &src[..src_working_width as usize]; + let dst = &mut dst[..dst_working_width as usize]; + let a_plane = &a_plane[..a_working_width as usize]; + for ((src, dst), a_ch) in src + .as_chunks::<3>() + .0 + .iter() + .zip(dst.as_chunks_mut::<4>().0.iter_mut()) + .zip(a_plane.iter()) + { + dst[0] = src[0]; + dst[1] = src[1]; + dst[2] = src[2]; + dst[3] = *a_ch; } + } + Ok(()) +} - #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] - unsafe { - while _cx + 4 < width as usize { - let xyz_pixel = vld3q_f32(src_ptr.add(_cx * 3usize)); - let a_pixel = vld1q_f32(a_ptr.add(_cx)); - let dst_pixel = float32x4x4_t(xyz_pixel.0, xyz_pixel.1, xyz_pixel.2, a_pixel); - vst4q_f32(dst_ptr.add(_cx * 4), dst_pixel); - _cx += 4; - } - } +#[cfg(test)] +mod tests { + use super::*; + use crate::BufferStore; + + fn make_src(width: u32, height: u32) -> ImageBuffer<'static, f32> { + let stride = width * 3; + let data = (0..stride * height) + .map(|i| (i as f32) / (stride * height) as f32) + .collect::>(); + ImageBuffer::from_vec(data, width, height, stride, 3).unwrap() + } - for x in _cx..width as usize { - unsafe { - let px = x * 4; - let s_x = x * 3; - let dst = dst_ptr.add(px); - let src = src_ptr.add(s_x); - dst.write_unaligned(src.read_unaligned()); - dst.add(1).write_unaligned(src.add(1).read_unaligned()); - dst.add(2).write_unaligned(src.add(2).read_unaligned()); - dst.add(3).write_unaligned(a_ptr.add(x).read_unaligned()); - } + fn make_alpha(width: u32, height: u32, fill: f32) -> ImageBuffer<'static, f32> { + let stride = width; + let data = vec![fill; (stride * height) as usize]; + ImageBuffer::from_vec(data, width, height, stride, 1).unwrap() + } + + fn make_dst(width: u32, height: u32) -> ImageBufferMut<'static, f32> { + let stride = width * 4; + let data = BufferStore::Owned(vec![0f32; (stride * height) as usize]); + ImageBufferMut::new(data, width, height, stride, 4).unwrap() + } + + #[test] + fn appends_alpha_values_correctly() { + let src = make_src(2, 2); + let alpha = make_alpha(2, 2, 0.5); + let mut dst = make_dst(2, 2); + + append_alpha(&src, &mut dst, &alpha).unwrap(); + + let out = dst.data.borrow(); + // every fourth element (the alpha channel) must equal 0.5 + for pixel in out.chunks(4) { + assert_eq!(pixel[3], 0.5, "alpha channel mismatch"); } + } + + #[test] + fn rgb_channels_are_preserved() { + let src = make_src(2, 1); + let alpha = make_alpha(2, 1, 1.0); + let mut dst = make_dst(2, 1); + + append_alpha(&src, &mut dst, &alpha).unwrap(); - dst_offset += dst_stride as usize; - a_offset += a_stride as usize; - src_offset += src_stride as usize; + let src_data = src.data; + let out = dst.data.borrow(); + // pixels 0 and 1 — R, G, B must match src exactly + assert_eq!(out[0], src_data[0]); + assert_eq!(out[1], src_data[1]); + assert_eq!(out[2], src_data[2]); + assert_eq!(out[4], src_data[3]); + assert_eq!(out[5], src_data[4]); + assert_eq!(out[6], src_data[5]); } -} -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[target_feature(enable = "sse4.1")] -#[inline] -unsafe fn concat_alpha_sse( - width: u32, - mut _cx: usize, - src_ptr: *const f32, - a_ptr: *const f32, - dst_ptr: *mut f32, -) { - while _cx + 4 < width as usize { - let xyz_chan_ptr = src_ptr.add(_cx * 3usize); - let a_chan_ptr = a_ptr.add(_cx); - let xyz0 = _mm_loadu_ps(xyz_chan_ptr); - let xyz1 = _mm_loadu_ps(xyz_chan_ptr.add(4)); - let xyz2 = _mm_loadu_ps(xyz_chan_ptr.add(8)); - let a_pixel = _mm_loadu_ps(a_chan_ptr); - let (x_p, y_p, z_p) = sse_deinterleave_rgb_ps(xyz0, xyz1, xyz2); - let (xyza0, xyza1, xyza2, xyza3) = sse_interleave_ps_rgba(x_p, y_p, z_p, a_pixel); - let xyza_chan_ptr = dst_ptr.add(_cx * 4usize); - _mm_storeu_ps(xyza_chan_ptr, xyza0); - _mm_storeu_ps(xyza_chan_ptr.add(4), xyza1); - _mm_storeu_ps(xyza_chan_ptr.add(8), xyza2); - _mm_storeu_ps(xyza_chan_ptr.add(12), xyza3); - _cx += 4; + #[test] + fn works_with_single_pixel() { + let src = ImageBuffer::new(&[0.1f32, 0.2, 0.3], 1, 1, 3, 3).unwrap(); + let alpha = ImageBuffer::new(&[0.9f32], 1, 1, 1, 1).unwrap(); + let mut dst = make_dst(1, 1); + + append_alpha(&src, &mut dst, &alpha).unwrap(); + + let out = dst.data.borrow(); + assert_eq!(out[0], 0.1); + assert_eq!(out[1], 0.2); + assert_eq!(out[2], 0.3); + assert_eq!(out[3], 0.9); } -} -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[target_feature(enable = "avx2")] -#[inline] -unsafe fn concat_alpha_avx( - width: u32, - mut _cx: usize, - src_ptr: *const f32, - a_ptr: *const f32, - dst_ptr: *mut f32, -) { - while _cx + 8 < width as usize { - let xyz_chan_ptr = src_ptr.add(_cx * 3usize); - let a_chan_ptr = a_ptr.add(_cx); - let xyz0 = _mm256_loadu_ps(xyz_chan_ptr); - let xyz1 = _mm256_loadu_ps(xyz_chan_ptr.add(8)); - let xyz2 = _mm256_loadu_ps(xyz_chan_ptr.add(16)); - let a_pixel = _mm256_loadu_ps(a_chan_ptr); - let (x_p, y_p, z_p) = avx2_deinterleave_rgb_ps(xyz0, xyz1, xyz2); - - let xyza_chan_ptr = dst_ptr.add(_cx * 4usize); - - let (xyza0, xyza1, xyza2, xyza3) = avx2_interleave_rgba_ps(x_p, y_p, z_p, a_pixel); - _mm256_store_ps(xyza_chan_ptr, xyza0); - _mm256_store_ps(xyza_chan_ptr.add(8), xyza1); - _mm256_store_ps(xyza_chan_ptr.add(16), xyza2); - _mm256_store_ps(xyza_chan_ptr.add(32), xyza3); - _cx += 8; + // --- channel count validation --- + + #[test] + fn rejects_src_with_wrong_channel_count() { + let stride = 4; + let data = vec![0f32; (stride * 1) as usize]; + let src = ImageBuffer::new(data.leak(), 1, 1, stride, 4).unwrap(); + let alpha = make_alpha(1, 1, 1.0); + let mut dst = make_dst(1, 1); + + let err = append_alpha(&src, &mut dst, &alpha).unwrap_err(); + assert!(matches!(err, ColorError::Generic(_))); + } + + #[test] + fn rejects_dst_with_wrong_channel_count() { + let src = make_src(1, 1); + let alpha = make_alpha(1, 1, 1.0); + let stride = 3; + let data = BufferStore::Owned(vec![0f32; (stride * 1) as usize]); + let mut dst = ImageBufferMut::new(data, 1, 1, stride, 3).unwrap(); + + let err = append_alpha(&src, &mut dst, &alpha).unwrap_err(); + assert!(matches!(err, ColorError::Generic(_))); + } + + #[test] + fn rejects_alpha_plane_with_wrong_channel_count() { + let src = make_src(1, 1); + let stride = 2; + let data = vec![0f32; (stride * 1) as usize]; + let alpha = ImageBuffer::new(data.leak(), 1, 1, stride, 2).unwrap(); + let mut dst = make_dst(1, 1); + + let err = append_alpha(&src, &mut dst, &alpha).unwrap_err(); + assert!(matches!(err, ColorError::Generic(_))); + } + + #[test] + fn rejects_mismatched_src_and_dst_dimensions() { + let src = make_src(2, 2); + let alpha = make_alpha(2, 2, 1.0); + let mut dst = make_dst(4, 4); + + let err = append_alpha(&src, &mut dst, &alpha).unwrap_err(); + assert!(matches!(err, ColorError::DimensionMismatch { .. })); + } + + #[test] + fn rejects_mismatched_alpha_and_dst_dimensions() { + let src = make_src(2, 2); + let alpha = make_alpha(4, 4, 1.0); + let mut dst = make_dst(2, 2); + + let err = append_alpha(&src, &mut dst, &alpha).unwrap_err(); + assert!(matches!(err, ColorError::DimensionMismatch { .. })); } } diff --git a/src/err.rs b/src/err.rs new file mode 100644 index 0000000..e691282 --- /dev/null +++ b/src/err.rs @@ -0,0 +1,64 @@ +/* + * // Copyright 2026 (c) the Radzivon Bartoshyk. All rights reserved. + * // + * // Use of this source code is governed by a BSD-style + * // license that can be found in the LICENSE file. + */ +use std::fmt; + +#[derive(Debug)] +pub enum ColorError { + BufferTooSmall { + expected: usize, + got: usize, + }, + StrideTooNarrow { + min: u32, + given: u32, + }, + DimensionMismatch { + expected: (u32, u32), + got: (u32, u32), + }, + DimensionOverflow, + ChannelCountMismatch { + expected: u32, + got: u32, + }, + Generic(String), + ZeroImageSize, + UnsupportedChannelsCount(u32), +} + +impl fmt::Display for ColorError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ColorError::BufferTooSmall { expected, got } => { + write!(f, "buffer too small: expected {expected} bytes, got {got}") + } + ColorError::StrideTooNarrow { min, given } => { + write!( + f, + "stride {given} is too narrow for width \u{00d7} channels (minimum {min})" + ) + } + ColorError::DimensionMismatch { expected, got } => f.write_fmt(format_args!( + "source and destination dimensions differ expected {:?} got {:?}", + expected, got + )), + ColorError::DimensionOverflow => { + f.write_str("source and destination dimensions overflow") + } + ColorError::ChannelCountMismatch { expected, got } => f.write_fmt(format_args!( + "channel count mismatch: expected {expected}, got {got}" + )), + ColorError::Generic(msg) => f.write_str(msg), + ColorError::ZeroImageSize => f.write_str("Image may not have zero based size"), + ColorError::UnsupportedChannelsCount(provided) => { + f.write_fmt(format_args!("Channels count {} is not supported", provided)) + } + } + } +} + +impl std::error::Error for ColorError {} diff --git a/src/gamma_curves.rs b/src/gamma_curves.rs index 49e5890..fb03695 100644 --- a/src/gamma_curves.rs +++ b/src/gamma_curves.rs @@ -291,7 +291,7 @@ pub fn hlg_from_linear(linear: f32) -> f32 { #[inline] /// Gamma transfer function for HLG pub fn trc_linear(v: f32) -> f32 { - v.min(1.).min(0.) + v.min(1.).max(0.) } #[repr(C)] diff --git a/src/hsv_to_image.rs b/src/hsv_to_image.rs index 0863b7f..725413f 100644 --- a/src/hsv_to_image.rs +++ b/src/hsv_to_image.rs @@ -110,17 +110,17 @@ fn hsv_u16_to_channels< } }; - *dst_slice.get_unchecked_mut(hx + image_configuration.get_r_channel_offset()) = + *dst_slice.get_unchecked_mut(hx + image_configuration.r_index()) = rgb.r; - *dst_slice.get_unchecked_mut(hx + image_configuration.get_g_channel_offset()) = + *dst_slice.get_unchecked_mut(hx + image_configuration.g_index()) = rgb.g; - *dst_slice.get_unchecked_mut(hx + image_configuration.get_b_channel_offset()) = + *dst_slice.get_unchecked_mut(hx + image_configuration.b_index()) = rgb.b; if image_configuration.has_alpha() { let a = src.add(3).read_unaligned(); *dst_slice - .get_unchecked_mut(hx + image_configuration.get_a_channel_offset()) = + .get_unchecked_mut(hx + image_configuration.a_index()) = a as u8; } } diff --git a/src/image.rs b/src/image.rs index 277ebf6..1954144 100644 --- a/src/image.rs +++ b/src/image.rs @@ -31,7 +31,7 @@ impl ImageConfiguration { } #[inline(always)] - pub const fn get_r_channel_offset(&self) -> usize { + pub const fn r_index(&self) -> usize { match self { ImageConfiguration::Rgb => 0, ImageConfiguration::Rgba => 0, @@ -40,7 +40,7 @@ impl ImageConfiguration { } #[inline(always)] - pub const fn get_g_channel_offset(&self) -> usize { + pub const fn g_index(&self) -> usize { match self { ImageConfiguration::Rgb | ImageConfiguration::Bgr => 1, ImageConfiguration::Rgba | ImageConfiguration::Bgra => 1, @@ -48,7 +48,7 @@ impl ImageConfiguration { } #[inline(always)] - pub const fn get_b_channel_offset(&self) -> usize { + pub const fn b_index(&self) -> usize { match self { ImageConfiguration::Rgb => 2, ImageConfiguration::Rgba => 2, @@ -56,7 +56,7 @@ impl ImageConfiguration { } } #[inline(always)] - pub const fn get_a_channel_offset(&self) -> usize { + pub const fn a_index(&self) -> usize { match self { ImageConfiguration::Rgb | ImageConfiguration::Bgr => 0, ImageConfiguration::Rgba | ImageConfiguration::Bgra => 3, diff --git a/src/image_to_hsv.rs b/src/image_to_hsv.rs index 3bc614c..9533422 100644 --- a/src/image_to_hsv.rs +++ b/src/image_to_hsv.rs @@ -90,13 +90,13 @@ fn channels_to_hsv_u16< let px = x * channels; let src = src_ptr.add(px); let r = src - .add(image_configuration.get_r_channel_offset()) + .add(image_configuration.r_index()) .read_unaligned(); let g = src - .add(image_configuration.get_g_channel_offset()) + .add(image_configuration.g_index()) .read_unaligned(); let b = src - .add(image_configuration.get_b_channel_offset()) + .add(image_configuration.b_index()) .read_unaligned(); let rgb = Rgb::::new(r, g, b); @@ -121,7 +121,7 @@ fn channels_to_hsv_u16< if image_configuration.has_alpha() { let a = src - .add(image_configuration.get_a_channel_offset()) + .add(image_configuration.a_index()) .read_unaligned(); dst.add(3).write_unaligned(a as u16); } diff --git a/src/image_to_jzazbz.rs b/src/image_to_jzazbz.rs index cf43e1a..83f14f2 100644 --- a/src/image_to_jzazbz.rs +++ b/src/image_to_jzazbz.rs @@ -9,12 +9,10 @@ use crate::image::ImageConfiguration; use crate::neon::neon_image_to_jzazbz; #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] use crate::sse::sse_image_to_jzazbz; -use crate::{Jzazbz, Jzczhz, Rgb, TransferFunction, Xyz, SRGB_TO_XYZ_D65}; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -use std::slice; +use crate::{ + ColorError, ImageBuffer, ImageBufferMut, Jzazbz, Jzczhz, Rgb, TransferFunction, Xyz, + SRGB_TO_XYZ_D65, +}; #[repr(u8)] #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] @@ -37,23 +35,16 @@ impl From for JzazbzTarget { #[allow(clippy::type_complexity)] fn channels_to_jzaz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { let target: JzazbzTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); - - let mut _wide_row_handle: Option< - unsafe fn(usize, *const f32, usize, u32, *mut f32, usize, f32) -> usize, - > = None; + let mut _wide_row_handle: Option usize> = + None; #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] { @@ -65,101 +56,135 @@ fn channels_to_jzaz( _wide_row_handle = Some(sse_image_to_jzazbz::); } - let dst_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - dst.as_mut_ptr() as *mut u8, - dst_stride as usize * height as usize, - ) - }; - - let mut lut_table = vec![0f32; 256]; + let mut lut_table = [0f32; 256]; for (i, table) in lut_table.iter_mut().enumerate() { *table = transfer_function.linearize(i as f32 * (1. / 255.0)); } - let iter; - #[cfg(feature = "rayon")] - { - iter = dst_slice_safe_align - .par_chunks_exact_mut(dst_stride as usize) - .zip(src.par_chunks_exact(src_stride as usize)); - } - #[cfg(not(feature = "rayon"))] - { - iter = dst_slice_safe_align - .chunks_exact_mut(dst_stride as usize) - .zip(src.chunks_exact(src_stride as usize)); + let mut a_lut_table = [0f32; 256]; + for (i, lut) in a_lut_table.iter_mut().enumerate() { + *lut = i as f32 * (1. / 255.0); } - iter.for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; + let width = src.width; - let dst_ptr = dst.as_mut_ptr() as *mut f32; + let mut linearized_row = vec![0f32; src_r_width as usize]; - let mut linearized_row = vec![0f32; width as usize * channels]; - for (linear_chunk, src_chunk) in linearized_row - .chunks_exact_mut(channels) - .zip(src.chunks_exact(channels)) + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) + { + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; + + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + for (dst, src) in linearized_row + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + dst[0] = lut_table[src[image_configuration.r_index()] as usize]; + dst[1] = lut_table[src[image_configuration.g_index()] as usize]; + dst[2] = lut_table[src[image_configuration.b_index()] as usize]; + } + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba { - linear_chunk[image_configuration.get_r_channel_offset()] = *lut_table - .get_unchecked(src_chunk[image_configuration.get_r_channel_offset()] as usize); - linear_chunk[image_configuration.get_g_channel_offset()] = *lut_table - .get_unchecked(src_chunk[image_configuration.get_g_channel_offset()] as usize); - linear_chunk[image_configuration.get_b_channel_offset()] = *lut_table - .get_unchecked(src_chunk[image_configuration.get_b_channel_offset()] as usize); - if image_configuration.has_alpha() { - linear_chunk[image_configuration.get_a_channel_offset()] = - src_chunk[image_configuration.get_a_channel_offset()] as f32 * (1. / 255.0); + for (dst, src) in linearized_row + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + dst[0] = lut_table[src[image_configuration.r_index()] as usize]; + dst[1] = lut_table[src[image_configuration.g_index()] as usize]; + dst[2] = lut_table[src[image_configuration.b_index()] as usize]; + dst[3] = a_lut_table[src[image_configuration.a_index()] as usize]; } } + let mut cx = 0usize; + if let Some(dispatcher) = _wide_row_handle { - _cx = dispatcher( - _cx, - linearized_row.as_ptr(), - 0, - width, - dst.as_mut_ptr() as *mut f32, - 0, - display_luminance, - ); + cx = unsafe { dispatcher(cx, &linearized_row, dst, width, display_luminance) }; } - for x in _cx..width as usize { - let px = x * channels; - - let src = linearized_row.get_unchecked(px..); - let r = *src.get_unchecked(image_configuration.get_r_channel_offset()); - let g = *src.get_unchecked(image_configuration.get_g_channel_offset()); - let b = *src.get_unchecked(image_configuration.get_b_channel_offset()); - - let xyz = Xyz::from_linear_rgb(Rgb::::new(r, g, b), &SRGB_TO_XYZ_D65); - - let dst_store = dst_ptr.add(px); - - match target { - JzazbzTarget::Jzazbz => { - let jzazbz = Jzazbz::from_xyz_with_display_luminance(xyz, display_luminance); + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + let linearized_row = &linearized_row[cx * 3..]; + let dst = &mut dst[cx * 3..]; + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(linearized_row.as_chunks::<3>().0.iter()) + { + let xyz = + Xyz::from_linear_rgb(Rgb::::new(src[0], src[1], src[2]), &SRGB_TO_XYZ_D65); + match target { + JzazbzTarget::Jzazbz => { + let jzazbz = + Jzazbz::from_xyz_with_display_luminance(xyz, display_luminance); - dst_store.write_unaligned(jzazbz.jz); - dst_store.add(1).write_unaligned(jzazbz.az); - dst_store.add(2).write_unaligned(jzazbz.bz); - } - JzazbzTarget::Jzczhz => { - let jzczhz = Jzczhz::from_xyz_with_display_luminance(xyz, display_luminance); + dst[0] = jzazbz.jz; + dst[1] = jzazbz.az; + dst[2] = jzazbz.bz; + } + JzazbzTarget::Jzczhz => { + let jzczhz = + Jzczhz::from_xyz_with_display_luminance(xyz, display_luminance); - dst_store.write_unaligned(jzczhz.jz); - dst_store.add(1).write_unaligned(jzczhz.cz); - dst_store.add(2).write_unaligned(jzczhz.hz); + dst[0] = jzczhz.jz; + dst[1] = jzczhz.cz; + dst[2] = jzczhz.hz; + } } } + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba + { + let linearized_row = &linearized_row[cx * 4..]; + let dst = &mut dst[cx * 4..]; + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(linearized_row.as_chunks::<4>().0.iter()) + { + let xyz = + Xyz::from_linear_rgb(Rgb::::new(src[0], src[1], src[2]), &SRGB_TO_XYZ_D65); + match target { + JzazbzTarget::Jzazbz => { + let jzazbz = + Jzazbz::from_xyz_with_display_luminance(xyz, display_luminance); + + dst[0] = jzazbz.jz; + dst[1] = jzazbz.az; + dst[2] = jzazbz.bz; + } + JzazbzTarget::Jzczhz => { + let jzczhz = + Jzczhz::from_xyz_with_display_luminance(xyz, display_luminance); - if image_configuration.has_alpha() { - let a = *src.get_unchecked(image_configuration.get_a_channel_offset()); - dst_store.add(3).write_unaligned(a); + dst[0] = jzczhz.jz; + dst[1] = jzczhz.cz; + dst[2] = jzczhz.hz; + } + } + dst[3] = src[3]; } } - }); + } + Ok(()) } /// This function converts RGB to Jzazbz against D65 white point. This is much more effective than naive direct transformation @@ -174,25 +199,17 @@ fn channels_to_jzaz( /// * `display_luminance` - Target display luminance /// * `transfer_function` - transfer function to linear colorspace pub fn rgb_to_jzazbz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_jzaz::<{ ImageConfiguration::Rgb as u8 }, { JzazbzTarget::Jzazbz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts RGBA to Jzazbz against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -207,25 +224,17 @@ pub fn rgb_to_jzazbz( /// * `display_luminance` - Target display luminance /// * `transfer_function` - transfer function to linear colorspace pub fn rgba_to_jzazbz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_jzaz::<{ ImageConfiguration::Rgba as u8 }, { JzazbzTarget::Jzazbz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts BGRA to Jzazbz against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -240,25 +249,17 @@ pub fn rgba_to_jzazbz( /// * `display_luminance` - Target display luminance /// * `transfer_function` - transfer function to linear colorspace pub fn bgra_to_jzazbz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_jzaz::<{ ImageConfiguration::Bgra as u8 }, { JzazbzTarget::Jzazbz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts BGR to Jzazbz against D65 white point. This is much more effective than naive direct transformation @@ -273,25 +274,17 @@ pub fn bgra_to_jzazbz( /// * `display_luminance` - Target display luminance /// * `transfer_function` - transfer function to linear colorspace pub fn bgr_to_jzazbz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_jzaz::<{ ImageConfiguration::Bgr as u8 }, { JzazbzTarget::Jzazbz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts RGB to Jzczhz against D65 white point. This is much more effective than naive direct transformation @@ -306,25 +299,17 @@ pub fn bgr_to_jzazbz( /// * `display_luminance` - Target display luminance /// * `transfer_function` - transfer function to linear colorspace pub fn rgb_to_jzczhz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_jzaz::<{ ImageConfiguration::Rgb as u8 }, { JzazbzTarget::Jzczhz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts RGBA to Jzczhz against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -339,25 +324,17 @@ pub fn rgb_to_jzczhz( /// * `display_luminance` - Target display luminance /// * `transfer_function` - transfer function to linear colorspace pub fn rgba_to_jzczhz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_jzaz::<{ ImageConfiguration::Rgba as u8 }, { JzazbzTarget::Jzczhz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts BGRA to Jzczhz against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -372,25 +349,17 @@ pub fn rgba_to_jzczhz( /// * `display_luminance` - Target display luminance /// * `transfer_function` - transfer function to linear colorspace pub fn bgra_to_jzczhz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_jzaz::<{ ImageConfiguration::Bgra as u8 }, { JzazbzTarget::Jzczhz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts BGR to Jzczhz against D65 white point. This is much more effective than naive direct transformation @@ -405,23 +374,464 @@ pub fn bgra_to_jzczhz( /// * `display_luminance` - Target display luminance /// * `transfer_function` - transfer function to linear colorspace pub fn bgr_to_jzczhz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_jzaz::<{ ImageConfiguration::Bgr as u8 }, { JzazbzTarget::Jzczhz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::BufferStore; + + // ── helpers ─────────────────────────────────────────────────────────────── + + fn make_src_u8(data: Vec, width: u32, channels: u32) -> ImageBuffer<'static, u8> { + ImageBuffer { + data: std::borrow::Cow::Owned(data), + width, + height: 1, + channels, + stride: width * channels, + } + } + + fn make_dst_f32(width: u32, channels: u32) -> ImageBufferMut<'static, f32> { + ImageBufferMut { + data: BufferStore::Owned(vec![0f32; (width * channels) as usize]), + width, + height: 1, + channels, + stride: width * channels, + } + } + + const DISPLAY_LUMINANCE: f32 = 200.0; + + #[test] + fn test_rgb_to_jzazbz_black() { + // Black (0,0,0) => Jz should be ~0, az/bz ~0 + let src = make_src_u8(vec![0, 0, 0], 1, 3); + let mut dst = make_dst_f32(1, 3); + rgb_to_jzazbz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + assert!( + dst.data[0].abs() < 0.01, + "Jz ~0 for black, got {}", + dst.data[0] + ); + assert!( + dst.data[1].abs() < 0.01, + "az ~0 for black, got {}", + dst.data[1] + ); + assert!( + dst.data[2].abs() < 0.01, + "bz ~0 for black, got {}", + dst.data[2] + ); + } + + #[test] + fn test_rgb_to_jzazbz_white() { + // White (255,255,255) => Jz should be > 0, az/bz ~0 (neutral) + let src = make_src_u8(vec![255, 255, 255], 1, 3); + let mut dst = make_dst_f32(1, 3); + rgb_to_jzazbz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + assert!( + dst.data[0] > 0.0, + "Jz should be > 0 for white, got {}", + dst.data[0] + ); + assert!( + dst.data[1].abs() < 0.01, + "az ~0 for white, got {}", + dst.data[1] + ); + assert!( + dst.data[2].abs() < 0.01, + "bz ~0 for white, got {}", + dst.data[2] + ); + } + + #[test] + fn test_rgb_to_jzazbz_white_brighter_than_black() { + let src_black = make_src_u8(vec![0, 0, 0], 1, 3); + let src_white = make_src_u8(vec![255, 255, 255], 1, 3); + let mut dst_black = make_dst_f32(1, 3); + let mut dst_white = make_dst_f32(1, 3); + rgb_to_jzazbz( + &src_black, + &mut dst_black, + DISPLAY_LUMINANCE, + TransferFunction::Srgb, + ) + .unwrap(); + rgb_to_jzazbz( + &src_white, + &mut dst_white, + DISPLAY_LUMINANCE, + TransferFunction::Srgb, + ) + .unwrap(); + assert!( + dst_white.data[0] > dst_black.data[0], + "White Jz should exceed black Jz" + ); + } + + #[test] + fn test_rgb_to_jzazbz_multiple_pixels() { + let src = make_src_u8(vec![0, 0, 0, 255, 255, 255], 2, 3); + let mut dst = make_dst_f32(2, 3); + rgb_to_jzazbz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + // pixel 0 = black + assert!(dst.data[0].abs() < 0.01); + // pixel 1 = white + assert!(dst.data[3] > 0.0); + } + + #[test] + fn test_rgb_to_jzazbz_red_has_positive_az() { + // Red in Jzazbz: az should be positive (red-green axis) + let src = make_src_u8(vec![255, 0, 0], 1, 3); + let mut dst = make_dst_f32(1, 3); + rgb_to_jzazbz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + assert!( + dst.data[1] > 0.0, + "az should be positive for red, got {}", + dst.data[1] + ); + } + + #[test] + fn test_rgb_to_jzazbz_display_luminance_affects_jz() { + let src = make_src_u8(vec![128, 128, 128], 1, 3); + let mut dst_low = make_dst_f32(1, 3); + let mut dst_high = make_dst_f32(1, 3); + rgb_to_jzazbz(&src, &mut dst_low, 100.0, TransferFunction::Srgb).unwrap(); + rgb_to_jzazbz(&src, &mut dst_high, 1000.0, TransferFunction::Srgb).unwrap(); + assert_ne!( + dst_low.data[0], dst_high.data[0], + "Different display luminance should yield different Jz" + ); + } + + // ── rgba_to_jzazbz ─────────────────────────────────────────────────────── + + #[test] + fn test_rgba_to_jzazbz_alpha_normalized() { + // Alpha 255 => normalized to 1.0 + let src = make_src_u8(vec![255, 255, 255, 255], 1, 4); + let mut dst = make_dst_f32(1, 4); + rgba_to_jzazbz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + let alpha = dst.data[3]; + assert!( + (alpha - 1.0).abs() < 0.01, + "Alpha should be ~1.0, got {alpha}" + ); + } + + #[test] + fn test_rgba_to_jzazbz_zero_alpha() { + let src = make_src_u8(vec![255, 255, 255, 0], 1, 4); + let mut dst = make_dst_f32(1, 4); + rgba_to_jzazbz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + assert!( + dst.data[3].abs() < 0.01, + "Alpha should be ~0.0, got {}", + dst.data[3] + ); + } + + #[test] + fn test_rgba_to_jzazbz_half_alpha() { + let src = make_src_u8(vec![0, 0, 0, 128], 1, 4); + let mut dst = make_dst_f32(1, 4); + rgba_to_jzazbz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + let alpha = dst.data[3]; + assert!( + (alpha - 128.0 / 255.0).abs() < 0.01, + "Alpha ~0.502, got {alpha}" + ); + } + + // ── bgr_to_jzazbz ──────────────────────────────────────────────────────── + + #[test] + fn test_bgr_to_jzazbz_matches_rgb_for_neutral() { + // For neutral grey RGB==BGR so results must match + let src_rgb = make_src_u8(vec![128, 128, 128], 1, 3); + let src_bgr = make_src_u8(vec![128, 128, 128], 1, 3); + let mut dst_rgb = make_dst_f32(1, 3); + let mut dst_bgr = make_dst_f32(1, 3); + rgb_to_jzazbz( + &src_rgb, + &mut dst_rgb, + DISPLAY_LUMINANCE, + TransferFunction::Srgb, + ) + .unwrap(); + bgr_to_jzazbz( + &src_bgr, + &mut dst_bgr, + DISPLAY_LUMINANCE, + TransferFunction::Srgb, + ) + .unwrap(); + assert!((dst_rgb.data[0] - dst_bgr.data[0]).abs() < 1e-5); + assert!((dst_rgb.data[1] - dst_bgr.data[1]).abs() < 1e-5); + assert!((dst_rgb.data[2] - dst_bgr.data[2]).abs() < 1e-5); + } + + #[test] + fn test_bgr_to_jzazbz_red_channel_swap() { + // Pure red in RGB = (255,0,0); pure red in BGR = (0,0,255) + // Both should give same Jz/az/bz since they represent the same color + let src_rgb = make_src_u8(vec![255, 0, 0], 1, 3); + let src_bgr = make_src_u8(vec![0, 0, 255], 1, 3); + let mut dst_rgb = make_dst_f32(1, 3); + let mut dst_bgr = make_dst_f32(1, 3); + rgb_to_jzazbz( + &src_rgb, + &mut dst_rgb, + DISPLAY_LUMINANCE, + TransferFunction::Srgb, + ) + .unwrap(); + bgr_to_jzazbz( + &src_bgr, + &mut dst_bgr, + DISPLAY_LUMINANCE, + TransferFunction::Srgb, + ) + .unwrap(); + assert!( + (dst_rgb.data[0] - dst_bgr.data[0]).abs() < 1e-4, + "Jz mismatch" + ); + assert!( + (dst_rgb.data[1] - dst_bgr.data[1]).abs() < 1e-4, + "az mismatch" + ); + assert!( + (dst_rgb.data[2] - dst_bgr.data[2]).abs() < 1e-4, + "bz mismatch" + ); + } + + // ── bgra_to_jzazbz ─────────────────────────────────────────────────────── + + #[test] + fn test_bgra_to_jzazbz_alpha_preserved() { + let src = make_src_u8(vec![0, 0, 255, 200], 1, 4); // BGR red + alpha 200 + let mut dst = make_dst_f32(1, 4); + bgra_to_jzazbz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + let alpha = dst.data[3]; + assert!( + (alpha - 200.0 / 255.0).abs() < 0.01, + "Alpha ~0.784, got {alpha}" + ); + } + + // ── rgb_to_jzczhz ──────────────────────────────────────────────────────── + + #[test] + fn test_rgb_to_jzczhz_black() { + let src = make_src_u8(vec![0, 0, 0], 1, 3); + let mut dst = make_dst_f32(1, 3); + rgb_to_jzczhz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + assert!(dst.data[0].abs() < 0.01, "Jz ~0 for black"); + assert!(dst.data[1].abs() < 0.01, "Cz ~0 for black"); + } + + #[test] + fn test_rgb_to_jzczhz_white_low_chroma() { + // White is neutral — chroma (Cz) should be near 0 + let src = make_src_u8(vec![255, 255, 255], 1, 3); + let mut dst = make_dst_f32(1, 3); + rgb_to_jzczhz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + assert!(dst.data[0] > 0.0, "Jz > 0 for white"); + assert!( + dst.data[1].abs() < 0.01, + "Cz ~0 for white, got {}", + dst.data[1] + ); + } + + #[test] + fn test_rgb_to_jzczhz_saturated_has_chroma() { + // A saturated colour should have Cz > 0 + let src = make_src_u8(vec![255, 0, 0], 1, 3); + let mut dst = make_dst_f32(1, 3); + rgb_to_jzczhz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + assert!( + dst.data[1] > 0.0, + "Cz should be > 0 for red, got {}", + dst.data[1] + ); + } + + #[test] + fn test_rgb_to_jzczhz_hue_in_range() { + // Hz (hue) must be in [0, 2π) + let src = make_src_u8(vec![255, 0, 0], 1, 3); + let mut dst = make_dst_f32(1, 3); + rgb_to_jzczhz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + let hz = dst.data[2]; + assert!( + hz >= 0.0 && hz < std::f32::consts::TAU, + "Hz out of range: {hz}" + ); + } + + // ── rgba_to_jzczhz ─────────────────────────────────────────────────────── + + #[test] + fn test_rgba_to_jzczhz_alpha_passthrough() { + let src = make_src_u8(vec![255, 0, 0, 180], 1, 4); + let mut dst = make_dst_f32(1, 4); + rgba_to_jzczhz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + let alpha = dst.data[3]; + assert!( + (alpha - 180.0 / 255.0).abs() < 0.01, + "Alpha ~0.706, got {alpha}" + ); + } + + // ── bgr_to_jzczhz ──────────────────────────────────────────────────────── + + #[test] + fn test_bgr_to_jzczhz_neutral_matches_rgb() { + let src_rgb = make_src_u8(vec![100, 100, 100], 1, 3); + let src_bgr = make_src_u8(vec![100, 100, 100], 1, 3); + let mut dst_rgb = make_dst_f32(1, 3); + let mut dst_bgr = make_dst_f32(1, 3); + rgb_to_jzczhz( + &src_rgb, + &mut dst_rgb, + DISPLAY_LUMINANCE, + TransferFunction::Srgb, + ) + .unwrap(); + bgr_to_jzczhz( + &src_bgr, + &mut dst_bgr, + DISPLAY_LUMINANCE, + TransferFunction::Srgb, + ) + .unwrap(); + assert!((dst_rgb.data[0] - dst_bgr.data[0]).abs() < 1e-5); + assert!((dst_rgb.data[1] - dst_bgr.data[1]).abs() < 1e-5); + } + + // ── bgra_to_jzczhz ─────────────────────────────────────────────────────── + + #[test] + fn test_bgra_to_jzczhz_alpha_passthrough() { + let src = make_src_u8(vec![0, 255, 0, 90], 1, 4); // BGR green + alpha + let mut dst = make_dst_f32(1, 4); + bgra_to_jzczhz(&src, &mut dst, DISPLAY_LUMINANCE, TransferFunction::Srgb).unwrap(); + let alpha = dst.data[3]; + assert!( + (alpha - 90.0 / 255.0).abs() < 0.01, + "Alpha ~0.353, got {alpha}" + ); + } + + // ── jzazbz vs jzczhz consistency ───────────────────────────────────────── + + #[test] + fn test_jzazbz_and_jzczhz_same_jz_for_same_input() { + // Both share the same Jz lightness — must agree + let src1 = make_src_u8(vec![200, 100, 50], 1, 3); + let src2 = make_src_u8(vec![200, 100, 50], 1, 3); + let mut dst_lab = make_dst_f32(1, 3); + let mut dst_lch = make_dst_f32(1, 3); + rgb_to_jzazbz( + &src1, + &mut dst_lab, + DISPLAY_LUMINANCE, + TransferFunction::Srgb, + ) + .unwrap(); + rgb_to_jzczhz( + &src2, + &mut dst_lch, + DISPLAY_LUMINANCE, + TransferFunction::Srgb, + ) + .unwrap(); + assert!( + (dst_lab.data[0] - dst_lch.data[0]).abs() < 1e-4, + "Jz must match between Jzazbz and Jzczhz: {} vs {}", + dst_lab.data[0], + dst_lch.data[0] + ); + } + + #[test] + fn test_jzazbz_chroma_equals_jzczhz_cz() { + // Cz = sqrt(az² + bz²) + let src1 = make_src_u8(vec![255, 0, 0], 1, 3); + let src2 = make_src_u8(vec![255, 0, 0], 1, 3); + let mut dst_lab = make_dst_f32(1, 3); + let mut dst_lch = make_dst_f32(1, 3); + rgb_to_jzazbz( + &src1, + &mut dst_lab, + DISPLAY_LUMINANCE, + TransferFunction::Srgb, + ) + .unwrap(); + rgb_to_jzczhz( + &src2, + &mut dst_lch, + DISPLAY_LUMINANCE, + TransferFunction::Srgb, + ) + .unwrap(); + let cz_from_lab = (dst_lab.data[1].powi(2) + dst_lab.data[2].powi(2)).sqrt(); + let cz_from_lch = dst_lch.data[1]; + assert!( + (cz_from_lab - cz_from_lch).abs() < 1e-4, + "Cz mismatch: sqrt(az²+bz²)={cz_from_lab} vs Cz={cz_from_lch}" + ); + } + + // ── transfer function ───────────────────────────────────────────────────── + + #[test] + fn test_linear_transfer_function_differs_from_srgb() { + let src1 = make_src_u8(vec![128, 64, 32], 1, 3); + let src2 = make_src_u8(vec![128, 64, 32], 1, 3); + let mut dst_srgb = make_dst_f32(1, 3); + let mut dst_linear = make_dst_f32(1, 3); + rgb_to_jzazbz( + &src1, + &mut dst_srgb, + DISPLAY_LUMINANCE, + TransferFunction::Srgb, + ) + .unwrap(); + rgb_to_jzazbz( + &src2, + &mut dst_linear, + DISPLAY_LUMINANCE, + TransferFunction::Linear, + ) + .unwrap(); + assert_ne!( + dst_srgb.data[0], dst_linear.data[0], + "Srgb and Linear transfer functions should produce different Jz" + ); + } } diff --git a/src/image_to_lalphabeta.rs b/src/image_to_lalphabeta.rs index 90ab16a..fe6f5ce 100644 --- a/src/image_to_lalphabeta.rs +++ b/src/image_to_lalphabeta.rs @@ -61,15 +61,15 @@ fn channels_to_lalphabeta( .chunks_exact_mut(channels) .zip(src.chunks_exact(channels)) { - linear_chunk[image_configuration.get_r_channel_offset()] = *lut_table - .get_unchecked(src_chunk[image_configuration.get_r_channel_offset()] as usize); - linear_chunk[image_configuration.get_g_channel_offset()] = *lut_table - .get_unchecked(src_chunk[image_configuration.get_g_channel_offset()] as usize); - linear_chunk[image_configuration.get_b_channel_offset()] = *lut_table - .get_unchecked(src_chunk[image_configuration.get_b_channel_offset()] as usize); + linear_chunk[image_configuration.r_index()] = *lut_table + .get_unchecked(src_chunk[image_configuration.r_index()] as usize); + linear_chunk[image_configuration.g_index()] = *lut_table + .get_unchecked(src_chunk[image_configuration.g_index()] as usize); + linear_chunk[image_configuration.b_index()] = *lut_table + .get_unchecked(src_chunk[image_configuration.b_index()] as usize); if image_configuration.has_alpha() { - linear_chunk[image_configuration.get_a_channel_offset()] = - src_chunk[image_configuration.get_a_channel_offset()] as f32 * (1. / 255.0); + linear_chunk[image_configuration.a_index()] = + src_chunk[image_configuration.a_index()] as f32 * (1. / 255.0); } } @@ -79,9 +79,9 @@ fn channels_to_lalphabeta( let px = x * channels; let src = linearized_row.get_unchecked(px..); - let r = *src.get_unchecked(image_configuration.get_r_channel_offset()); - let g = *src.get_unchecked(image_configuration.get_g_channel_offset()); - let b = *src.get_unchecked(image_configuration.get_b_channel_offset()); + let r = *src.get_unchecked(image_configuration.r_index()); + let g = *src.get_unchecked(image_configuration.g_index()); + let b = *src.get_unchecked(image_configuration.b_index()); let rgb = Rgb::::new(r, g, b); let dst_store = dst_ptr.add(px); @@ -91,7 +91,7 @@ fn channels_to_lalphabeta( dst_store.add(2).write_unaligned(lalphabeta.beta); if image_configuration.has_alpha() { - let a = *src.get_unchecked(image_configuration.get_a_channel_offset()); + let a = *src.get_unchecked(image_configuration.a_index()); dst_store.add(3).write_unaligned(a); } } diff --git a/src/image_to_linear.rs b/src/image_to_linear.rs index 3735479..af8fec7 100644 --- a/src/image_to_linear.rs +++ b/src/image_to_linear.rs @@ -6,97 +6,79 @@ */ use crate::gamma_curves::TransferFunction; use crate::image::ImageConfiguration; -use crate::Rgb; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -use std::slice; +use crate::{ColorError, ImageBuffer, ImageBufferMut}; #[allow(clippy::type_complexity)] fn channels_to_linear( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { + src.validate()?; + dst.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); if USE_ALPHA && !image_configuration.has_alpha() { panic!("Alpha may be set only on images with alpha"); } - let channels = image_configuration.get_channels_count(); - - let mut lut_table = vec![0f32; 256]; + let mut lut_table = [0f32; 256]; for (i, lut) in lut_table.iter_mut().enumerate() { *lut = transfer_function.linearize(i as f32 * (1. / 255.0)); } - let dst_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - dst.as_mut_ptr() as *mut u8, - dst_stride as usize * height as usize, - ) - }; - - let iter; - - #[cfg(feature = "rayon")] - { - iter = dst_slice_safe_align - .par_chunks_exact_mut(dst_stride as usize) - .zip(src.par_chunks_exact(src_stride as usize)); + let mut a_lut_table = [0f32; 256]; + for (i, lut) in a_lut_table.iter_mut().enumerate() { + *lut = i as f32 * (1. / 255.0); } - #[cfg(not(feature = "rayon"))] + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; + + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) { - iter = dst_slice_safe_align - .chunks_exact_mut(dst_stride as usize) - .zip(src.chunks_exact(src_stride as usize)); - } - - iter.for_each(|(dst_row, src_row)| unsafe { - let mut _cx = 0usize; - - let src_ptr = src_row.as_ptr(); - let dst_ptr = dst_row.as_mut_ptr() as *mut f32; - - for x in _cx..width as usize { - let px = x * channels; - let dst = dst_ptr.add(px); - let src = src_ptr.add(px); - let r = src - .add(image_configuration.get_r_channel_offset()) - .read_unaligned(); - let g = src - .add(image_configuration.get_g_channel_offset()) - .read_unaligned(); - let b = src - .add(image_configuration.get_b_channel_offset()) - .read_unaligned(); - - let rgb = Rgb::::new(r, g, b); - - dst.add(image_configuration.get_r_channel_offset()) - .write_unaligned(*lut_table.get_unchecked(rgb.r as usize)); - dst.add(image_configuration.get_g_channel_offset()) - .write_unaligned(*lut_table.get_unchecked(rgb.g as usize)); - dst.add(image_configuration.get_b_channel_offset()) - .write_unaligned(*lut_table.get_unchecked(rgb.b as usize)); - - if USE_ALPHA && image_configuration.has_alpha() { - let a = src - .add(image_configuration.get_a_channel_offset()) - .read_unaligned(); - let a_lin = a as f32 * (1f32 / 255f32); - dst.add(image_configuration.get_a_channel_offset()) - .write_unaligned(a_lin); + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; + + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + dst[0] = lut_table[src[0] as usize]; + dst[1] = lut_table[src[1] as usize]; + dst[2] = lut_table[src[2] as usize]; + } + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba + { + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + dst[0] = lut_table[src[0] as usize]; + dst[1] = lut_table[src[1] as usize]; + dst[2] = lut_table[src[2] as usize]; + dst[3] = a_lut_table[src[3] as usize]; } } - }); + } + + Ok(()) } /// This function converts RGB to linear colorspace @@ -112,23 +94,11 @@ fn channels_to_linear( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn rgb_to_linear( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { - channels_to_linear::<{ ImageConfiguration::Rgb as u8 }, false>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + channels_to_linear::<{ ImageConfiguration::Rgb as u8 }, false>(src, dst, transfer_function) } /// This function converts RGBA to liner color space @@ -144,23 +114,11 @@ pub fn rgb_to_linear( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn rgba_to_linear( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { - channels_to_linear::<{ ImageConfiguration::Rgba as u8 }, true>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + channels_to_linear::<{ ImageConfiguration::Rgba as u8 }, true>(src, dst, transfer_function) } /// This function converts BGRA to Linear. @@ -176,23 +134,11 @@ pub fn rgba_to_linear( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn bgra_to_linear( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { - channels_to_linear::<{ ImageConfiguration::Bgra as u8 }, true>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + channels_to_linear::<{ ImageConfiguration::Bgra as u8 }, true>(src, dst, transfer_function) } /// This function converts BGR to linear @@ -208,21 +154,194 @@ pub fn bgra_to_linear( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn bgr_to_linear( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { - channels_to_linear::<{ ImageConfiguration::Bgr as u8 }, false>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + channels_to_linear::<{ ImageConfiguration::Bgr as u8 }, false>(src, dst, transfer_function) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::BufferStore; + + fn make_src(width: u32, height: u32, channels: u32, data: Vec) -> ImageBuffer<'static, u8> { + ImageBuffer { + width, + height, + channels, + data: data.into(), + stride: width * channels, + } + } + + fn make_dst(width: u32, height: u32, channels: u32) -> ImageBufferMut<'static, f32> { + let len = (width * height * channels) as usize; + ImageBufferMut { + width, + height, + channels, + data: BufferStore::Owned(vec![0f32; len]), + stride: width * channels, + } + } + + #[test] + fn alpha_channel_is_linear_not_gamma() { + // Alpha at 128 → should be ~0.502 (128/255), not sRGB-linearised (~0.216) + let src = make_src(1, 1, 4, vec![0u8, 0, 0, 128]); + let mut dst = make_dst(1, 1, 4); + let _ = rgba_to_linear(&src, &mut dst, TransferFunction::Srgb); + let alpha_out = dst.data.borrow()[3]; + let expected = 128.0 / 255.0; + assert!( + (alpha_out - expected).abs() < 1e-4, + "expected alpha ≈ {expected}, got {alpha_out}" + ); + } + + #[test] + fn color_channel_is_gamma_corrected_not_linear() { + // 128 in sRGB ≠ 128/255 ≈ 0.502 in linear; it should be ≈ 0.216 + let src = make_src(1, 1, 3, vec![128u8, 128, 128]); + let mut dst = make_dst(1, 1, 3); + let _ = rgb_to_linear(&src, &mut dst, TransferFunction::Srgb); + let r = dst.data.borrow()[0]; + // sRGB linearisation of 0.502 ≈ 0.2158 + assert!( + (r - 0.2158).abs() < 5e-3, + "expected r ≈ 0.2158 (sRGB linearised), got {r}" + ); + } + + #[test] + fn black_pixel_maps_to_zero() { + let src = make_src(1, 1, 3, vec![0u8, 0, 0]); + let mut dst = make_dst(1, 1, 3); + let _ = rgb_to_linear(&src, &mut dst, TransferFunction::Srgb); + for v in dst.data.borrow().iter() { + assert_eq!(*v, 0.0, "black should map to 0.0"); + } + } + + #[test] + fn white_pixel_maps_to_one() { + let src = make_src(1, 1, 3, vec![255u8, 255, 255]); + let mut dst = make_dst(1, 1, 3); + let _ = rgb_to_linear(&src, &mut dst, TransferFunction::Srgb); + for v in dst.data.borrow().iter() { + assert!((v - 1.0).abs() < 1e-4, "white should map to ≈ 1.0, got {v}"); + } + } + + // ── channel-order: BGR vs RGB ───────────────────────────────────────────── + + #[test] + fn bgr_channel_order_is_preserved() { + // A pixel with distinct R/G/B values; we verify the values land in + // the right output slots regardless of byte ordering. + let r_in = 200u8; + let g_in = 100u8; + let b_in = 50u8; + + let src_rgb = make_src(1, 1, 3, vec![r_in, g_in, b_in]); + let mut dst_rgb = make_dst(1, 1, 3); + let _ = rgb_to_linear(&src_rgb, &mut dst_rgb, TransferFunction::Srgb); + + let src_bgr = make_src(1, 1, 3, vec![b_in, g_in, r_in]); + let mut dst_bgr = make_dst(1, 1, 3); + let _ = bgr_to_linear(&src_bgr, &mut dst_bgr, TransferFunction::Srgb); + + let rgb = dst_rgb.data.borrow(); + let bgr = dst_bgr.data.borrow(); + // bgr output slot 0 = Blue, rgb output slot 2 = Blue + assert!((bgr[0] - rgb[2]).abs() < 1e-5, "B mismatch"); + assert!((bgr[1] - rgb[1]).abs() < 1e-5, "G mismatch"); + assert!((bgr[2] - rgb[0]).abs() < 1e-5, "R mismatch"); + } + + // ── multi-pixel / stride correctness ───────────────────────────────────── + + #[test] + fn multi_pixel_row_all_channels_converted() { + // 3 pixels in a single row + let src = make_src( + 3, + 1, + 3, + vec![ + 255, 0, 0, // red + 0, 255, 0, // green + 0, 0, 255, // blue + ], + ); + let mut dst = make_dst(3, 1, 3); + let _ = rgb_to_linear(&src, &mut dst, TransferFunction::Srgb); + let d = dst.data.borrow(); + assert!((d[0] - 1.0).abs() < 1e-4, "pixel 0 R should be 1.0"); + assert_eq!(d[1], 0.0, "pixel 0 G should be 0.0"); + assert_eq!(d[2], 0.0, "pixel 0 B should be 0.0"); + assert_eq!(d[3], 0.0, "pixel 1 R should be 0.0"); + assert!((d[4] - 1.0).abs() < 1e-4, "pixel 1 G should be 1.0"); + } + + #[test] + fn multi_row_image_respects_stride() { + // 1×2 image (two rows of a single pixel) + let src = make_src( + 1, + 2, + 3, + vec![ + 255, 255, 255, // row 0: white + 0, 0, 0, // row 1: black + ], + ); + let mut dst = make_dst(1, 2, 3); + let _ = rgb_to_linear(&src, &mut dst, TransferFunction::Srgb); + let d = dst.data.borrow(); + assert!((d[0] - 1.0).abs() < 1e-4, "row 0 should be white (1.0)"); + assert_eq!(d[3], 0.0, "row 1 should be black (0.0)"); + } + + #[test] + fn rgba_alpha_fully_opaque_remains_one() { + let src = make_src(1, 1, 4, vec![128, 64, 32, 255]); + let mut dst = make_dst(1, 1, 4); + let _ = rgba_to_linear(&src, &mut dst, TransferFunction::Srgb); + let alpha = dst.data.borrow()[3]; + assert!((alpha - 1.0).abs() < 1e-4, "alpha 255 → 1.0, got {alpha}"); + } + + #[test] + fn rgba_alpha_fully_transparent_is_zero() { + let src = make_src(1, 1, 4, vec![255, 255, 255, 0]); + let mut dst = make_dst(1, 1, 4); + let _ = rgba_to_linear(&src, &mut dst, TransferFunction::Srgb); + let alpha = dst.data.borrow()[3]; + assert_eq!(alpha, 0.0, "alpha 0 → 0.0"); + } + + #[test] + fn mismatched_dimensions_returns_error() { + let src = make_src(2, 2, 3, vec![0u8; 12]); + let mut dst = make_dst(4, 4, 3); // different size + let result = rgb_to_linear(&src, &mut dst, TransferFunction::Srgb); + assert!(result.is_err(), "expected error for dimension mismatch"); + } + + #[test] + fn linear_transfer_function_is_identity() { + // With a linear (gamma = 1.0) transfer function 128 → 128/255 ≈ 0.502 + let src = make_src(1, 1, 3, vec![128, 128, 128]); + let mut dst = make_dst(1, 1, 3); + let _ = rgb_to_linear(&src, &mut dst, TransferFunction::Linear); + let v = dst.data.borrow()[0]; + let expected = 128.0 / 255.0; + assert!( + (v - expected).abs() < 1e-4, + "linear TF: expected {expected}, got {v}" + ); + } } diff --git a/src/image_to_linear_u8.rs b/src/image_to_linear_u8.rs index e8e4d2b..d4733dd 100644 --- a/src/image_to_linear_u8.rs +++ b/src/image_to_linear_u8.rs @@ -6,72 +6,74 @@ */ use crate::gamma_curves::TransferFunction; use crate::image::ImageConfiguration; -use crate::Rgb; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; +use crate::{ColorError, ImageBuffer, ImageBufferMut}; #[allow(clippy::type_complexity)] fn channels_to_linear( - l_src: &[u8], - src_stride: u32, - l_dst: &mut [u8], - dst_stride: u32, - width: u32, - _: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { + src.validate()?; + dst.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); if USE_ALPHA && !image_configuration.has_alpha() { panic!("Alpha may be set only on images with alpha"); } - let channels = image_configuration.get_channels_count(); - - let mut lut_table = vec![0u8; 256]; + let mut lut_table = [0u8; 256]; for (i, lut) in lut_table.iter_mut().enumerate() { - *lut = (transfer_function.linearize(i as f32 * (1. / 255.0)) * 255.).min(255.) as u8; - } - - let iter; - #[cfg(feature = "rayon")] - { - iter = l_dst - .par_chunks_exact_mut(dst_stride as usize) - .zip(l_src.par_chunks_exact(src_stride as usize)); + *lut = (transfer_function.linearize(i as f32 * (1. / 255.0)) * 255.) + .round() + .min(255.) as u8; } - #[cfg(not(feature = "rayon"))] - { - iter = l_dst - .chunks_exact_mut(dst_stride as usize) - .zip(l_src.chunks_exact(src_stride as usize)); - } - - iter.for_each(|(dst_row, src_row)| unsafe { - let mut _cx = 0usize; - - for x in _cx..width as usize { - let px = x * channels; - let r = *src_row.get_unchecked(px + image_configuration.get_r_channel_offset()); - let g = *src_row.get_unchecked(px + image_configuration.get_g_channel_offset()); - let b = *src_row.get_unchecked(px + image_configuration.get_b_channel_offset()); - - let rgb = Rgb::::new(r, g, b); - *dst_row.get_unchecked_mut(px + image_configuration.get_r_channel_offset()) = - *lut_table.get_unchecked(rgb.r as usize); - *dst_row.get_unchecked_mut(px + image_configuration.get_g_channel_offset()) = - *lut_table.get_unchecked(rgb.g as usize); - *dst_row.get_unchecked_mut(px + image_configuration.get_b_channel_offset()) = - *lut_table.get_unchecked(rgb.b as usize); + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; - if USE_ALPHA && image_configuration.has_alpha() { - let a = *src_row.get_unchecked(px + image_configuration.get_a_channel_offset()); - *dst_row.get_unchecked_mut(px + image_configuration.get_a_channel_offset()) = a; + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) + { + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + dst[0] = lut_table[src[0] as usize]; + dst[1] = lut_table[src[1] as usize]; + dst[2] = lut_table[src[2] as usize]; + } + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba + { + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + dst[0] = lut_table[src[0] as usize]; + dst[1] = lut_table[src[1] as usize]; + dst[2] = lut_table[src[2] as usize]; + dst[3] = src[3]; } } - }); + } + Ok(()) } /// This function converts RGB to Linear. This is much more effective than naive direct transformation @@ -85,23 +87,11 @@ fn channels_to_linear( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn rgb_to_linear_u8( - src: &[u8], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { - channels_to_linear::<{ ImageConfiguration::Rgb as u8 }, false>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + channels_to_linear::<{ ImageConfiguration::Rgb as u8 }, false>(src, dst, transfer_function) } /// This function converts RGBA to Linear, Alpha channel is normalized. This is much more effective than naive direct transformation @@ -115,23 +105,11 @@ pub fn rgb_to_linear_u8( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn rgba_to_linear_u8( - src: &[u8], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { - channels_to_linear::<{ ImageConfiguration::Rgba as u8 }, true>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + channels_to_linear::<{ ImageConfiguration::Rgba as u8 }, true>(src, dst, transfer_function) } /// This function converts BGRA to Linear, Alpha channel is normalized. This is much more effective than naive direct transformation @@ -145,23 +123,11 @@ pub fn rgba_to_linear_u8( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn bgra_to_linear_u8( - src: &[u8], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { - channels_to_linear::<{ ImageConfiguration::Bgra as u8 }, true>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + channels_to_linear::<{ ImageConfiguration::Bgra as u8 }, true>(src, dst, transfer_function) } /// This function converts BGR to Linear. This is much more effective than naive direct transformation @@ -175,21 +141,299 @@ pub fn bgra_to_linear_u8( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn bgr_to_linear_u8( - src: &[u8], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { - channels_to_linear::<{ ImageConfiguration::Bgr as u8 }, false>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + channels_to_linear::<{ ImageConfiguration::Bgr as u8 }, false>(src, dst, transfer_function) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::BufferStore; + + // ── helpers ─────────────────────────────────────────────────────────────── + + fn make_src(width: u32, height: u32, channels: u32, data: Vec) -> ImageBuffer<'static, u8> { + ImageBuffer { + width, + height, + channels, + data: data.into(), + stride: width * channels, + } + } + + fn make_dst(width: u32, height: u32, channels: u32) -> ImageBufferMut<'static, u8> { + let len = (width * height * channels) as usize; + ImageBufferMut { + width, + height, + channels, + data: BufferStore::Owned(vec![0u8; len]), + stride: width * channels, + } + } + + // Helper: linearise a single byte through the sRGB transfer function, + // matching exactly how the LUT is built in `channels_to_linear`. + fn srgb_linearize_u8(v: u8) -> u8 { + (TransferFunction::Srgb.linearize(v as f32 / 255.0) * 255.0).min(255.0) as u8 + } + + // ── validation ──────────────────────────────────────────────────────────── + + #[test] + fn unsupported_channel_count_returns_error() { + let src = make_src(1, 1, 2, vec![128u8, 64]); + let mut dst = make_dst(1, 1, 2); + let result = rgb_to_linear_u8(&src, &mut dst, TransferFunction::Srgb); + assert!(matches!( + result, + Err(ColorError::UnsupportedChannelsCount(2)) + )); + } + + #[test] + fn mismatched_dimensions_returns_error() { + let src = make_src(2, 2, 3, vec![0u8; 12]); + let mut dst = make_dst(4, 4, 3); + let result = rgb_to_linear_u8(&src, &mut dst, TransferFunction::Srgb); + assert!(result.is_err()); + } + + // ── black / white anchors ───────────────────────────────────────────────── + + #[test] + fn black_maps_to_zero() { + let src = make_src(1, 1, 3, vec![0, 0, 0]); + let mut dst = make_dst(1, 1, 3); + rgb_to_linear_u8(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst.data.borrow(), &[0u8, 0, 0]); + } + + #[test] + fn white_maps_to_255() { + let src = make_src(1, 1, 3, vec![255, 255, 255]); + let mut dst = make_dst(1, 1, 3); + rgb_to_linear_u8(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst.data.borrow(), &[255u8, 255, 255]); + } + + // ── LUT correctness ─────────────────────────────────────────────────────── + + #[test] + fn mid_grey_matches_srgb_lut() { + let v = 128u8; + let src = make_src(1, 1, 3, vec![v, v, v]); + let mut dst = make_dst(1, 1, 3); + rgb_to_linear_u8(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let expected = srgb_linearize_u8(v); + let d = dst.data.borrow(); + assert_eq!(d[0], expected, "R: expected {expected}, got {}", d[0]); + assert_eq!(d[1], expected, "G: expected {expected}, got {}", d[1]); + assert_eq!(d[2], expected, "B: expected {expected}, got {}", d[2]); + } + + #[test] + fn all_256_lut_entries_are_monotonically_non_decreasing() { + // Linearisation must be monotone — a brighter input can never produce + // a darker output. + for v in 1u8..=255 { + let lo = srgb_linearize_u8(v - 1); + let hi = srgb_linearize_u8(v); + assert!( + hi >= lo, + "LUT not monotone: lut[{}]={} > lut[{}]={}", + v, + hi, + v - 1, + lo + ); + } + } + + #[test] + fn linear_transfer_function_is_identity() { + // With a γ=1 (linear) transfer function the LUT should be the identity. + for v in 0u8..=255 { + let out = + (TransferFunction::Linear.linearize(v as f32 / 255.0) * 255.0).min(255.0) as u8; + assert_eq!(out, v, "linear TF: lut[{v}]={out}, expected {v}"); + } + } + + // ── sRGB vs linear TF produce different results for mid-grey ───────────── + + #[test] + fn srgb_and_linear_tf_differ_for_mid_grey() { + let src_srgb = make_src(1, 1, 3, vec![128, 128, 128]); + let src_linear = make_src(1, 1, 3, vec![128, 128, 128]); + let mut dst_srgb = make_dst(1, 1, 3); + let mut dst_linear = make_dst(1, 1, 3); + rgb_to_linear_u8(&src_srgb, &mut dst_srgb, TransferFunction::Srgb).unwrap(); + rgb_to_linear_u8(&src_linear, &mut dst_linear, TransferFunction::Linear).unwrap(); + let srgb_out = dst_srgb.data.borrow()[0]; + let linear_out = dst_linear.data.borrow()[0]; + assert_ne!( + srgb_out, linear_out, + "sRGB and linear TF should give different results for mid-grey" + ); + assert_eq!(linear_out, 128, "linear TF should be identity for 128"); + } + + // ── RGBA: alpha is passed through unchanged ─────────────────────────────── + + #[test] + fn rgba_alpha_is_passed_through_unchanged() { + for alpha in [0u8, 1, 127, 128, 254, 255] { + let src = make_src(1, 1, 4, vec![200, 100, 50, alpha]); + let mut dst = make_dst(1, 1, 4); + rgba_to_linear_u8(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let out_alpha = dst.data.borrow()[3]; + assert_eq!( + out_alpha, alpha, + "alpha {alpha} should be passed through unchanged, got {out_alpha}" + ); + } + } + + #[test] + fn rgba_colour_channels_are_linearised_independently_of_alpha() { + let v = 200u8; + let expected = srgb_linearize_u8(v); + + let src_opaque = make_src(1, 1, 4, vec![v, v, v, 255]); + let src_transp = make_src(1, 1, 4, vec![v, v, v, 0]); + let mut dst_opaque = make_dst(1, 1, 4); + let mut dst_transp = make_dst(1, 1, 4); + rgba_to_linear_u8(&src_opaque, &mut dst_opaque, TransferFunction::Srgb).unwrap(); + rgba_to_linear_u8(&src_transp, &mut dst_transp, TransferFunction::Srgb).unwrap(); + + for i in 0..3 { + assert_eq!(dst_opaque.data.borrow()[i], expected); + assert_eq!(dst_transp.data.borrow()[i], expected); + } + } + + // ── BGRA: alpha is passed through unchanged ─────────────────────────────── + + #[test] + fn bgra_alpha_is_passed_through_unchanged() { + let src = make_src(1, 1, 4, vec![50, 100, 200, 128]); + let mut dst = make_dst(1, 1, 4); + bgra_to_linear_u8(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!( + dst.data.borrow()[3], + 128, + "BGRA alpha must be copied verbatim" + ); + } + + // ── BGR / RGB channel ordering ──────────────────────────────────────────── + + #[test] + fn bgr_and_rgb_agree_on_grey() { + // Grey is order-invariant: both paths must produce the same output. + let src_rgb = make_src(1, 1, 3, vec![128, 128, 128]); + let src_bgr = make_src(1, 1, 3, vec![128, 128, 128]); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + rgb_to_linear_u8(&src_rgb, &mut dst_rgb, TransferFunction::Srgb).unwrap(); + bgr_to_linear_u8(&src_bgr, &mut dst_bgr, TransferFunction::Srgb).unwrap(); + assert_eq!( + &*dst_rgb.data.borrow(), + &*dst_bgr.data.borrow(), + "grey should give identical output for rgb and bgr" + ); + } + + #[test] + fn bgr_reversal_matches_rgb_with_reversed_input() { + let (r, g, b) = (200u8, 128u8, 50u8); + + let src_rgb = make_src(1, 1, 3, vec![r, g, b]); + let src_bgr = make_src(1, 1, 3, vec![b, g, r]); // reversed + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + rgb_to_linear_u8(&src_rgb, &mut dst_rgb, TransferFunction::Srgb).unwrap(); + bgr_to_linear_u8(&src_bgr, &mut dst_bgr, TransferFunction::Srgb).unwrap(); + + // BGR output slot 0 is B_linear; RGB output slot 2 is B_linear + let r_out = dst_rgb.data.borrow(); + let b_out = dst_bgr.data.borrow(); + assert_eq!(b_out[0], r_out[2], "B channel mismatch"); + assert_eq!(b_out[1], r_out[1], "G channel mismatch"); + assert_eq!(b_out[2], r_out[0], "R channel mismatch"); + } + + // ── multi-pixel / stride ────────────────────────────────────────────────── + + #[test] + fn multi_pixel_row_each_pixel_converted_independently() { + let src = make_src( + 3, + 1, + 3, + vec![ + 0, 0, 0, // black + 128, 128, 128, // mid-grey + 255, 255, 255, // white + ], + ); + let mut dst = make_dst(3, 1, 3); + rgb_to_linear_u8(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[0], 0, "pixel 0 should be 0"); + assert_eq!( + d[3], + srgb_linearize_u8(128), + "pixel 1 should be linearised 128" + ); + assert_eq!(d[6], 255, "pixel 2 should be 255"); + } + + #[test] + fn multi_row_image_respects_stride() { + // 1×3 image: row 0 black, row 1 mid-grey, row 2 white + let src = make_src(1, 3, 3, vec![0, 0, 0, 128, 128, 128, 255, 255, 255]); + let mut dst = make_dst(1, 3, 3); + rgb_to_linear_u8(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[0], 0, "row 0 should be 0"); + assert_eq!( + d[3], + srgb_linearize_u8(128), + "row 1 should be linearised 128" + ); + assert_eq!(d[6], 255, "row 2 should be 255"); + } + + // ── each channel is converted independently ─────────────────────────────── + + #[test] + fn each_rgb_channel_is_converted_independently() { + let (r, g, b) = (200u8, 100u8, 50u8); + let src = make_src(1, 1, 3, vec![r, g, b]); + let mut dst = make_dst(1, 1, 3); + rgb_to_linear_u8(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[0], srgb_linearize_u8(r), "R channel"); + assert_eq!(d[1], srgb_linearize_u8(g), "G channel"); + assert_eq!(d[2], srgb_linearize_u8(b), "B channel"); + } + + #[test] + fn each_rgba_colour_channel_is_converted_independently() { + let (r, g, b, a) = (200u8, 100u8, 50u8, 180u8); + let src = make_src(1, 1, 4, vec![r, g, b, a]); + let mut dst = make_dst(1, 1, 4); + rgba_to_linear_u8(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[0], srgb_linearize_u8(r), "R channel"); + assert_eq!(d[1], srgb_linearize_u8(g), "G channel"); + assert_eq!(d[2], srgb_linearize_u8(b), "B channel"); + assert_eq!(d[3], a, "alpha passthrough"); + } } diff --git a/src/image_to_oklab.rs b/src/image_to_oklab.rs index 993918d..432aa24 100644 --- a/src/image_to_oklab.rs +++ b/src/image_to_oklab.rs @@ -13,13 +13,9 @@ use crate::oklch::Oklch; #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] use crate::sse::sse_image_to_oklab; use crate::{ - bgr_to_linear, bgra_to_linear, rgb_to_linear, rgba_to_linear, Oklab, Rgb, TransferFunction, + bgr_to_linear, bgra_to_linear, rgb_to_linear, rgba_to_linear, ColorError, ImageBuffer, + ImageBufferMut, Oklab, Rgb, TransferFunction, }; -#[cfg(feature = "rayon")] -use rayon::iter::ParallelIterator; -#[cfg(feature = "rayon")] -use rayon::prelude::ParallelSliceMut; -use std::slice; #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] pub(crate) enum OklabTarget { @@ -39,39 +35,30 @@ impl From for OklabTarget { } } -#[allow(clippy::type_complexity)] fn channels_to_oklab( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { + dst.validate()?; + src.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } let target: OklabTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); - - let callee = match image_configuration { + let linear_converter = match image_configuration { ImageConfiguration::Rgb => rgb_to_linear, ImageConfiguration::Rgba => rgba_to_linear, ImageConfiguration::Bgra => bgra_to_linear, ImageConfiguration::Bgr => bgr_to_linear, }; - callee( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); + linear_converter(src, dst, transfer_function)?; - let mut _wide_row_handle: Option usize> = None; + let mut _wide_row_handle: Option usize> = None; #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] { @@ -88,73 +75,71 @@ fn channels_to_oklab( _wide_row_handle = Some(avx_image_to_oklab::); } - let dst_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - dst.as_mut_ptr() as *mut u8, - dst_stride as usize * height as usize, - ) - }; + let dst_stride = dst.stride(); + let dst_full_width = dst.width + * if image_configuration.has_alpha() { + 4 + } else { + 3 + }; + let width = dst.width; + for dst_row in dst.data.borrow_mut().chunks_mut(dst_stride) { + let dst = &mut dst_row[..dst_full_width as usize]; - let iter; - #[cfg(feature = "rayon")] - { - iter = dst_slice_safe_align.par_chunks_exact_mut(dst_stride as usize); - } - - #[cfg(not(feature = "rayon"))] - { - iter = dst_slice_safe_align.chunks_exact_mut(dst_stride as usize); - } - - iter.for_each(|dst| unsafe { - let mut _cx = 0usize; - - let dst_ptr = dst.as_mut_ptr() as *mut f32; + let mut cx = 0usize; if let Some(dispatcher) = _wide_row_handle { - _cx = dispatcher(_cx, width, dst_ptr, 0) + cx = unsafe { dispatcher(cx, width, dst) }; } - for x in _cx..width as usize { - let px = x * channels; - - let src = dst_ptr.add(px); - let r = src - .add(image_configuration.get_r_channel_offset()) - .read_unaligned(); - let g = src - .add(image_configuration.get_g_channel_offset()) - .read_unaligned(); - let b = src - .add(image_configuration.get_b_channel_offset()) - .read_unaligned(); - - let rgb = Rgb::::new(r, g, b); - let dst_store = dst_ptr.add(px); - - match target { - OklabTarget::Oklab => { - let oklab = Oklab::from_linear_rgb(rgb); - dst_store.write_unaligned(oklab.l); - dst_store.add(1).write_unaligned(oklab.a); - dst_store.add(2).write_unaligned(oklab.b); - } - OklabTarget::Oklch => { - let oklch = Oklch::from_linear_rgb(rgb); - dst_store.write_unaligned(oklch.l); - dst_store.add(1).write_unaligned(oklch.c); - dst_store.add(2).write_unaligned(oklch.h); + let rem = &mut dst_row[cx..]; + if image_configuration.has_alpha() { + for dst in rem.as_chunks_mut::<4>().0.iter_mut() { + let rgb = Rgb::::new( + dst[image_configuration.r_index()], + dst[image_configuration.g_index()], + dst[image_configuration.b_index()], + ); + match target { + OklabTarget::Oklab => { + let oklab = Oklab::from_linear_rgb(rgb); + dst[0] = oklab.l; + dst[1] = oklab.a; + dst[2] = oklab.b; + } + OklabTarget::Oklch => { + let oklch = Oklch::from_linear_rgb(rgb); + dst[0] = oklch.l; + dst[1] = oklch.c; + dst[2] = oklch.c; + } } } - - if image_configuration.has_alpha() { - let a = src - .add(image_configuration.get_a_channel_offset()) - .read_unaligned(); - dst_store.add(3).write_unaligned(a); + } else { + for dst in rem.as_chunks_mut::<3>().0.iter_mut() { + let rgb = Rgb::::new( + dst[image_configuration.r_index()], + dst[image_configuration.g_index()], + dst[image_configuration.b_index()], + ); + match target { + OklabTarget::Oklab => { + let oklab = Oklab::from_linear_rgb(rgb); + dst[0] = oklab.l; + dst[1] = oklab.a; + dst[2] = oklab.b; + } + OklabTarget::Oklch => { + let oklch = Oklch::from_linear_rgb(rgb); + dst[0] = oklch.l; + dst[1] = oklch.c; + dst[2] = oklch.h; + } + } } } - }); + } + Ok(()) } /// This function converts RGB to Oklab against D65 white point. This is much more effective than naive direct transformation @@ -168,23 +153,15 @@ fn channels_to_oklab( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - transfer function to linear colorspace pub fn rgb_to_oklab( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_oklab::<{ ImageConfiguration::Rgb as u8 }, { OklabTarget::Oklab as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts RGBA to Oklab against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -198,23 +175,15 @@ pub fn rgb_to_oklab( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - transfer function to linear colorspace pub fn rgba_to_oklab( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_oklab::<{ ImageConfiguration::Rgba as u8 }, { OklabTarget::Oklab as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts BGRA to Oklab against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -228,23 +197,15 @@ pub fn rgba_to_oklab( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - transfer function to linear colorspace pub fn bgra_to_oklab( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_oklab::<{ ImageConfiguration::Bgra as u8 }, { OklabTarget::Oklab as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts BGR to Oklab against D65 white point. This is much more effective than naive direct transformation @@ -258,23 +219,15 @@ pub fn bgra_to_oklab( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - transfer function to linear colorspace pub fn bgr_to_oklab( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_oklab::<{ ImageConfiguration::Bgr as u8 }, { OklabTarget::Oklab as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts RGB to Oklch against D65 white point. This is much more effective than naive direct transformation @@ -288,23 +241,15 @@ pub fn bgr_to_oklab( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - transfer function to linear colorspace pub fn rgb_to_oklch( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_oklab::<{ ImageConfiguration::Rgb as u8 }, { OklabTarget::Oklch as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts RGBA to Oklch against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -318,23 +263,15 @@ pub fn rgb_to_oklch( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - transfer function to linear colorspace pub fn rgba_to_oklch( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_oklab::<{ ImageConfiguration::Rgba as u8 }, { OklabTarget::Oklch as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts BGRA to Oklch against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -348,23 +285,15 @@ pub fn rgba_to_oklch( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - transfer function to linear colorspace pub fn bgra_to_oklch( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_oklab::<{ ImageConfiguration::Bgra as u8 }, { OklabTarget::Oklch as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts BGR to Oklch against D65 white point. This is much more effective than naive direct transformation @@ -378,21 +307,315 @@ pub fn bgra_to_oklch( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - transfer function to linear colorspace pub fn bgr_to_oklch( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_oklab::<{ ImageConfiguration::Bgr as u8 }, { OklabTarget::Oklch as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::BufferStore; + + fn make_src(width: u32, height: u32, channels: u32, data: Vec) -> ImageBuffer<'static, u8> { + ImageBuffer { + width, + height, + channels, + data: data.into(), + stride: width * channels, + } + } + + fn make_dst(width: u32, height: u32, channels: u32) -> ImageBufferMut<'static, f32> { + let len = (width * height * channels) as usize; + ImageBufferMut { + width, + height, + channels, + data: BufferStore::Owned(vec![0f32; len]), + stride: width * channels, + } + } + + struct OklabRef { + rgb: [u8; 3], + l: f32, + a: f32, + b: f32, + } + + const OKLAB_REFS: &[OklabRef] = &[ + OklabRef { + rgb: [0, 0, 0], + l: 0.0, + a: 0.0, + b: 0.0, + }, // black + OklabRef { + rgb: [255, 255, 255], + l: 1.0, + a: 0.0, + b: 0.0, + }, // white + OklabRef { + rgb: [255, 0, 0], + l: 0.6279, + a: 0.2249, + b: 0.1257, + }, // red + OklabRef { + rgb: [0, 255, 0], + l: 0.8664, + a: -0.2338, + b: 0.1794, + }, // green + OklabRef { + rgb: [0, 0, 255], + l: 0.4520, + a: -0.0324, + b: -0.3118, + }, // blue + ]; + + // ── validation ──────────────────────────────────────────────────────────── + + #[test] + fn unsupported_channel_count_returns_error() { + // 2-channel images are not supported + let src = make_src(1, 1, 2, vec![128u8, 128]); + let mut dst = make_dst(1, 1, 2); + let result = channels_to_oklab::< + { ImageConfiguration::Rgb as u8 }, + { OklabTarget::Oklab as u8 }, + >(&src, &mut dst, TransferFunction::Srgb); + assert!(matches!( + result, + Err(ColorError::UnsupportedChannelsCount(2)) + )); + } + + #[test] + fn mismatched_dimensions_returns_error() { + let src = make_src(2, 2, 3, vec![0u8; 12]); + let mut dst = make_dst(4, 4, 3); + let result = rgb_to_oklab(&src, &mut dst, TransferFunction::Srgb); + assert!(result.is_err()); + } + + // ── black / white anchors ───────────────────────────────────────────────── + + #[test] + fn black_maps_to_oklab_zero() { + let src = make_src(1, 1, 3, vec![0, 0, 0]); + let mut dst = make_dst(1, 1, 3); + rgb_to_oklab(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert!(d[0].abs() < 0.005, "L of black should be ≈ 0, got {}", d[0]); + assert!(d[1].abs() < 0.005, "a of black should be ≈ 0, got {}", d[1]); + assert!(d[2].abs() < 0.005, "b of black should be ≈ 0, got {}", d[2]); + } + + #[test] + fn white_maps_to_oklab_l1_ab0() { + let src = make_src(1, 1, 3, vec![255, 255, 255]); + let mut dst = make_dst(1, 1, 3); + rgb_to_oklab(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert!( + (d[0] - 1.0).abs() < 0.005, + "L of white should be ≈ 1, got {}", + d[0] + ); + assert!(d[1].abs() < 0.005, "a of white should be ≈ 0, got {}", d[1]); + assert!(d[2].abs() < 0.005, "b of white should be ≈ 0, got {}", d[2]); + } + + // ── reference values for primary colours ───────────────────────────────── + + #[test] + fn primary_colours_match_oklab_reference_values() { + for r in OKLAB_REFS { + let src = make_src(1, 1, 3, r.rgb.to_vec()); + let mut dst = make_dst(1, 1, 3); + rgb_to_oklab(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let tol = 0.005; + assert!( + (d[0] - r.l).abs() < tol, + "rgb={:?}: L expected {}, got {}", + r.rgb, + r.l, + d[0] + ); + assert!( + (d[1] - r.a).abs() < tol, + "rgb={:?}: a expected {}, got {}", + r.rgb, + r.a, + d[1] + ); + assert!( + (d[2] - r.b).abs() < tol, + "rgb={:?}: b expected {}, got {}", + r.rgb, + r.b, + d[2] + ); + } + } + + #[test] + fn rgba_alpha_fully_opaque_is_one() { + let src = make_src(1, 1, 4, vec![128, 64, 32, 255]); + let mut dst = make_dst(1, 1, 4); + rgba_to_oklab(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let alpha = dst.data.borrow()[3]; + assert!((alpha - 1.0).abs() < 1e-4, "alpha 255 → 1.0, got {alpha}"); + } + + #[test] + fn rgba_alpha_fully_transparent_is_zero() { + let src = make_src(1, 1, 4, vec![128, 64, 32, 0]); + let mut dst = make_dst(1, 1, 4); + rgba_to_oklab(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let alpha = dst.data.borrow()[3]; + assert_eq!(alpha, 0.0, "alpha 0 → 0.0"); + } + + #[test] + fn rgba_alpha_midpoint_is_half() { + let src = make_src(1, 1, 4, vec![0, 0, 0, 128]); + let mut dst = make_dst(1, 1, 4); + rgba_to_oklab(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let alpha = dst.data.borrow()[3]; + let expected = 128.0 / 255.0; + assert!( + (alpha - expected).abs() < 1e-4, + "alpha 128 → {expected}, got {alpha}" + ); + } + + #[test] + fn rgba_lab_channels_are_unaffected_by_alpha() { + // The same RGB with different alpha values must produce identical L/a/b + let src_opaque = make_src(1, 1, 4, vec![200, 100, 50, 255]); + let src_transp = make_src(1, 1, 4, vec![200, 100, 50, 0]); + let mut dst_opaque = make_dst(1, 1, 4); + let mut dst_transp = make_dst(1, 1, 4); + rgba_to_oklab(&src_opaque, &mut dst_opaque, TransferFunction::Srgb).unwrap(); + rgba_to_oklab(&src_transp, &mut dst_transp, TransferFunction::Srgb).unwrap(); + let a = dst_opaque.data.borrow(); + let b = dst_transp.data.borrow(); + for i in 0..3 { + assert!( + (a[i] - b[i]).abs() < 1e-5, + "channel {i}: alpha should not affect L/a/b ({} vs {})", + a[i], + b[i] + ); + } + } + + // ── BGR / BGRA channel ordering ─────────────────────────────────────────── + + #[test] + fn bgr_and_rgb_produce_same_oklab_for_grey() { + // Grey is channel-order-invariant — a safe sanity check + let src_rgb = make_src(1, 1, 3, vec![128, 128, 128]); + let src_bgr = make_src(1, 1, 3, vec![128, 128, 128]); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + rgb_to_oklab(&src_rgb, &mut dst_rgb, TransferFunction::Srgb).unwrap(); + bgr_to_oklab(&src_bgr, &mut dst_bgr, TransferFunction::Srgb).unwrap(); + let r = dst_rgb.data.borrow(); + let b = dst_bgr.data.borrow(); + for i in 0..3 { + assert!( + (r[i] - b[i]).abs() < 1e-5, + "channel {i}: grey should give identical oklab for rgb vs bgr" + ); + } + } + + #[test] + fn bgr_and_rgb_differ_for_non_grey_input() { + let pixel = vec![200u8, 100, 50]; // R≠G≠B + let src_rgb = make_src(1, 1, 3, pixel.clone()); + let src_bgr = make_src(1, 1, 3, pixel); // same bytes, interpreted as B/G/R + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + rgb_to_oklab(&src_rgb, &mut dst_rgb, TransferFunction::Srgb).unwrap(); + bgr_to_oklab(&src_bgr, &mut dst_bgr, TransferFunction::Srgb).unwrap(); + let r = dst_rgb.data.borrow(); + let b = dst_bgr.data.borrow(); + assert!( + (r[0] - b[0]).abs() > 0.01 || (r[1] - b[1]).abs() > 0.01, + "rgb and bgr should differ for a non-grey pixel" + ); + } + + // ── multi-pixel / stride ────────────────────────────────────────────────── + + #[test] + fn multi_pixel_row_each_pixel_converted_independently() { + // Two pixels: black then white + let src = make_src(2, 1, 3, vec![0, 0, 0, 255, 255, 255]); + let mut dst = make_dst(2, 1, 3); + rgb_to_oklab(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert!(d[0].abs() < 0.005, "pixel 0 L should be ≈ 0"); + assert!((d[3] - 1.0).abs() < 0.005, "pixel 1 L should be ≈ 1"); + } + + #[test] + fn multi_row_image_respects_stride() { + // 1×2 image: row 0 = black, row 1 = white + let src = make_src(1, 2, 3, vec![0, 0, 0, 255, 255, 255]); + let mut dst = make_dst(1, 2, 3); + rgb_to_oklab(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert!(d[0].abs() < 0.005, "row 0 L should be ≈ 0"); + assert!((d[3] - 1.0).abs() < 0.005, "row 1 L should be ≈ 1"); + } + + #[test] + fn oklch_l_is_bounded_for_primary_colours() { + for r in OKLAB_REFS { + let src = make_src(1, 1, 3, r.rgb.to_vec()); + let mut dst = make_dst(1, 1, 3); + rgb_to_oklch(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let l = dst.data.borrow()[0]; + assert!( + l >= 0.0 && l <= 1.0, + "rgb={:?}: Oklch L={l} is outside [0,1]", + r.rgb + ); + } + } + + // ── transfer-function variant ───────────────────────────────────────────── + + #[test] + fn linear_and_srgb_transfer_functions_differ_for_mid_grey() { + let src_srgb = make_src(1, 1, 3, vec![128, 128, 128]); + let src_linear = make_src(1, 1, 3, vec![128, 128, 128]); + let mut dst_srgb = make_dst(1, 1, 3); + let mut dst_linear = make_dst(1, 1, 3); + rgb_to_oklab(&src_srgb, &mut dst_srgb, TransferFunction::Srgb).unwrap(); + rgb_to_oklab(&src_linear, &mut dst_linear, TransferFunction::Linear).unwrap(); + let ls = dst_srgb.data.borrow()[0]; + let ll = dst_linear.data.borrow()[0]; + assert!( + (ls - ll).abs() > 0.01, + "sRGB and Linear transfer functions should give different L for mid-grey \ + (sRGB L={ls}, linear L={ll})" + ); + } } diff --git a/src/image_to_sigmoidal.rs b/src/image_to_sigmoidal.rs index a0ce064..6a474c0 100644 --- a/src/image_to_sigmoidal.rs +++ b/src/image_to_sigmoidal.rs @@ -13,30 +13,25 @@ use crate::image::ImageConfiguration; use crate::neon::neon_image_to_sigmoidal; #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] use crate::sse::sse_image_to_sigmoidal_row; -use crate::Rgb; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -use std::slice; +use crate::{ColorError, ImageBuffer, ImageBufferMut, Rgb}; #[allow(clippy::type_complexity)] fn image_to_sigmoidal( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, -) { + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, +) -> Result<(), ColorError> { + src.validate()?; + dst.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); if USE_ALPHA && !image_configuration.has_alpha() { panic!("Alpha may be set only on images with alpha"); } - let channels = image_configuration.get_channels_count(); - - let mut _wide_row_handler: Option usize> = None; + let mut _wide_row_handler: Option usize> = None; #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] { @@ -55,70 +50,75 @@ fn image_to_sigmoidal( const COLOR_SCALE: f32 = 1f32 / 255f32; - let dst_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - dst.as_mut_ptr() as *mut u8, - dst_stride as usize * height as usize, - ) - }; + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; + let width = src.width; - let iter; - - #[cfg(feature = "rayon")] - { - iter = dst_slice_safe_align - .par_chunks_exact_mut(dst_stride as usize) - .zip(src.par_chunks_exact(src_stride as usize)); - } - #[cfg(not(feature = "rayon"))] + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) { - iter = dst_slice_safe_align - .chunks_exact_mut(dst_stride as usize) - .zip(src.chunks_exact(src_stride as usize)); - } - - iter.for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; - - let src_ptr = src.as_ptr(); - let dst_ptr = dst.as_mut_ptr() as *mut f32; + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; + let mut cx = 0usize; if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher(_cx, src_ptr, width, dst_ptr); + cx = unsafe { dispatcher(cx, src, dst, width) }; } - for x in _cx..width as usize { - let px = x * channels; - let src = src_ptr.add(px); - let r = src - .add(image_configuration.get_r_channel_offset()) - .read_unaligned(); - let g = src - .add(image_configuration.get_g_channel_offset()) - .read_unaligned(); - let b = src - .add(image_configuration.get_b_channel_offset()) - .read_unaligned(); - - let rgb = Rgb::::new(r, g, b); - - let writing_ptr = dst_ptr.add(px); - - let sigmoidal = rgb.to_sigmoidal(); - writing_ptr.write_unaligned(sigmoidal.sr); - writing_ptr.add(1).write_unaligned(sigmoidal.sg); - writing_ptr.add(2).write_unaligned(sigmoidal.sb); - - if image_configuration.has_alpha() { - let a = src - .add(image_configuration.get_a_channel_offset()) - .read_unaligned() as f32 - * COLOR_SCALE; - - writing_ptr.add(3).write_unaligned(a); + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + let dst = &mut dst[cx * 3..]; + let src = &src[cx * 3..]; + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + let rgb = Rgb::::new( + src[image_configuration.r_index()], + src[image_configuration.g_index()], + src[image_configuration.b_index()], + ); + + let sigmoidal = rgb.to_sigmoidal(); + + dst[0] = sigmoidal.sr; + dst[1] = sigmoidal.sg; + dst[2] = sigmoidal.sb; + } + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba + { + let dst = &mut dst[cx * 4..]; + let src = &src[cx * 4..]; + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + let rgb = Rgb::::new( + src[image_configuration.r_index()], + src[image_configuration.g_index()], + src[image_configuration.b_index()], + ); + + let sigmoidal = rgb.to_sigmoidal(); + dst[0] = sigmoidal.sr; + dst[1] = sigmoidal.sg; + dst[2] = sigmoidal.sb; + dst[3] = src[image_configuration.a_index()] as f32 * COLOR_SCALE; } } - }); + } + + Ok(()) } /// This function converts RGB to Sigmoidal. This is much more effective than naive direct transformation @@ -131,16 +131,10 @@ fn image_to_sigmoidal( /// * `dst` - A mutable slice to receive HSV data /// * `dst_stride` - Bytes per row for dst data pub fn rgb_to_sigmoidal( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, -) { - image_to_sigmoidal::<{ ImageConfiguration::Rgb as u8 }, false>( - src, src_stride, dst, dst_stride, width, height, - ); + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, +) -> Result<(), ColorError> { + image_to_sigmoidal::<{ ImageConfiguration::Rgb as u8 }, false>(src, dst) } /// This function converts BGRA to Sigmoidal. Alpha channel will be normalized. This is much more effective than naive direct transformation @@ -153,16 +147,10 @@ pub fn rgb_to_sigmoidal( /// * `dst` - A mutable slice to receive Sigmodal data /// * `dst_stride` - Bytes per row for dst data pub fn bgra_to_sigmoidal( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, -) { - image_to_sigmoidal::<{ ImageConfiguration::Bgra as u8 }, true>( - src, src_stride, dst, dst_stride, width, height, - ); + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, +) -> Result<(), ColorError> { + image_to_sigmoidal::<{ ImageConfiguration::Bgra as u8 }, true>(src, dst) } /// This function converts RGBA to Sigmoidal. Alpha channel will be normalized. This is much more effective than naive direct transformation @@ -175,14 +163,241 @@ pub fn bgra_to_sigmoidal( /// * `dst` - A mutable slice to receive Sigmoidal data /// * `dst_stride` - Bytes per row for dst data pub fn rgba_to_sigmoidal( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, -) { - image_to_sigmoidal::<{ ImageConfiguration::Rgba as u8 }, true>( - src, src_stride, dst, dst_stride, width, height, - ); + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, +) -> Result<(), ColorError> { + image_to_sigmoidal::<{ ImageConfiguration::Rgba as u8 }, true>(src, dst) +} + +#[cfg(test)] +mod tests { + use crate::BufferStore; + use super::*; + + // ── helpers ─────────────────────────────────────────────────────────────── + + fn make_src(data: Vec, width: u32, channels: u32) -> ImageBuffer<'static, u8> { + ImageBuffer { + data: std::borrow::Cow::Owned(data), + width, + height: 1, + channels, + stride: width * channels, + } + } + + fn make_dst(width: u32, channels: u32) -> ImageBufferMut<'static, f32> { + ImageBufferMut { + data: BufferStore::Owned(vec![0f32; (width * channels) as usize]), + width, + height: 1, + channels, + stride: width * channels, + } + } + + // ── rgb_to_sigmoidal ────────────────────────────────────────────────────── + + #[test] + fn test_rgb_to_sigmoidal_output_in_range() { + // All output values must be in [0.0, 1.0] + let src = make_src(vec![128, 64, 200], 1, 3); + let mut dst = make_dst(1, 3); + rgb_to_sigmoidal(&src, &mut dst).unwrap(); + for &v in dst.data.borrow() { + assert!(v >= 0.0 && v <= 1.0, "Value out of range: {v}"); + } + } + + #[test] + fn test_rgb_to_sigmoidal_monotonic() { + // Brighter input => higher sigmoidal output (monotonically increasing) + let src_dark = make_src(vec![50, 50, 50], 1, 3); + let src_mid = make_src(vec![128, 128, 128], 1, 3); + let src_bright = make_src(vec![200, 200, 200], 1, 3); + let mut dst_dark = make_dst(1, 3); + let mut dst_mid = make_dst(1, 3); + let mut dst_bright = make_dst(1, 3); + rgb_to_sigmoidal(&src_dark, &mut dst_dark).unwrap(); + rgb_to_sigmoidal(&src_mid, &mut dst_mid).unwrap(); + rgb_to_sigmoidal(&src_bright, &mut dst_bright).unwrap(); + assert!(dst_dark.data[0] < dst_mid.data[0], "dark < mid"); + assert!(dst_mid.data[0] < dst_bright.data[0], "mid < bright"); + } + + #[test] + fn test_rgb_to_sigmoidal_midpoint_near_half() { + // Sigmoid midpoint: input 128 should map to ~0.5 + let src = make_src(vec![128, 128, 128], 1, 3); + let mut dst = make_dst(1, 3); + rgb_to_sigmoidal(&src, &mut dst).unwrap(); + for &v in dst.data.borrow() { + assert!( + (v - 0.5).abs() < 0.15, + "Mid input ~128 should map near 0.5, got {v}" + ); + } + } + + #[test] + fn test_rgb_to_sigmoidal_invalid_channels() { + let src = make_src(vec![1, 2], 1, 2); + let mut dst = make_dst(1, 2); + let result = rgb_to_sigmoidal(&src, &mut dst); + assert!(result.is_err(), "Should fail with 2 channels"); + } + + // ── rgba_to_sigmoidal ───────────────────────────────────────────────────── + + #[test] + fn test_rgba_to_sigmoidal_alpha_normalized_full() { + // Alpha 255 => 1.0 + let src = make_src(vec![128, 64, 32, 255], 1, 4); + let mut dst = make_dst(1, 4); + rgba_to_sigmoidal(&src, &mut dst).unwrap(); + assert!( + (dst.data[3] - 1.0).abs() < 0.01, + "Alpha 255 => 1.0, got {}", + dst.data[3] + ); + } + + #[test] + fn test_rgba_to_sigmoidal_alpha_normalized_zero() { + // Alpha 0 => 0.0 + let src = make_src(vec![128, 64, 32, 0], 1, 4); + let mut dst = make_dst(1, 4); + rgba_to_sigmoidal(&src, &mut dst).unwrap(); + assert!( + dst.data[3].abs() < 0.01, + "Alpha 0 => 0.0, got {}", + dst.data[3] + ); + } + + #[test] + fn test_rgba_to_sigmoidal_alpha_normalized_half() { + // Alpha 128 => ~0.502 + let src = make_src(vec![0, 0, 0, 128], 1, 4); + let mut dst = make_dst(1, 4); + rgba_to_sigmoidal(&src, &mut dst).unwrap(); + let expected = 128.0f32 / 255.0; + assert!( + (dst.data[3] - expected).abs() < 0.01, + "Alpha 128 => ~{expected:.3}, got {}", + dst.data[3] + ); + } + + #[test] + fn test_rgba_to_sigmoidal_rgb_channels_unaffected_by_alpha() { + // RGB sigmoidal values must be the same regardless of alpha + let src_a255 = make_src(vec![200, 100, 50, 255], 1, 4); + let src_a0 = make_src(vec![200, 100, 50, 0], 1, 4); + let mut dst_a255 = make_dst(1, 4); + let mut dst_a0 = make_dst(1, 4); + rgba_to_sigmoidal(&src_a255, &mut dst_a255).unwrap(); + rgba_to_sigmoidal(&src_a0, &mut dst_a0).unwrap(); + for i in 0..3 { + assert!( + (dst_a255.data[i] - dst_a0.data[i]).abs() < 1e-6, + "Channel {i} should not depend on alpha" + ); + } + } + + #[test] + fn test_rgba_to_sigmoidal_output_in_range() { + let src = make_src(vec![10, 200, 75, 180], 1, 4); + let mut dst = make_dst(1, 4); + rgba_to_sigmoidal(&src, &mut dst).unwrap(); + for &v in dst.data.borrow() { + assert!(v >= 0.0 && v <= 1.0, "Value out of range: {v}"); + } + } + + #[test] + fn test_bgra_to_sigmoidal_channel_swap() { + // Pure red in RGBA = (255,0,0,255); pure red in BGRA = (0,0,255,255) + // Both represent the same color — sr should match + let src_rgba = make_src(vec![255, 0, 0, 255], 1, 4); + let src_bgra = make_src(vec![0, 0, 255, 255], 1, 4); + let mut dst_rgba = make_dst(1, 4); + let mut dst_bgra = make_dst(1, 4); + rgba_to_sigmoidal(&src_rgba, &mut dst_rgba).unwrap(); + bgra_to_sigmoidal(&src_bgra, &mut dst_bgra).unwrap(); + assert!( + (dst_rgba.data[0] - dst_bgra.data[0]).abs() < 1e-5, + "sr mismatch: rgba={} bgra={}", + dst_rgba.data[0], + dst_bgra.data[0] + ); + assert!( + (dst_rgba.data[1] - dst_bgra.data[1]).abs() < 1e-5, + "sg mismatch" + ); + assert!( + (dst_rgba.data[2] - dst_bgra.data[2]).abs() < 1e-5, + "sb mismatch" + ); + } + + #[test] + fn test_bgra_to_sigmoidal_alpha_normalized() { + let src = make_src(vec![0, 0, 0, 200], 1, 4); + let mut dst = make_dst(1, 4); + bgra_to_sigmoidal(&src, &mut dst).unwrap(); + let expected = 200.0f32 / 255.0; + assert!( + (dst.data[3] - expected).abs() < 0.01, + "Alpha ~{expected:.3}, got {}", + dst.data[3] + ); + } + + #[test] + fn test_bgra_to_sigmoidal_neutral_matches_rgba() { + // For neutral grey BGR==RGB so both should give identical results + let src_rgba = make_src(vec![128, 128, 128, 128], 1, 4); + let src_bgra = make_src(vec![128, 128, 128, 128], 1, 4); + let mut dst_rgba = make_dst(1, 4); + let mut dst_bgra = make_dst(1, 4); + rgba_to_sigmoidal(&src_rgba, &mut dst_rgba).unwrap(); + bgra_to_sigmoidal(&src_bgra, &mut dst_bgra).unwrap(); + for i in 0..4 { + assert!( + (dst_rgba.data[i] - dst_bgra.data[i]).abs() < 1e-5, + "Channel {i} mismatch for neutral grey" + ); + } + } + + #[test] + fn test_bgra_to_sigmoidal_output_in_range() { + let src = make_src(vec![33, 177, 210, 99], 1, 4); + let mut dst = make_dst(1, 4); + bgra_to_sigmoidal(&src, &mut dst).unwrap(); + for &v in dst.data.borrow() { + assert!(v >= 0.0 && v <= 1.0, "Value out of range: {v}"); + } + } + + // ── rgb vs rgba consistency ─────────────────────────────────────────────── + + #[test] + fn test_rgb_and_rgba_same_rgb_channels() { + // RGB channels must be identical whether or not alpha is present + let src_rgb = make_src(vec![100, 150, 200], 1, 3); + let src_rgba = make_src(vec![100, 150, 200, 255], 1, 4); + let mut dst_rgb = make_dst(1, 3); + let mut dst_rgba = make_dst(1, 4); + rgb_to_sigmoidal(&src_rgb, &mut dst_rgb).unwrap(); + rgba_to_sigmoidal(&src_rgba, &mut dst_rgba).unwrap(); + for i in 0..3 { + assert!( + (dst_rgb.data[i] - dst_rgba.data[i]).abs() < 1e-5, + "Channel {i} differs between RGB and RGBA" + ); + } + } } diff --git a/src/image_to_xyz_lab.rs b/src/image_to_xyz_lab.rs index 44f87d0..bc818de 100644 --- a/src/image_to_xyz_lab.rs +++ b/src/image_to_xyz_lab.rs @@ -73,7 +73,7 @@ fn channels_to_xyz); } - let mut lut_table = vec![0f32; 256]; + let mut lut_table = [0f32; 256]; for (i, lut) in lut_table.iter_mut().enumerate() { *lut = transfer_function.linearize(i as f32 * (1. / 255.0)); } @@ -108,20 +108,20 @@ fn channels_to_xyz::new(r, g, b); let ptr = dst_ptr.add(x * 3); @@ -181,7 +181,7 @@ fn channels_to_xyz::new(r, g, b); let ptr = dst_ptr.add(x * 3); diff --git a/src/image_xyza_laba.rs b/src/image_xyza_laba.rs index 524efe8..c6fcb46 100644 --- a/src/image_xyza_laba.rs +++ b/src/image_xyza_laba.rs @@ -89,14 +89,14 @@ fn channels_to_xyz_with_alpha::new(r, g, b); let px = x * channels; @@ -153,7 +153,7 @@ fn channels_to_xyz_with_alpha( _wide_row_handle = Some(neon_jzazbz_to_image::); } - let mut lut_table = vec![0u8; 2049]; + let mut lut_table = [0u8; 2049]; for (i, lut) in lut_table.iter_mut().enumerate() { - *lut = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.).min(255.) as u8; + *lut = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.) + .round() + .min(255.) as u8; } let src_slice_safe_align = unsafe { @@ -110,12 +112,12 @@ fn jzazbz_to_image( }; let dst = transient_row.get_unchecked_mut((x * channels)..); - *dst.get_unchecked_mut(image_configuration.get_r_channel_offset()) = rgb.r; - *dst.get_unchecked_mut(image_configuration.get_g_channel_offset()) = rgb.g; - *dst.get_unchecked_mut(image_configuration.get_b_channel_offset()) = rgb.b; + *dst.get_unchecked_mut(image_configuration.r_index()) = rgb.r; + *dst.get_unchecked_mut(image_configuration.g_index()) = rgb.g; + *dst.get_unchecked_mut(image_configuration.b_index()) = rgb.b; if image_configuration.has_alpha() { let l_a = src_ptr.add(px + 3).read_unaligned(); - *dst.get_unchecked_mut(image_configuration.get_a_channel_offset()) = l_a; + *dst.get_unchecked_mut(image_configuration.a_index()) = l_a; } } @@ -123,34 +125,25 @@ fn jzazbz_to_image( .chunks_exact_mut(channels) .zip(transient_row.chunks_exact(channels)) { - let r_cast = (src_chunks[image_configuration.get_r_channel_offset()] - .min(1.) - .max(0.) - * 2048f32) - .round(); - let g_cast = (src_chunks[image_configuration.get_g_channel_offset()] - .min(1.) - .max(0.) - * 2048f32) - .round(); - let b_cast = (src_chunks[image_configuration.get_b_channel_offset()] - .min(1.) - .max(0.) - * 2048f32) - .round(); + let r_cast = + (src_chunks[image_configuration.r_index()].min(1.).max(0.) * 2048f32).round(); + let g_cast = + (src_chunks[image_configuration.g_index()].min(1.).max(0.) * 2048f32).round(); + let b_cast = + (src_chunks[image_configuration.b_index()].min(1.).max(0.) * 2048f32).round(); - dst_chunk[image_configuration.get_r_channel_offset()] = + dst_chunk[image_configuration.r_index()] = *lut_table.get_unchecked((r_cast as usize).min(2048)); - dst_chunk[image_configuration.get_g_channel_offset()] = + dst_chunk[image_configuration.g_index()] = *lut_table.get_unchecked((g_cast as usize).min(2048)); - dst_chunk[image_configuration.get_b_channel_offset()] = + dst_chunk[image_configuration.b_index()] = *lut_table.get_unchecked((b_cast as usize).min(2048)); if image_configuration.has_alpha() { - let a_cast = (src_chunks[image_configuration.get_a_channel_offset()] * 255.) + let a_cast = (src_chunks[image_configuration.a_index()] * 255.) .min(255.) .max(0.) as u8; - dst_chunk[image_configuration.get_a_channel_offset()] = a_cast; + dst_chunk[image_configuration.a_index()] = a_cast; } } }); diff --git a/src/lalphabeta_to_image.rs b/src/lalphabeta_to_image.rs index cd71468..f0a31bc 100644 --- a/src/lalphabeta_to_image.rs +++ b/src/lalphabeta_to_image.rs @@ -67,13 +67,13 @@ fn lalphabeta_to_image( let rgb = lalphabeta.to_linear_rgb(&XYZ_TO_SRGB_D65); let dst = transient_row.get_unchecked_mut((x * channels)..); - *dst.get_unchecked_mut(image_configuration.get_r_channel_offset()) = rgb.r; - *dst.get_unchecked_mut(image_configuration.get_g_channel_offset()) = rgb.g; - *dst.get_unchecked_mut(image_configuration.get_b_channel_offset()) = rgb.b; + *dst.get_unchecked_mut(image_configuration.r_index()) = rgb.r; + *dst.get_unchecked_mut(image_configuration.g_index()) = rgb.g; + *dst.get_unchecked_mut(image_configuration.b_index()) = rgb.b; if image_configuration.has_alpha() { let l_a = src_ptr.add(px + 3).read_unaligned(); let a_value = (l_a * 255f32).max(0f32).round(); - *dst.get_unchecked_mut(image_configuration.get_a_channel_offset()) = a_value; + *dst.get_unchecked_mut(image_configuration.a_index()) = a_value; } } @@ -81,9 +81,9 @@ fn lalphabeta_to_image( .chunks_exact_mut(channels) .zip(transient_row.chunks_exact(channels)) { - let r = src[image_configuration.get_r_channel_offset()]; - let g = src[image_configuration.get_g_channel_offset()]; - let b = src[image_configuration.get_b_channel_offset()]; + let r = src[image_configuration.r_index()]; + let g = src[image_configuration.g_index()]; + let b = src[image_configuration.b_index()]; let rgb = (Rgb::::new( r.min(1f32).max(0f32), @@ -93,15 +93,15 @@ fn lalphabeta_to_image( .round() .cast::(); - *dst.get_unchecked_mut(image_configuration.get_r_channel_offset()) = + *dst.get_unchecked_mut(image_configuration.r_index()) = *lut_table.get_unchecked(rgb.r.min(2048) as usize); - *dst.get_unchecked_mut(image_configuration.get_g_channel_offset()) = + *dst.get_unchecked_mut(image_configuration.g_index()) = *lut_table.get_unchecked(rgb.g.min(2048) as usize); - *dst.get_unchecked_mut(image_configuration.get_b_channel_offset()) = + *dst.get_unchecked_mut(image_configuration.b_index()) = *lut_table.get_unchecked(rgb.b.min(2048) as usize); if image_configuration.has_alpha() { - *dst.get_unchecked_mut(image_configuration.get_a_channel_offset()) = - *src.get_unchecked(image_configuration.get_a_channel_offset()) as u8; + *dst.get_unchecked_mut(image_configuration.a_index()) = + *src.get_unchecked(image_configuration.a_index()) as u8; } } }); diff --git a/src/lib.rs b/src/lib.rs index 3f7966d..d797ad4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,9 @@ )] #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] mod avx; +mod buffer; mod concat_alpha; +mod err; mod euclidean; mod gamma_curves; mod hsl; @@ -131,6 +133,8 @@ pub use xyza_laba_to_image::luv_with_alpha_to_rgba; pub use xyza_laba_to_image::xyz_with_alpha_to_bgra; pub use xyza_laba_to_image::xyz_with_alpha_to_rgba; +pub use buffer::{BufferStore, ImageBuffer, ImageBufferMut}; +pub use err::ColorError; pub use euclidean::EuclideanDistance; pub use image_to_jzazbz::bgr_to_jzazbz; pub use image_to_jzazbz::bgr_to_jzczhz; diff --git a/src/linear_to_image.rs b/src/linear_to_image.rs index af538bb..15834c0 100644 --- a/src/linear_to_image.rs +++ b/src/linear_to_image.rs @@ -66,13 +66,13 @@ fn linear_to_gamma_channels::new( @@ -85,19 +85,19 @@ fn linear_to_gamma_channels::new(r, g, b); let dst = dst.get_unchecked_mut(px..); - *dst.get_unchecked_mut(image_configuration.get_r_channel_offset()) = + *dst.get_unchecked_mut(image_configuration.r_index()) = *lut_table.get_unchecked(rgb.r as usize); - *dst.get_unchecked_mut(image_configuration.get_g_channel_offset()) = + *dst.get_unchecked_mut(image_configuration.g_index()) = *lut_table.get_unchecked(rgb.g as usize); - *dst.get_unchecked_mut(image_configuration.get_b_channel_offset()) = + *dst.get_unchecked_mut(image_configuration.b_index()) = *lut_table.get_unchecked(rgb.b as usize); if USE_ALPHA && image_configuration.has_alpha() { - let a = src.get_unchecked(px + image_configuration.get_a_channel_offset()); - *dst.get_unchecked_mut(image_configuration.get_a_channel_offset()) = *a; + let a = src.get_unchecked(px + image_configuration.a_index()); + *dst.get_unchecked_mut(image_configuration.a_index()) = *a; } } }); diff --git a/src/neon/image_to_jzazbz.rs b/src/neon/image_to_jzazbz.rs index 42a4623..69bb816 100644 --- a/src/neon/image_to_jzazbz.rs +++ b/src/neon/image_to_jzazbz.rs @@ -98,13 +98,11 @@ macro_rules! triple_to_jzazbz { } #[inline(always)] -pub unsafe fn neon_image_to_jzazbz( +pub(crate) unsafe fn neon_image_to_jzazbz( start_cx: usize, - src: *const f32, - src_offset: usize, + src: &[f32], + dst: &mut [f32], width: u32, - dst: *mut f32, - dst_offset: usize, display_luminance: f32, ) -> usize { let target: JzazbzTarget = TARGET.into(); @@ -112,10 +110,9 @@ pub unsafe fn neon_image_to_jzazbz( start_cx: usize, width: u32, - dst: *mut f32, - dst_offset: usize, + dst: &mut [f32], ) -> usize { let target: OklabTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); let channels = image_configuration.get_channels_count(); let mut cx = start_cx; - let dst_ptr = (dst as *mut u8).add(dst_offset) as *mut f32; - let (c0, c1, c2, c3, c4, c5, c6, c7, c8) = ( vdupq_n_f32(0.4122214708f32), vdupq_n_f32(0.5363325363f32), @@ -75,10 +72,10 @@ pub unsafe fn neon_image_to_oklab( +pub(crate) unsafe fn neon_oklab_to_image( start_cx: usize, - src: *const f32, - src_offset: usize, - dst: *mut f32, - dst_offset: u32, + src: &[f32], + dst: &mut [f32], width: u32, ) -> usize { let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); @@ -94,17 +92,15 @@ pub unsafe fn neon_oklab_to_image( - v_src_ptr, m0, m1, m2, m3, m4, m5, m6, m7, m8, c0, c1, c2, c3, c4, c5, c6, c7, c8, + src_ptr, m0, m1, m2, m3, m4, m5, m6, m7, m8, c0, c1, c2, c3, c4, c5, c6, c7, c8, ); - let in_place_ptr = - ((dst as *mut u8).add(dst_offset as usize) as *mut f32).add(cx * channels); - if image_configuration.has_alpha() { let store_rows = match image_configuration { ImageConfiguration::Rgb | ImageConfiguration::Rgba => { @@ -114,7 +110,7 @@ pub unsafe fn neon_oklab_to_image { @@ -124,7 +120,7 @@ pub unsafe fn neon_oklab_to_image float32x4_t { +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_color_to_sigmoidal(x: float32x4_t) -> float32x4_t { let x = vmulq_n_f32(x, 1f32 / 255f32); let negg = vnegq_f32(x); - let den = vaddq_f32(vdupq_n_f32(1f32), vexpq_f32(negg)); + let den = vaddq_f32(vdupq_n_f32(1f32), unsafe { vexpq_f32(negg) }); let erase_nan_mask = vceqzq_f32(den); let rcp = vrecpeq_f32(den); vbslq_f32(erase_nan_mask, vdupq_n_f32(0f32), rcp) } -#[inline(always)] -pub(crate) unsafe fn neon_sigmoidal_to_color(x: float32x4_t) -> float32x4_t { +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_sigmoidal_to_color(x: float32x4_t) -> float32x4_t { let den = vsubq_f32(vdupq_n_f32(1f32), x); let zero_mask_1 = vceqzq_f32(den); let k = vmulq_f32(x, vrecpeq_f32(den)); let zeros = vdupq_n_f32(0f32); let zero_mask_2 = vcleq_f32(k, zeros); - let ln = vlnq_fast_f32(k); + let ln = unsafe { vlnq_fast_f32(k) }; vbslq_f32(vandq_u32(zero_mask_1, zero_mask_2), zeros, ln) } -#[inline(always)] -pub(crate) unsafe fn neon_rgb_to_sigmoidal( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_rgb_to_sigmoidal( r: uint32x4_t, g: uint32x4_t, b: uint32x4_t, @@ -41,8 +44,9 @@ pub(crate) unsafe fn neon_rgb_to_sigmoidal( (sr, sg, sb) } -#[inline(always)] -pub(crate) unsafe fn neon_sigmoidal_to_rgb( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_sigmoidal_to_rgb( sr: float32x4_t, sg: float32x4_t, sb: float32x4_t, diff --git a/src/neon/to_sigmoidal.rs b/src/neon/to_sigmoidal.rs index 762dd45..28c1c00 100644 --- a/src/neon/to_sigmoidal.rs +++ b/src/neon/to_sigmoidal.rs @@ -12,12 +12,12 @@ use crate::{ }; use std::arch::aarch64::*; -#[inline(always)] -pub unsafe fn neon_image_to_sigmoidal( +#[target_feature(enable = "neon")] +pub(crate) fn neon_image_to_sigmoidal( start_cx: usize, - src: *const u8, + src: &[u8], + dst: &mut [f32], width: u32, - dst: *mut f32, ) -> usize { let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); let mut cx = start_cx; @@ -27,12 +27,10 @@ pub unsafe fn neon_image_to_sigmoidal { @@ -67,7 +70,12 @@ pub unsafe fn neon_image_to_sigmoidal { @@ -96,7 +110,13 @@ pub unsafe fn neon_image_to_sigmoidal { @@ -135,7 +161,13 @@ pub unsafe fn neon_image_to_sigmoidal { @@ -165,16 +203,22 @@ pub unsafe fn neon_image_to_sigmoidal { @@ -209,7 +258,12 @@ pub unsafe fn neon_image_to_sigmoidal { @@ -238,16 +298,22 @@ pub unsafe fn neon_image_to_sigmoidal { @@ -282,7 +353,12 @@ pub unsafe fn neon_image_to_sigmoidal( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { + dst.validate()?; + src.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } let target: OklabTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let mut _wide_row_handle: Option< - unsafe fn(usize, *const f32, usize, *mut f32, u32, u32) -> usize, - > = None; + let mut _wide_row_handle: Option usize> = None; #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] if std::arch::is_x86_feature_detected!("sse4.1") { @@ -52,103 +45,130 @@ fn oklab_to_image( _wide_row_handle = Some(neon_oklab_to_image::); } - let mut lut_table = vec![0u8; 2049]; - for (i, lut) in lut_table.iter_mut().enumerate() { - *lut = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.).min(255.) as u8; + let mut lut_table = [0u8; 65535]; + for (i, lut) in lut_table[..2049].iter_mut().enumerate() { + *lut = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.) + .round() + .min(255.) as u8; } let channels = image_configuration.get_channels_count(); - let src_slice_safe_align = unsafe { - slice::from_raw_parts( - src.as_ptr() as *const u8, - src_stride as usize * height as usize, - ) - }; + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; + let width = src.width; - let iter; - #[cfg(feature = "rayon")] - { - iter = dst - .par_chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.par_chunks_exact(src_stride as usize)); - } + let mut transient_row = vec![0f32; src.width as usize * channels]; - #[cfg(not(feature = "rayon"))] + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) { - iter = dst - .chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.chunks_exact(src_stride as usize)); - } - - iter.for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; - - let mut transient_row = vec![0f32; width as usize * channels]; - - let src_ptr = src.as_ptr() as *mut f32; + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; + let mut cx = 0usize; if let Some(dispatcher) = _wide_row_handle { - _cx = dispatcher(_cx, src_ptr, 0, transient_row.as_mut_ptr(), 0, width) + cx = unsafe { dispatcher(cx, src, &mut transient_row, width) }; } - for x in _cx..width as usize { - let px = x * channels; - let source_p = src_ptr.add(px); - let l_x = source_p.read_unaligned(); - let l_y = source_p.add(1).read_unaligned(); - let l_z = source_p.add(2).read_unaligned(); - let rgb = match target { - OklabTarget::Oklab => { - let oklab = Oklab::new(l_x, l_y, l_z); - oklab.to_linear_rgb() - } - OklabTarget::Oklch => { - let oklch = Oklch::new(l_x, l_y, l_z); - oklch.to_linear_rgb() - } - }; - - let v_dst = transient_row.get_unchecked_mut((x * channels)..); - *v_dst.get_unchecked_mut(image_configuration.get_r_channel_offset()) = rgb.r; - *v_dst.get_unchecked_mut(image_configuration.get_g_channel_offset()) = rgb.g; - *v_dst.get_unchecked_mut(image_configuration.get_b_channel_offset()) = rgb.b; - if image_configuration.has_alpha() { - let l_a = source_p.add(3).read_unaligned(); - *v_dst.get_unchecked_mut(image_configuration.get_a_channel_offset()) = l_a; + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + let transient_row = &mut transient_row[cx * 3..]; + let src = &src[cx * 3usize..]; + for (dst, src) in transient_row + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + let rgb = match target { + OklabTarget::Oklab => { + let oklab = Oklab::new(src[0], src[1], src[2]); + oklab.to_linear_rgb() + } + OklabTarget::Oklch => { + let oklch = Oklch::new(src[0], src[1], src[2]); + oklch.to_linear_rgb() + } + }; + dst[0] = rgb.r; + dst[1] = rgb.g; + dst[2] = rgb.b; } - } - for (dst_chunks, src_chunks) in dst - .chunks_exact_mut(channels) - .zip(transient_row.chunks_exact_mut(channels)) + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(transient_row.as_chunks::<3>().0.iter()) + { + let rgb = (Rgb::::new( + src[0].max(0.).min(1.), + src[1].max(0.).min(1.), + src[2].max(0.).min(1.), + ) * Rgb::::dup(2048f32)) + .round() + .cast::(); + + dst[image_configuration.r_index()] = lut_table[rgb.r.min(2048) as usize]; + dst[image_configuration.g_index()] = lut_table[rgb.g.min(2048) as usize]; + dst[image_configuration.b_index()] = lut_table[rgb.b.min(2048) as usize]; + } + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba { - let rgb = (Rgb::::new( - src_chunks[image_configuration.get_r_channel_offset()] - .max(0.) - .min(1.), - src_chunks[image_configuration.get_g_channel_offset()] - .max(0.) - .min(1.), - src_chunks[image_configuration.get_b_channel_offset()] - .max(0.) - .min(1.), - ) * Rgb::::dup(2048f32)) - .round() - .cast::(); - - dst_chunks[image_configuration.get_r_channel_offset()] = - *lut_table.get_unchecked((rgb.r as usize).min(2048)); - dst_chunks[image_configuration.get_g_channel_offset()] = - *lut_table.get_unchecked((rgb.g as usize).min(2048)); - dst_chunks[image_configuration.get_b_channel_offset()] = - *lut_table.get_unchecked((rgb.b as usize).min(2048)); - if image_configuration.has_alpha() { - let a_lin = (src_chunks[3] * 255f32).round() as u8; - dst_chunks[image_configuration.get_a_channel_offset()] = a_lin; + let transient_row = &mut transient_row[cx * 4..]; + let src = &src[cx * 4usize..]; + for (dst, src) in transient_row + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + let rgb = match target { + OklabTarget::Oklab => { + let oklab = Oklab::new(src[0], src[1], src[2]); + oklab.to_linear_rgb() + } + OklabTarget::Oklch => { + let oklch = Oklch::new(src[0], src[1], src[2]); + oklch.to_linear_rgb() + } + }; + dst[0] = rgb.r; + dst[1] = rgb.g; + dst[2] = rgb.b; + dst[3] = src[3]; + } + + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(transient_row.as_chunks::<4>().0.iter()) + { + let rgb = (Rgb::::new( + src[0].max(0.).min(1.), + src[1].max(0.).min(1.), + src[2].max(0.).min(1.), + ) * Rgb::::dup(2048f32)) + .round() + .cast::(); + + dst[image_configuration.r_index()] = lut_table[rgb.r.min(2048) as usize]; + dst[image_configuration.g_index()] = lut_table[rgb.g.min(2048) as usize]; + dst[image_configuration.b_index()] = lut_table[rgb.b.min(2048) as usize]; + dst[image_configuration.a_index()] = (src[3] * 255.).round() as u8; } } - }); + } + + Ok(()) } /// This function converts Oklab with interleaved alpha channel to RGBA. This is much more effective than naive direct transformation @@ -162,23 +182,15 @@ fn oklab_to_image( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn oklab_to_rgba( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { oklab_to_image::<{ ImageConfiguration::Rgba as u8 }, { OklabTarget::Oklab as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts Oklab to RGB. This is much more effective than naive direct transformation @@ -192,23 +204,15 @@ pub fn oklab_to_rgba( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn oklab_to_rgb( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { oklab_to_image::<{ ImageConfiguration::Rgb as u8 }, { OklabTarget::Oklab as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts Oklab to BGR. This is much more effective than naive direct transformation @@ -222,23 +226,15 @@ pub fn oklab_to_rgb( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn oklab_to_bgr( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { oklab_to_image::<{ ImageConfiguration::Bgr as u8 }, { OklabTarget::Oklab as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts Oklab with interleaved alpha channel to BGRA. This is much more effective than naive direct transformation @@ -252,23 +248,15 @@ pub fn oklab_to_bgr( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn oklab_to_bgra( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { oklab_to_image::<{ ImageConfiguration::Bgra as u8 }, { OklabTarget::Oklab as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts *Oklch* with interleaved alpha channel to RGBA. This is much more effective than naive direct transformation @@ -282,23 +270,15 @@ pub fn oklab_to_bgra( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn oklch_to_rgba( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { oklab_to_image::<{ ImageConfiguration::Rgba as u8 }, { OklabTarget::Oklch as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts *Oklch* to RGB. This is much more effective than naive direct transformation @@ -312,23 +292,15 @@ pub fn oklch_to_rgba( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn oklch_to_rgb( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { oklab_to_image::<{ ImageConfiguration::Rgb as u8 }, { OklabTarget::Oklch as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts *Oklch* to BGR. This is much more effective than naive direct transformation @@ -342,23 +314,15 @@ pub fn oklch_to_rgb( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn oklch_to_bgr( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { oklab_to_image::<{ ImageConfiguration::Bgr as u8 }, { OklabTarget::Oklch as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts *Oklch* with interleaved alpha channel to BGRA. This is much more effective than naive direct transformation @@ -372,21 +336,216 @@ pub fn oklch_to_bgr( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn oklch_to_bgra( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { oklab_to_image::<{ ImageConfiguration::Bgra as u8 }, { OklabTarget::Oklch as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::BufferStore; + + // ── helpers ────────────────────────────────────────────────────────────── + + fn make_src(data: Vec, width: u32, channels: u32) -> ImageBuffer<'static, f32> { + ImageBuffer { + data: std::borrow::Cow::Owned(data), + width, + height: 1, + channels, + stride: width * channels, + } + } + + fn make_dst(width: u32, channels: u32) -> ImageBufferMut<'static, u8> { + ImageBufferMut { + data: BufferStore::Owned(vec![0u8; (width * channels) as usize]), + width, + height: 1, + channels, + stride: width * channels, + } + } + + // ── oklab_to_rgb ───────────────────────────────────────────────────────── + + #[test] + fn test_oklab_to_rgb_white() { + // Oklab white: L=1.0, a=0.0, b=0.0 => RGB (255,255,255) + let src = make_src(vec![1.0f32, 0.0, 0.0], 1, 3); + let mut dst = make_dst(1, 3); + oklab_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(dst.data[0], 255, "R should be 255 for white"); + assert_eq!(dst.data[1], 255, "G should be 255 for white"); + assert_eq!(dst.data[2], 255, "B should be 255 for white"); + } + + #[test] + fn test_oklab_to_rgb_black() { + // Oklab black: L=0.0, a=0.0, b=0.0 => RGB (0,0,0) + let src = make_src(vec![0.0f32, 0.0, 0.0], 1, 3); + let mut dst = make_dst(1, 3); + oklab_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(dst.data[0], 0); + assert_eq!(dst.data[1], 0); + assert_eq!(dst.data[2], 0); + } + + #[test] + fn test_oklab_to_rgb_multiple_pixels() { + // Two pixels: black and white + let src = make_src(vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0], 2, 3); + let mut dst = make_dst(2, 3); + oklab_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&dst.data[0..3], &[0, 0, 0]); + assert_eq!(&dst.data[3..6], &[255, 255, 255]); + } + + // ── oklab_to_rgba ──────────────────────────────────────────────────────── + + #[test] + fn test_oklab_to_rgba_white_full_alpha() { + // L=1, a=0, b=0, alpha=1.0 => (255,255,255,255) + let src = make_src(vec![1.0f32, 0.0, 0.0, 1.0], 1, 4); + let mut dst = make_dst(1, 4); + oklab_to_rgba(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(dst.data[0], 255); + assert_eq!(dst.data[1], 255); + assert_eq!(dst.data[2], 255); + assert_eq!(dst.data[3], 255, "Alpha should be 255"); + } + + #[test] + fn test_oklab_to_rgba_zero_alpha() { + let src = make_src(vec![1.0f32, 0.0, 0.0, 0.0], 1, 4); + let mut dst = make_dst(1, 4); + oklab_to_rgba(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(dst.data[3], 0, "Alpha should be 0"); + } + + #[test] + fn test_oklab_to_rgba_half_alpha() { + let src = make_src(vec![1.0f32, 0.0, 0.0, 0.5], 1, 4); + let mut dst = make_dst(1, 4); + oklab_to_rgba(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let alpha = dst.data[3]; + assert!((alpha as i16 - 128).abs() <= 2, "Alpha ~128, got {alpha}"); + } + + // ── oklab_to_bgr ───────────────────────────────────────────────────────── + + #[test] + fn test_oklab_to_bgr_white() { + let src = make_src(vec![1.0f32, 0.0, 0.0], 1, 3); + let mut dst = make_dst(1, 3); + oklab_to_bgr(&src, &mut dst, TransferFunction::Srgb).unwrap(); + // BGR order — all 255 for white + assert_eq!(dst.data[0], 255); + assert_eq!(dst.data[1], 255); + assert_eq!(dst.data[2], 255); + } + + // ── oklab_to_bgra ──────────────────────────────────────────────────────── + + #[test] + fn test_oklab_to_bgra_alpha_preserved() { + let src = make_src(vec![0.5f32, 0.0, 0.0, 0.75], 1, 4); + let mut dst = make_dst(1, 4); + oklab_to_bgra(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let alpha = dst.data[3]; + assert!((alpha as i16 - 191).abs() <= 2, "Alpha ~191, got {alpha}"); + } + + // ── oklch_to_rgb ───────────────────────────────────────────────────────── + + #[test] + fn test_oklch_to_rgb_white() { + // Oklch white: L=1.0, C=0.0, H=0.0 => RGB (255,255,255) + let src = make_src(vec![1.0f32, 0.0, 0.0], 1, 3); + let mut dst = make_dst(1, 3); + oklch_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(dst.data[0], 255); + assert_eq!(dst.data[1], 255); + assert_eq!(dst.data[2], 255); + } + + #[test] + fn test_oklch_to_rgb_black() { + let src = make_src(vec![0.0f32, 0.0, 0.0], 1, 3); + let mut dst = make_dst(1, 3); + oklch_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&dst.data[0..3], &[0, 0, 0]); + } + + // ── oklch_to_rgba ──────────────────────────────────────────────────────── + + #[test] + fn test_oklch_to_rgba_alpha_passthrough() { + let src = make_src(vec![1.0f32, 0.0, 0.0, 0.5], 1, 4); + let mut dst = make_dst(1, 4); + oklch_to_rgba(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let alpha = dst.data[3]; + assert!((alpha as i16 - 128).abs() <= 2, "Alpha ~128, got {alpha}"); + } + + // ── oklch_to_bgr / oklch_to_bgra ──────────────────────────────────────── + + #[test] + fn test_oklch_to_bgr_white() { + let src = make_src(vec![1.0f32, 0.0, 0.0], 1, 3); + let mut dst = make_dst(1, 3); + oklch_to_bgr(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&dst.data[0..3], &[255, 255, 255]); + } + + #[test] + fn test_oklch_to_bgra_black_zero_alpha() { + let src = make_src(vec![0.0f32, 0.0, 0.0, 0.0], 1, 4); + let mut dst = make_dst(1, 4); + oklch_to_bgra(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&dst.data[0..4], &[0, 0, 0, 0]); + } + + // ── error / edge cases ─────────────────────────────────────────────────── + + #[test] + fn test_invalid_channel_count_returns_error() { + let src = make_src(vec![1.0f32, 0.0], 1, 2); // 2 channels — unsupported + let mut dst = make_dst(1, 2); + let result = oklab_to_rgb(&src, &mut dst, TransferFunction::Srgb); + assert!(result.is_err(), "Should fail with 2 channels"); + } + + #[test] + fn test_out_of_range_values_clamped() { + // Values > 1.0 should clamp to 255, not panic + let src = make_src(vec![2.0f32, 0.5, 0.5], 1, 3); + let mut dst = make_dst(1, 3); + let result = oklab_to_rgb(&src, &mut dst, TransferFunction::Srgb); + assert!(result.is_ok()); + assert_eq!(dst.data[0], 255); + } + + #[test] + fn test_negative_values_clamped() { + let src = make_src(vec![-1.0f32, 0.0, 0.0], 1, 3); + let mut dst = make_dst(1, 3); + let result = oklab_to_rgb(&src, &mut dst, TransferFunction::Srgb); + assert!(result.is_ok()); + } + + #[test] + fn test_linear_transfer_function() { + let src = make_src(vec![1.0f32, 0.0, 0.0], 1, 3); + let mut dst = make_dst(1, 3); + oklab_to_rgb(&src, &mut dst, TransferFunction::Linear).unwrap(); + assert_eq!(&dst.data[0..3], &[255, 255, 255]); + } } diff --git a/src/sigmoidal_to_image.rs b/src/sigmoidal_to_image.rs index 04891bd..a86bba8 100644 --- a/src/sigmoidal_to_image.rs +++ b/src/sigmoidal_to_image.rs @@ -98,11 +98,11 @@ fn sigmoidal_to_image( let dst = dst_ptr.add(hx); - dst.add(image_configuration.get_r_channel_offset()) + dst.add(image_configuration.r_index()) .write_unaligned(rgb.r); - dst.add(image_configuration.get_g_channel_offset()) + dst.add(image_configuration.g_index()) .write_unaligned(rgb.g); - dst.add(image_configuration.get_b_channel_offset()) + dst.add(image_configuration.b_index()) .write_unaligned(rgb.b); if image_configuration.has_alpha() { @@ -110,7 +110,7 @@ fn sigmoidal_to_image( .max(0f32) .round() .min(255f32); - dst.add(image_configuration.get_a_channel_offset()) + dst.add(image_configuration.a_index()) .write_unaligned(a as u8); } } diff --git a/src/sse/image_to_jzazbz.rs b/src/sse/image_to_jzazbz.rs index 5ec81f0..807f36e 100644 --- a/src/sse/image_to_jzazbz.rs +++ b/src/sse/image_to_jzazbz.rs @@ -114,26 +114,22 @@ macro_rules! triple_to_jzazbz { } #[target_feature(enable = "sse4.1")] -pub unsafe fn sse_image_to_jzazbz( +pub(crate) unsafe fn sse_image_to_jzazbz( start_cx: usize, - src: *const f32, - src_offset: usize, + src: &[f32], + dst: &mut [f32], width: u32, - dst: *mut f32, - dst_offset: usize, display_luminance: f32, ) -> usize { let target: JzazbzTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); let channels = image_configuration.get_channels_count(); let mut cx = start_cx; - - let dst_ptr = (dst as *mut u8).add(dst_offset) as *mut f32; - let luminance = _mm_set1_ps(display_luminance); - while cx + 4 < width as usize { - let src_ptr = ((src as *const u8).add(src_offset) as *const f32).add(cx * channels); + while cx + 4 <= width as usize { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); + let dst_ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); let (r_chan, g_chan, b_chan, a_chan) = load_f32_and_deinterleave!(src_ptr, image_configuration); diff --git a/src/sse/image_to_oklab.rs b/src/sse/image_to_oklab.rs index 5e62a6d..658268e 100644 --- a/src/sse/image_to_oklab.rs +++ b/src/sse/image_to_oklab.rs @@ -46,19 +46,16 @@ macro_rules! triple_to_oklab { } #[target_feature(enable = "sse4.1")] -pub unsafe fn sse_image_to_oklab( +pub(crate) unsafe fn sse_image_to_oklab( start_cx: usize, width: u32, - dst: *mut f32, - dst_offset: usize, + dst: &mut [f32], ) -> usize { let target: OklabTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); let channels = image_configuration.get_channels_count(); let mut cx = start_cx; - let dst_ptr = (dst as *mut u8).add(dst_offset) as *mut f32; - let (c0, c1, c2, c3, c4, c5, c6, c7, c8) = ( _mm_set1_ps(0.4122214708f32), _mm_set1_ps(0.5363325363f32), @@ -83,10 +80,10 @@ pub unsafe fn sse_image_to_oklab( } #[target_feature(enable = "sse4.1")] -pub unsafe fn sse_oklab_to_image( +pub(crate) unsafe fn sse_oklab_to_image( start_cx: usize, - src: *const f32, - src_offset: usize, - dst: *mut f32, - dst_offset: u32, + src: &[f32], + dst: &mut [f32], width: u32, ) -> usize { let target: OklabTarget = TARGET.into(); @@ -100,8 +98,8 @@ pub unsafe fn sse_oklab_to_image( start_cx: usize, - src: *const u8, + src: &[u8], + dst: &mut [f32], width: u32, - dst: *mut f32, ) -> usize { let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); let mut cx = start_cx; @@ -38,10 +38,8 @@ pub unsafe fn sse_image_to_sigmoidal_row< let channels = image_configuration.get_channels_count(); - let dst_ptr = (dst as *mut u8) as *mut f32; - - while cx + 16 < width as usize { - let src_ptr = src.add(cx * channels); + while cx + 16 <= width as usize { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); let (r_chan, g_chan, b_chan, a_chan) = load_u8_and_deinterleave!(src_ptr, image_configuration); @@ -63,7 +61,7 @@ pub unsafe fn sse_image_to_sigmoidal_row< if USE_ALPHA { let a_low_low = _mm_mul_ps(_mm_cvtepi32_ps(_mm_cvtepi16_epi32(a_low)), u8_scale); - let ptr = dst_ptr.add(cx * channels); + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); store_and_interleave_v4_f32!( ptr, image_configuration, @@ -73,7 +71,7 @@ pub unsafe fn sse_image_to_sigmoidal_row< a_low_low ); } else { - let ptr = dst_ptr.add(cx * channels); + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); store_and_interleave_v3_f32!(ptr, image_configuration, x_low_low, y_low_low, z_low_low); } @@ -87,7 +85,9 @@ pub unsafe fn sse_image_to_sigmoidal_row< if USE_ALPHA { let a_low_high = _mm_mul_ps(_mm_cvtepi32_ps(_mm_unpackhi_epi16(a_low, zeros)), u8_scale); - let ptr = dst_ptr.add(cx * channels + 4 * channels); + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels..) + .as_mut_ptr(); store_and_interleave_v4_f32!( ptr, image_configuration, @@ -97,7 +97,9 @@ pub unsafe fn sse_image_to_sigmoidal_row< a_low_high ); } else { - let ptr = dst_ptr.add(cx * channels + 4 * channels); + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels..) + .as_mut_ptr(); store_and_interleave_v3_f32!( ptr, image_configuration, @@ -122,7 +124,9 @@ pub unsafe fn sse_image_to_sigmoidal_row< if USE_ALPHA { let a_high_low = _mm_mul_ps(_mm_cvtepi32_ps(_mm_cvtepi16_epi32(a_high)), u8_scale); - let ptr = dst_ptr.add(cx * channels + 4 * channels * 2); + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels * 2..) + .as_mut_ptr(); store_and_interleave_v4_f32!( ptr, image_configuration, @@ -132,7 +136,9 @@ pub unsafe fn sse_image_to_sigmoidal_row< a_high_low ); } else { - let ptr = dst_ptr.add(cx * channels + 4 * channels * 2); + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels * 2..) + .as_mut_ptr(); store_and_interleave_v3_f32!( ptr, image_configuration, @@ -154,7 +160,9 @@ pub unsafe fn sse_image_to_sigmoidal_row< _mm_cvtepi32_ps(_mm_unpackhi_epi16(a_high, _mm_setzero_si128())), u8_scale, ); - let ptr = dst_ptr.add(cx * channels + 4 * channels * 3); + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels * 3..) + .as_mut_ptr(); store_and_interleave_v4_f32!( ptr, image_configuration, @@ -164,7 +172,9 @@ pub unsafe fn sse_image_to_sigmoidal_row< a_high_high ); } else { - let ptr = dst_ptr.add(cx * channels + 4 * channels * 3); + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels * 3..) + .as_mut_ptr(); store_and_interleave_v3_f32!( ptr, image_configuration, @@ -178,7 +188,7 @@ pub unsafe fn sse_image_to_sigmoidal_row< } while cx + 8 < width as usize { - let src_ptr = src.add(cx * channels); + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); let (r_chan, g_chan, b_chan, a_chan) = load_u8_and_deinterleave_half!(src_ptr, image_configuration); @@ -200,7 +210,7 @@ pub unsafe fn sse_image_to_sigmoidal_row< if USE_ALPHA { let a_low_low = _mm_mul_ps(_mm_cvtepi32_ps(_mm_cvtepi16_epi32(a_low)), u8_scale); - let ptr = dst_ptr.add(cx * channels); + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); store_and_interleave_v4_f32!( ptr, image_configuration, @@ -210,7 +220,7 @@ pub unsafe fn sse_image_to_sigmoidal_row< a_low_low ); } else { - let ptr = dst_ptr.add(cx * channels); + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); store_and_interleave_v3_f32!(ptr, image_configuration, x_low_low, y_low_low, z_low_low); } @@ -224,7 +234,9 @@ pub unsafe fn sse_image_to_sigmoidal_row< if USE_ALPHA { let a_low_high = _mm_mul_ps(_mm_cvtepi32_ps(_mm_unpackhi_epi16(a_low, zeros)), u8_scale); - let ptr = dst_ptr.add(cx * channels + 4 * channels); + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels..) + .as_mut_ptr(); store_and_interleave_v4_f32!( ptr, image_configuration, @@ -234,7 +246,9 @@ pub unsafe fn sse_image_to_sigmoidal_row< a_low_high ); } else { - let ptr = dst_ptr.add(cx * channels + 4 * channels); + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels..) + .as_mut_ptr(); store_and_interleave_v3_f32!( ptr, image_configuration, diff --git a/src/xyz_lab_to_image.rs b/src/xyz_lab_to_image.rs index b5a2fe0..49a0f4c 100644 --- a/src/xyz_lab_to_image.rs +++ b/src/xyz_lab_to_image.rs @@ -142,13 +142,13 @@ fn xyz_to_channels Date: Thu, 9 Apr 2026 19:34:20 +0100 Subject: [PATCH 2/9] API improvements --- src/avx/from_sigmoidal.rs | 2 +- src/avx/image_to_oklab.rs | 2 +- src/avx/oklab_to_image.rs | 2 +- src/avx/to_sigmoidal.rs | 2 +- src/avx/to_xyz_lab.rs | 2 +- src/avx/xyz_lab_to_image.rs | 2 +- src/avx/xyza_laba_to_image.rs | 2 +- src/hsv_to_image.rs | 2 +- src/image.rs | 2 +- src/image_to_hsv.rs | 2 +- src/image_to_lalphabeta.rs | 215 ++++++++++++--------------------- src/image_to_sigmoidal.rs | 2 - src/image_to_xyz_lab.rs | 2 +- src/image_xyza_laba.rs | 2 +- src/jzazbz_to_image.rs | 2 +- src/lalphabeta_to_image.rs | 2 +- src/linear_to_image.rs | 2 +- src/linear_to_image_u8.rs | 2 +- src/neon/from_sigmoidal.rs | 2 +- src/neon/hsv_to_image.rs | 2 +- src/neon/image_to_hsv.rs | 4 +- src/neon/image_to_jzazbz.rs | 2 +- src/neon/image_to_oklab.rs | 2 +- src/neon/jzazbz_to_image.rs | 2 +- src/neon/oklab_to_image.rs | 2 +- src/neon/to_sigmoidal.rs | 2 +- src/neon/to_xyz_lab.rs | 2 +- src/neon/to_xyza_laba.rs | 2 +- src/neon/xyz_lab_to_image.rs | 2 +- src/neon/xyza_laba_to_image.rs | 2 +- src/oklab_to_image.rs | 2 +- src/sigmoidal_to_image.rs | 2 +- src/sse/from_sigmoidal.rs | 2 +- src/sse/hsv_to_image.rs | 2 +- src/sse/image_to_hsv.rs | 2 +- src/sse/image_to_jzazbz.rs | 2 +- src/sse/image_to_oklab.rs | 2 +- src/sse/jzazbz_to_image.rs | 2 +- src/sse/oklab_to_image.rs | 2 +- src/sse/to_sigmoidal.rs | 2 +- src/sse/to_xyz_lab.rs | 2 +- src/sse/to_xyza_laba.rs | 2 +- src/sse/xyz_lab_to_image.rs | 2 +- src/sse/xyza_laba_to_image.rs | 2 +- src/xyz_lab_to_image.rs | 2 +- src/xyza_laba_to_image.rs | 2 +- 46 files changed, 123 insertions(+), 184 deletions(-) diff --git a/src/avx/from_sigmoidal.rs b/src/avx/from_sigmoidal.rs index 889faf0..562dc22 100644 --- a/src/avx/from_sigmoidal.rs +++ b/src/avx/from_sigmoidal.rs @@ -50,7 +50,7 @@ pub unsafe fn avx_from_sigmoidal_row( ) -> usize { let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; diff --git a/src/avx/image_to_oklab.rs b/src/avx/image_to_oklab.rs index c4ee0f4..451950b 100644 --- a/src/avx/image_to_oklab.rs +++ b/src/avx/image_to_oklab.rs @@ -49,7 +49,7 @@ pub(crate) unsafe fn avx_image_to_oklab usize { let target: OklabTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; let (c0, c1, c2, c3, c4, c5, c6, c7, c8) = ( diff --git a/src/avx/oklab_to_image.rs b/src/avx/oklab_to_image.rs index 3d25ea1..d5c7734 100644 --- a/src/avx/oklab_to_image.rs +++ b/src/avx/oklab_to_image.rs @@ -71,7 +71,7 @@ pub(crate) unsafe fn avx_oklab_to_image usize { let target: OklabTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; let (m0, m1, m2, m3, m4, m5, m6, m7, m8) = ( diff --git a/src/avx/to_sigmoidal.rs b/src/avx/to_sigmoidal.rs index ff3fc3c..6e326a9 100644 --- a/src/avx/to_sigmoidal.rs +++ b/src/avx/to_sigmoidal.rs @@ -32,7 +32,7 @@ pub(crate) unsafe fn avx_image_to_sigmoidal_row< panic!("Use alpha flag used on image without alpha"); } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); while cx + 32 <= width as usize { let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); diff --git a/src/avx/to_xyz_lab.rs b/src/avx/to_xyz_lab.rs index 9fdea3e..1cc8e91 100644 --- a/src/avx/to_xyz_lab.rs +++ b/src/avx/to_xyz_lab.rs @@ -45,7 +45,7 @@ pub unsafe fn avx2_image_to_xyz_lab< } let target: XyzTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; let cq1 = _mm256_set1_ps(*matrix.get_unchecked(0).get_unchecked(0)); diff --git a/src/avx/xyz_lab_to_image.rs b/src/avx/xyz_lab_to_image.rs index 95e3a6a..8530934 100644 --- a/src/avx/xyz_lab_to_image.rs +++ b/src/avx/xyz_lab_to_image.rs @@ -95,7 +95,7 @@ pub unsafe fn avx_xyz_to_channels< panic!("Alpha may be set only on images with alpha"); } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; diff --git a/src/avx/xyza_laba_to_image.rs b/src/avx/xyza_laba_to_image.rs index e83ec89..00b73e2 100644 --- a/src/avx/xyza_laba_to_image.rs +++ b/src/avx/xyza_laba_to_image.rs @@ -82,7 +82,7 @@ pub unsafe fn avx_xyza_to_image); } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let scale = 1f32 / scale; diff --git a/src/image.rs b/src/image.rs index 1954144..983e0aa 100644 --- a/src/image.rs +++ b/src/image.rs @@ -15,7 +15,7 @@ pub enum ImageConfiguration { impl ImageConfiguration { #[inline(always)] - pub const fn get_channels_count(&self) -> usize { + pub const fn channel_count(&self) -> usize { match self { ImageConfiguration::Rgb | ImageConfiguration::Bgr => 3, ImageConfiguration::Rgba | ImageConfiguration::Bgra => 4, diff --git a/src/image_to_hsv.rs b/src/image_to_hsv.rs index 9533422..eaf969c 100644 --- a/src/image_to_hsv.rs +++ b/src/image_to_hsv.rs @@ -55,7 +55,7 @@ fn channels_to_hsv_u16< Some(sse_channels_to_hsv_u16::); } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); #[cfg(feature = "rayon")] { diff --git a/src/image_to_lalphabeta.rs b/src/image_to_lalphabeta.rs index fe6f5ce..40c0393 100644 --- a/src/image_to_lalphabeta.rs +++ b/src/image_to_lalphabeta.rs @@ -5,97 +5,86 @@ * // license that can be found in the LICENSE file. */ use crate::image::ImageConfiguration; -use crate::{LAlphaBeta, Rgb, TransferFunction, SRGB_TO_XYZ_D65}; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -use std::slice; +use crate::{ + ColorError, ImageBuffer, ImageBufferMut, LAlphaBeta, Rgb, TransferFunction, SRGB_TO_XYZ_D65, +}; #[inline(always)] fn channels_to_lalphabeta( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { + src.validate()?; + dst.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); - - let mut lut_table = vec![0f32; 256]; + let mut lut_table = [0.; 256]; for (i, element) in lut_table.iter_mut().enumerate() { *element = transfer_function.linearize(i as f32 * (1. / 255.0)); } - let dst_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - dst.as_mut_ptr() as *mut u8, - dst_stride as usize * height as usize, - ) - }; + let mut a_lut_table = [0f32; 256]; + for (i, lut) in a_lut_table.iter_mut().enumerate() { + *lut = i as f32 * (1. / 255.0); + } - let iter; + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; - #[cfg(feature = "rayon")] - { - iter = dst_slice_safe_align - .par_chunks_exact_mut(dst_stride as usize) - .zip(src.par_chunks_exact(src_stride as usize)); - } - #[cfg(not(feature = "rayon"))] + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) { - iter = dst_slice_safe_align - .chunks_exact_mut(dst_stride as usize) - .zip(src.chunks_exact(src_stride as usize)); - } + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; - iter.for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; - - let mut linearized_row = vec![0f32; width as usize * channels]; - for (linear_chunk, src_chunk) in linearized_row - .chunks_exact_mut(channels) - .zip(src.chunks_exact(channels)) + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb { - linear_chunk[image_configuration.r_index()] = *lut_table - .get_unchecked(src_chunk[image_configuration.r_index()] as usize); - linear_chunk[image_configuration.g_index()] = *lut_table - .get_unchecked(src_chunk[image_configuration.g_index()] as usize); - linear_chunk[image_configuration.b_index()] = *lut_table - .get_unchecked(src_chunk[image_configuration.b_index()] as usize); - if image_configuration.has_alpha() { - linear_chunk[image_configuration.a_index()] = - src_chunk[image_configuration.a_index()] as f32 * (1. / 255.0); + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + let r = lut_table[src[image_configuration.r_index()] as usize]; + let g = lut_table[src[image_configuration.g_index()] as usize]; + let b = lut_table[src[image_configuration.b_index()] as usize]; + let lalphabeta = LAlphaBeta::from_linear_rgb(Rgb::new(r, g, b), &SRGB_TO_XYZ_D65); + dst[0] = lalphabeta.l; + dst[1] = lalphabeta.alpha; + dst[2] = lalphabeta.beta; } - } - - let dst_ptr = dst.as_mut_ptr() as *mut f32; - - for x in _cx..width as usize { - let px = x * channels; - - let src = linearized_row.get_unchecked(px..); - let r = *src.get_unchecked(image_configuration.r_index()); - let g = *src.get_unchecked(image_configuration.g_index()); - let b = *src.get_unchecked(image_configuration.b_index()); - - let rgb = Rgb::::new(r, g, b); - let dst_store = dst_ptr.add(px); - let lalphabeta = LAlphaBeta::from_linear_rgb(rgb, &SRGB_TO_XYZ_D65); - dst_store.write_unaligned(lalphabeta.l); - dst_store.add(1).write_unaligned(lalphabeta.alpha); - dst_store.add(2).write_unaligned(lalphabeta.beta); - - if image_configuration.has_alpha() { - let a = *src.get_unchecked(image_configuration.a_index()); - dst_store.add(3).write_unaligned(a); + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba + { + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + let r = lut_table[src[image_configuration.r_index()] as usize]; + let g = lut_table[src[image_configuration.g_index()] as usize]; + let b = lut_table[src[image_configuration.b_index()] as usize]; + let lalphabeta = LAlphaBeta::from_linear_rgb(Rgb::new(r, g, b), &SRGB_TO_XYZ_D65); + dst[0] = lalphabeta.l; + dst[1] = lalphabeta.alpha; + dst[2] = lalphabeta.beta; + dst[3] = a_lut_table[src[image_configuration.a_index()] as usize]; } } - }); + } + + Ok(()) } /// This function converts RGB to *lαβ* against D65 white point. This is much more effective than naive direct transformation @@ -109,23 +98,11 @@ fn channels_to_lalphabeta( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - transfer function to linear colorspace pub fn rgb_to_lalphabeta( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { - channels_to_lalphabeta::<{ ImageConfiguration::Rgb as u8 }>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + channels_to_lalphabeta::<{ ImageConfiguration::Rgb as u8 }>(src, dst, transfer_function) } /// This function converts RGBA to *lαβ* against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -139,23 +116,11 @@ pub fn rgb_to_lalphabeta( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - transfer function to linear colorspace pub fn rgba_to_lalphabeta( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { - channels_to_lalphabeta::<{ ImageConfiguration::Rgba as u8 }>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + channels_to_lalphabeta::<{ ImageConfiguration::Rgba as u8 }>(src, dst, transfer_function) } /// This function converts BGRA to *lαβ* against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -169,23 +134,11 @@ pub fn rgba_to_lalphabeta( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - transfer function to linear colorspace pub fn bgra_to_lalphabeta( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { - channels_to_lalphabeta::<{ ImageConfiguration::Bgra as u8 }>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + channels_to_lalphabeta::<{ ImageConfiguration::Bgra as u8 }>(src, dst, transfer_function) } /// This function converts BGR to *lαβ* against D65 white point. This is much more effective than naive direct transformation @@ -199,21 +152,9 @@ pub fn bgra_to_lalphabeta( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - transfer function to linear colorspace pub fn bgr_to_lalphabeta( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { - channels_to_lalphabeta::<{ ImageConfiguration::Bgr as u8 }>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + channels_to_lalphabeta::<{ ImageConfiguration::Bgr as u8 }>(src, dst, transfer_function) } diff --git a/src/image_to_sigmoidal.rs b/src/image_to_sigmoidal.rs index 6a474c0..801e6f7 100644 --- a/src/image_to_sigmoidal.rs +++ b/src/image_to_sigmoidal.rs @@ -382,8 +382,6 @@ mod tests { } } - // ── rgb vs rgba consistency ─────────────────────────────────────────────── - #[test] fn test_rgb_and_rgba_same_rgb_channels() { // RGB channels must be identical whether or not alpha is present diff --git a/src/image_to_xyz_lab.rs b/src/image_to_xyz_lab.rs index bc818de..9d74a76 100644 --- a/src/image_to_xyz_lab.rs +++ b/src/image_to_xyz_lab.rs @@ -39,7 +39,7 @@ fn channels_to_xyz); } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut lut_table = vec![0f32; 256]; for (i, lut) in lut_table.iter_mut().enumerate() { diff --git a/src/jzazbz_to_image.rs b/src/jzazbz_to_image.rs index 372070a..0dbf318 100644 --- a/src/jzazbz_to_image.rs +++ b/src/jzazbz_to_image.rs @@ -75,7 +75,7 @@ fn jzazbz_to_image( } iter.for_each(|(dst, src)| unsafe { - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut _cx = 0usize; diff --git a/src/lalphabeta_to_image.rs b/src/lalphabeta_to_image.rs index f0a31bc..97ccfa7 100644 --- a/src/lalphabeta_to_image.rs +++ b/src/lalphabeta_to_image.rs @@ -23,7 +23,7 @@ fn lalphabeta_to_image( ) { let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut lut_table = vec![0u8; 2049]; for (i, lut) in lut_table.iter_mut().enumerate() { diff --git a/src/linear_to_image.rs b/src/linear_to_image.rs index 15834c0..9151559 100644 --- a/src/linear_to_image.rs +++ b/src/linear_to_image.rs @@ -28,7 +28,7 @@ fn linear_to_gamma_channels( ) -> usize { let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; diff --git a/src/neon/hsv_to_image.rs b/src/neon/hsv_to_image.rs index 9a20bb2..ff94d9b 100644 --- a/src/neon/hsv_to_image.rs +++ b/src/neon/hsv_to_image.rs @@ -32,7 +32,7 @@ pub unsafe fn neon_hsv_u16_to_image< panic!("Use alpha flag used on image without alpha"); } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let v_scale = vdupq_n_f32(scale); let dst_ptr = dst.add(dst_offset); diff --git a/src/neon/image_to_hsv.rs b/src/neon/image_to_hsv.rs index 7826111..366b75f 100644 --- a/src/neon/image_to_hsv.rs +++ b/src/neon/image_to_hsv.rs @@ -33,7 +33,7 @@ pub unsafe fn neon_channels_to_hsv< panic!("Use alpha flag used on image without alpha"); } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let v_scale = vdupq_n_f32(scale); @@ -186,7 +186,7 @@ pub unsafe fn neon_channels_to_hsv_u16< panic!("Use alpha flag used on image without alpha"); } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let v_scale = vdupq_n_f32(scale); diff --git a/src/neon/image_to_jzazbz.rs b/src/neon/image_to_jzazbz.rs index 69bb816..311c3f0 100644 --- a/src/neon/image_to_jzazbz.rs +++ b/src/neon/image_to_jzazbz.rs @@ -107,7 +107,7 @@ pub(crate) unsafe fn neon_image_to_jzazbz usize { let target: JzazbzTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; while cx + 4 <= width as usize { diff --git a/src/neon/image_to_oklab.rs b/src/neon/image_to_oklab.rs index b67464a..6121787 100644 --- a/src/neon/image_to_oklab.rs +++ b/src/neon/image_to_oklab.rs @@ -45,7 +45,7 @@ pub unsafe fn neon_image_to_oklab usize { let target: OklabTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; let (c0, c1, c2, c3, c4, c5, c6, c7, c8) = ( diff --git a/src/neon/jzazbz_to_image.rs b/src/neon/jzazbz_to_image.rs index 740169f..9233b5d 100644 --- a/src/neon/jzazbz_to_image.rs +++ b/src/neon/jzazbz_to_image.rs @@ -121,7 +121,7 @@ pub unsafe fn neon_jzazbz_to_image usize { let target: JzazbzTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; let luminance_scale: f32 = 1. / display_luminance; diff --git a/src/neon/oklab_to_image.rs b/src/neon/oklab_to_image.rs index 0eefee2..88359d5 100644 --- a/src/neon/oklab_to_image.rs +++ b/src/neon/oklab_to_image.rs @@ -65,7 +65,7 @@ pub(crate) unsafe fn neon_oklab_to_image usize { let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; let (m0, m1, m2, m3, m4, m5, m6, m7, m8) = ( diff --git a/src/neon/to_sigmoidal.rs b/src/neon/to_sigmoidal.rs index 28c1c00..e51a5c2 100644 --- a/src/neon/to_sigmoidal.rs +++ b/src/neon/to_sigmoidal.rs @@ -25,7 +25,7 @@ pub(crate) fn neon_image_to_sigmoidal usize { let target: XyzTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; let cq1 = vdupq_n_f32(*matrix.get_unchecked(0).get_unchecked(0)); diff --git a/src/neon/xyz_lab_to_image.rs b/src/neon/xyz_lab_to_image.rs index f5e54a6..d0acc7c 100644 --- a/src/neon/xyz_lab_to_image.rs +++ b/src/neon/xyz_lab_to_image.rs @@ -82,7 +82,7 @@ pub unsafe fn neon_xyz_to_channels< panic!("Alpha may be set only on images with alpha"); } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; diff --git a/src/neon/xyza_laba_to_image.rs b/src/neon/xyza_laba_to_image.rs index c412639..1c74b98 100644 --- a/src/neon/xyza_laba_to_image.rs +++ b/src/neon/xyza_laba_to_image.rs @@ -71,7 +71,7 @@ pub unsafe fn neon_xyza_to_image( .min(255.) as u8; } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let dst_stride = dst.stride(); let src_r_width = src.width * src.channels; diff --git a/src/sigmoidal_to_image.rs b/src/sigmoidal_to_image.rs index a86bba8..dcee138 100644 --- a/src/sigmoidal_to_image.rs +++ b/src/sigmoidal_to_image.rs @@ -33,7 +33,7 @@ fn sigmoidal_to_image( panic!("Alpha may be set only on images with alpha"); } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut _wide_row_handler: Option usize> = None; diff --git a/src/sse/from_sigmoidal.rs b/src/sse/from_sigmoidal.rs index 20376f0..0f26952 100644 --- a/src/sse/from_sigmoidal.rs +++ b/src/sse/from_sigmoidal.rs @@ -56,7 +56,7 @@ pub unsafe fn sse_from_sigmoidal_row( ) -> usize { let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; diff --git a/src/sse/hsv_to_image.rs b/src/sse/hsv_to_image.rs index 3671775..46ea16a 100644 --- a/src/sse/hsv_to_image.rs +++ b/src/sse/hsv_to_image.rs @@ -38,7 +38,7 @@ pub unsafe fn sse_hsv_u16_to_image< panic!("Use alpha flag used on image without alpha"); } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let v_scale = _mm_set1_ps(scale); diff --git a/src/sse/image_to_hsv.rs b/src/sse/image_to_hsv.rs index 60e3693..a247a1a 100644 --- a/src/sse/image_to_hsv.rs +++ b/src/sse/image_to_hsv.rs @@ -39,7 +39,7 @@ pub unsafe fn sse_channels_to_hsv_u16< panic!("Use alpha flag used on image without alpha"); } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let v_scale = _mm_set1_ps(scale); diff --git a/src/sse/image_to_jzazbz.rs b/src/sse/image_to_jzazbz.rs index 807f36e..bde95a3 100644 --- a/src/sse/image_to_jzazbz.rs +++ b/src/sse/image_to_jzazbz.rs @@ -123,7 +123,7 @@ pub(crate) unsafe fn sse_image_to_jzazbz usize { let target: JzazbzTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; let luminance = _mm_set1_ps(display_luminance); diff --git a/src/sse/image_to_oklab.rs b/src/sse/image_to_oklab.rs index 658268e..eea1197 100644 --- a/src/sse/image_to_oklab.rs +++ b/src/sse/image_to_oklab.rs @@ -53,7 +53,7 @@ pub(crate) unsafe fn sse_image_to_oklab usize { let target: OklabTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; let (c0, c1, c2, c3, c4, c5, c6, c7, c8) = ( diff --git a/src/sse/jzazbz_to_image.rs b/src/sse/jzazbz_to_image.rs index 8ea010b..5cb4d2e 100644 --- a/src/sse/jzazbz_to_image.rs +++ b/src/sse/jzazbz_to_image.rs @@ -131,7 +131,7 @@ pub unsafe fn sse_jzazbz_to_image usize { let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; let luminance_scale = _mm_set1_ps(1. / display_luminance); diff --git a/src/sse/oklab_to_image.rs b/src/sse/oklab_to_image.rs index d9f2db6..a63f44a 100644 --- a/src/sse/oklab_to_image.rs +++ b/src/sse/oklab_to_image.rs @@ -71,7 +71,7 @@ pub(crate) unsafe fn sse_oklab_to_image usize { let target: OklabTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; let (m0, m1, m2, m3, m4, m5, m6, m7, m8) = ( diff --git a/src/sse/to_sigmoidal.rs b/src/sse/to_sigmoidal.rs index 46e7681..bac2eb5 100644 --- a/src/sse/to_sigmoidal.rs +++ b/src/sse/to_sigmoidal.rs @@ -36,7 +36,7 @@ pub(crate) unsafe fn sse_image_to_sigmoidal_row< panic!("Use alpha flag used on image without alpha"); } - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); while cx + 16 <= width as usize { let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); diff --git a/src/sse/to_xyz_lab.rs b/src/sse/to_xyz_lab.rs index 880182f..a4c45fe 100644 --- a/src/sse/to_xyz_lab.rs +++ b/src/sse/to_xyz_lab.rs @@ -36,7 +36,7 @@ pub unsafe fn sse_channels_to_xyz_or_lab< } let target: XyzTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.get_channels_count(); + let channels = image_configuration.channel_count(); let mut cx = start_cx; let cq1 = _mm_set1_ps(*matrix.get_unchecked(0).get_unchecked(0)); diff --git a/src/sse/to_xyza_laba.rs b/src/sse/to_xyza_laba.rs index 1d16f52..cb4b502 100644 --- a/src/sse/to_xyza_laba.rs +++ b/src/sse/to_xyza_laba.rs @@ -28,7 +28,7 @@ pub unsafe fn sse_channels_to_xyza_laba Date: Fri, 10 Apr 2026 20:55:43 +0100 Subject: [PATCH 3/9] API improvements --- src/avx/cie.rs | 115 ++--- src/avx/mod.rs | 2 +- src/avx/to_xyz_lab.rs | 39 +- src/hsv_to_image.rs | 13 +- src/image_to_hsv.rs | 498 +++++++++++--------- src/image_to_linear.rs | 2 +- src/image_to_sigmoidal.rs | 4 +- src/image_to_xyz_lab.rs | 924 ++++++++++++-------------------------- src/image_xyza_laba.rs | 12 +- src/neon/image_to_hsv.rs | 52 ++- src/neon/mod.rs | 4 +- src/neon/to_xyz_lab.rs | 25 +- src/sse/cie.rs | 243 +++++----- src/sse/image_to_hsv.rs | 24 +- src/sse/math.rs | 15 +- src/sse/mod.rs | 2 +- src/sse/support.rs | 5 +- src/sse/to_xyz_lab.rs | 29 +- src/xyz_lab_to_image.rs | 30 +- src/xyza_laba_to_image.rs | 30 +- 20 files changed, 899 insertions(+), 1169 deletions(-) diff --git a/src/avx/cie.rs b/src/avx/cie.rs index 2977a36..747a3a0 100644 --- a/src/avx/cie.rs +++ b/src/avx/cie.rs @@ -118,70 +118,71 @@ pub(crate) unsafe fn avx2_triple_to_xyz( (x, y, z) } -#[inline(always)] -pub(crate) unsafe fn avx2_triple_to_luv( - x: __m256, - y: __m256, - z: __m256, -) -> (__m256, __m256, __m256) { - let zeros = _mm256_setzero_ps(); - let den = _mm256_prefer_fma_ps( - _mm256_prefer_fma_ps(x, z, _mm256_set1_ps(3f32)), - y, - _mm256_set1_ps(15f32), - ); - let nan_mask = _mm256_cmp_ps::<_CMP_LT_OS>(den, _mm256_set1_ps(0f32)); - let l_low_mask = _mm256_cmp_ps::<_CMP_LT_OS>(y, _mm256_set1_ps(LUV_CUTOFF_FORWARD_Y)); - let y_cbrt = _mm256_cbrt_ps(y); - let l = _mm256_select_ps( - l_low_mask, - _mm256_mul_ps(y, _mm256_set1_ps(LUV_MULTIPLIER_FORWARD_Y)), - _mm256_prefer_fma_ps(_mm256_set1_ps(-16f32), y_cbrt, _mm256_set1_ps(116f32)), - ); - let u_prime = _mm256_div_ps(_mm256_mul_ps(x, _mm256_set1_ps(4f32)), den); - let v_prime = _mm256_div_ps(_mm256_mul_ps(y, _mm256_set1_ps(9f32)), den); - let sub_u_prime = _mm256_sub_ps(u_prime, _mm256_set1_ps(LUV_WHITE_U_PRIME)); - let sub_v_prime = _mm256_sub_ps(v_prime, _mm256_set1_ps(LUV_WHITE_V_PRIME)); - let l13 = _mm256_mul_ps(l, _mm256_set1_ps(13f32)); - let u = _mm256_select_ps(nan_mask, zeros, _mm256_mul_ps(l13, sub_u_prime)); - let v = _mm256_select_ps(nan_mask, zeros, _mm256_mul_ps(l13, sub_v_prime)); - (l, u, v) +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx2_triple_to_luv(x: __m256, y: __m256, z: __m256) -> (__m256, __m256, __m256) { + unsafe { + let zeros = _mm256_setzero_ps(); + let den = _mm256_prefer_fma_ps( + _mm256_prefer_fma_ps(x, z, _mm256_set1_ps(3f32)), + y, + _mm256_set1_ps(15f32), + ); + let nan_mask = _mm256_cmp_ps::<_CMP_LT_OS>(den, _mm256_set1_ps(0f32)); + let l_low_mask = _mm256_cmp_ps::<_CMP_LT_OS>(y, _mm256_set1_ps(LUV_CUTOFF_FORWARD_Y)); + let y_cbrt = _mm256_cbrt_ps(y); + let l = _mm256_select_ps( + l_low_mask, + _mm256_mul_ps(y, _mm256_set1_ps(LUV_MULTIPLIER_FORWARD_Y)), + _mm256_prefer_fma_ps(_mm256_set1_ps(-16f32), y_cbrt, _mm256_set1_ps(116f32)), + ); + let u_prime = _mm256_div_ps(_mm256_mul_ps(x, _mm256_set1_ps(4f32)), den); + let v_prime = _mm256_div_ps(_mm256_mul_ps(y, _mm256_set1_ps(9f32)), den); + let sub_u_prime = _mm256_sub_ps(u_prime, _mm256_set1_ps(LUV_WHITE_U_PRIME)); + let sub_v_prime = _mm256_sub_ps(v_prime, _mm256_set1_ps(LUV_WHITE_V_PRIME)); + let l13 = _mm256_mul_ps(l, _mm256_set1_ps(13f32)); + let u = _mm256_select_ps(nan_mask, zeros, _mm256_mul_ps(l13, sub_u_prime)); + let v = _mm256_select_ps(nan_mask, zeros, _mm256_mul_ps(l13, sub_v_prime)); + (l, u, v) + } } -#[inline(always)] -pub(crate) unsafe fn avx2_triple_to_lab( - x: __m256, - y: __m256, - z: __m256, -) -> (__m256, __m256, __m256) { - let x = _mm256_mul_ps(x, _mm256_set1_ps(100f32 / 95.047f32)); - let z = _mm256_mul_ps(z, _mm256_set1_ps(100f32 / 108.883f32)); - let cbrt_x = _mm256_cbrt_ps(x); - let cbrt_y = _mm256_cbrt_ps(y); - let cbrt_z = _mm256_cbrt_ps(z); - let s_1 = _mm256_set1_ps(16.0 / 116.0); - let s_2 = _mm256_set1_ps(7.787); - let lower_x = _mm256_prefer_fma_ps(s_1, s_2, x); - let lower_y = _mm256_prefer_fma_ps(s_1, s_2, y); - let lower_z = _mm256_prefer_fma_ps(s_1, s_2, z); - let cutoff = _mm256_set1_ps(0.008856f32); - let x = _mm256_select_ps(_mm256_cmp_ps::<_CMP_GT_OS>(x, cutoff), cbrt_x, lower_x); - let y = _mm256_select_ps(_mm256_cmp_ps::<_CMP_GT_OS>(y, cutoff), cbrt_y, lower_y); - let z = _mm256_select_ps(_mm256_cmp_ps::<_CMP_GT_OS>(z, cutoff), cbrt_z, lower_z); - let l = _mm256_prefer_fma_ps(_mm256_set1_ps(-16.0f32), y, _mm256_set1_ps(116.0f32)); - let a = _mm256_mul_ps(_mm256_sub_ps(x, y), _mm256_set1_ps(500f32)); - let b = _mm256_mul_ps(_mm256_sub_ps(y, z), _mm256_set1_ps(200f32)); - (l, a, b) +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx2_triple_to_lab(x: __m256, y: __m256, z: __m256) -> (__m256, __m256, __m256) { + unsafe { + let x = _mm256_mul_ps(x, _mm256_set1_ps(100f32 / 95.047f32)); + let z = _mm256_mul_ps(z, _mm256_set1_ps(100f32 / 108.883f32)); + let cbrt_x = _mm256_cbrt_ps(x); + let cbrt_y = _mm256_cbrt_ps(y); + let cbrt_z = _mm256_cbrt_ps(z); + let s_1 = _mm256_set1_ps(16.0 / 116.0); + let s_2 = _mm256_set1_ps(7.787); + let lower_x = _mm256_prefer_fma_ps(s_1, s_2, x); + let lower_y = _mm256_prefer_fma_ps(s_1, s_2, y); + let lower_z = _mm256_prefer_fma_ps(s_1, s_2, z); + let cutoff = _mm256_set1_ps(0.008856f32); + let x = _mm256_select_ps(_mm256_cmp_ps::<_CMP_GT_OS>(x, cutoff), cbrt_x, lower_x); + let y = _mm256_select_ps(_mm256_cmp_ps::<_CMP_GT_OS>(y, cutoff), cbrt_y, lower_y); + let z = _mm256_select_ps(_mm256_cmp_ps::<_CMP_GT_OS>(z, cutoff), cbrt_z, lower_z); + let l = _mm256_prefer_fma_ps(_mm256_set1_ps(-16.0f32), y, _mm256_set1_ps(116.0f32)); + let a = _mm256_mul_ps(_mm256_sub_ps(x, y), _mm256_set1_ps(500f32)); + let b = _mm256_mul_ps(_mm256_sub_ps(y, z), _mm256_set1_ps(200f32)); + (l, a, b) + } } -#[inline(always)] +#[inline] +#[target_feature(enable = "avx2")] pub(crate) unsafe fn avx_triple_to_lch( x: __m256, y: __m256, z: __m256, ) -> (__m256, __m256, __m256) { - let (luv_l, luv_u, luv_v) = avx2_triple_to_luv(x, y, z); - let lch_c = _mm256_hypot_ps(luv_u, luv_v); - let lch_h = _mm256_atan2_ps(luv_v, luv_u); - (luv_l, lch_c, lch_h) + unsafe { + let (luv_l, luv_u, luv_v) = avx2_triple_to_luv(x, y, z); + let lch_c = _mm256_hypot_ps(luv_u, luv_v); + let lch_h = _mm256_atan2_ps(luv_v, luv_u); + (luv_l, lch_c, lch_h) + } } diff --git a/src/avx/mod.rs b/src/avx/mod.rs index 8823cfd..de60f08 100644 --- a/src/avx/mod.rs +++ b/src/avx/mod.rs @@ -26,7 +26,7 @@ pub use math::*; pub(crate) use oklab_to_image::avx_oklab_to_image; pub use support::*; pub(crate) use to_sigmoidal::avx_image_to_sigmoidal_row; -pub use to_xyz_lab::*; +pub(crate) use to_xyz_lab::avx2_image_to_xyz_lab; pub use utils::*; pub use xyz_lab_to_image::*; pub use xyza_laba_to_image::*; diff --git a/src/avx/to_xyz_lab.rs b/src/avx/to_xyz_lab.rs index 1cc8e91..942910a 100644 --- a/src/avx/to_xyz_lab.rs +++ b/src/avx/to_xyz_lab.rs @@ -25,22 +25,19 @@ use crate::xyz_target::XyzTarget; use crate::{avx_store_and_interleave_v3_direct_f32, load_f32_and_deinterleave}; #[target_feature(enable = "avx2")] -pub unsafe fn avx2_image_to_xyz_lab< +pub(crate) unsafe fn avx2_image_to_xyz_lab< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, const TARGET: u8, >( start_cx: usize, - src: *const f32, - src_offset: usize, + src: &[f32], + dst: &mut [f32], + a_linearized: &mut [f32], width: u32, - dst: *mut f32, - dst_offset: usize, - a_linearized: *mut f32, - a_offset: usize, matrix: &[[f32; 3]; 3], ) -> usize { - if USE_ALPHA && a_linearized.is_null() { + if USE_ALPHA && a_linearized.is_empty() { panic!("Null alpha channel with requirements of linearized alpha if not supported"); } let target: XyzTarget = TARGET.into(); @@ -58,10 +55,8 @@ pub unsafe fn avx2_image_to_xyz_lab< let cq8 = _mm256_set1_ps(*matrix.get_unchecked(2).get_unchecked(1)); let cq9 = _mm256_set1_ps(*matrix.get_unchecked(2).get_unchecked(2)); - let dst_ptr = (dst as *mut u8).add(dst_offset) as *mut f32; - - while cx + 8 < width as usize { - let src_ptr = ((src as *const u8).add(src_offset) as *const f32).add(cx * channels); + while cx + 8 <= width as usize { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); let (r_chan, g_chan, b_chan, a_chan) = avx_vld_f32_and_deinterleave::(src_ptr); @@ -91,20 +86,18 @@ pub unsafe fn avx2_image_to_xyz_lab< } } - let write_dst_ptr = dst_ptr.add(cx * 3); + let write_dst_ptr = dst.get_unchecked_mut(cx * 3..).as_mut_ptr(); avx_store_and_interleave_v3_direct_f32!(write_dst_ptr, x_low_low, y_low_low, z_low_low); if USE_ALPHA { - let a_ptr = (a_linearized as *mut u8).add(a_offset) as *mut f32; - - _mm256_storeu_ps(a_ptr.add(cx), a_chan); + _mm256_storeu_ps(a_linearized.get_unchecked_mut(cx), a_chan); } cx += 8; } - while cx + 4 < width as usize { - let src_ptr = ((src as *const u8).add(src_offset) as *const f32).add(cx * channels); + while cx + 4 <= width as usize { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); let (r_chan, g_chan, b_chan, a_chan) = load_f32_and_deinterleave!(src_ptr, image_configuration); @@ -146,14 +139,12 @@ pub unsafe fn avx2_image_to_xyz_lab< } let (v0, v1, v2) = sse_interleave_ps_rgb(x_low_low, y_low_low, z_low_low); - _mm_storeu_ps(dst_ptr.add(cx * 3), v0); - _mm_storeu_ps(dst_ptr.add(cx * 3 + 4), v1); - _mm_storeu_ps(dst_ptr.add(cx * 3 + 8), v2); + _mm_storeu_ps(dst.get_unchecked_mut(cx * 3), v0); + _mm_storeu_ps(dst.get_unchecked_mut(cx * 3 + 4), v1); + _mm_storeu_ps(dst.get_unchecked_mut(cx * 3 + 8), v2); if USE_ALPHA { - let a_ptr = (a_linearized as *mut u8).add(a_offset) as *mut f32; - - _mm_storeu_ps(a_ptr.add(cx), a_chan); + _mm_storeu_ps(a_linearized.get_unchecked_mut(cx), a_chan); } cx += 4; diff --git a/src/hsv_to_image.rs b/src/hsv_to_image.rs index 6d3bff7..77ef02e 100644 --- a/src/hsv_to_image.rs +++ b/src/hsv_to_image.rs @@ -110,18 +110,13 @@ fn hsv_u16_to_channels< } }; - *dst_slice.get_unchecked_mut(hx + image_configuration.r_index()) = - rgb.r; - *dst_slice.get_unchecked_mut(hx + image_configuration.g_index()) = - rgb.g; - *dst_slice.get_unchecked_mut(hx + image_configuration.b_index()) = - rgb.b; + *dst_slice.get_unchecked_mut(hx + image_configuration.r_index()) = rgb.r; + *dst_slice.get_unchecked_mut(hx + image_configuration.g_index()) = rgb.g; + *dst_slice.get_unchecked_mut(hx + image_configuration.b_index()) = rgb.b; if image_configuration.has_alpha() { let a = src.add(3).read_unaligned(); - *dst_slice - .get_unchecked_mut(hx + image_configuration.a_index()) = - a as u8; + *dst_slice.get_unchecked_mut(hx + image_configuration.a_index()) = a as u8; } } }); diff --git a/src/image_to_hsv.rs b/src/image_to_hsv.rs index eaf969c..4fb4705 100644 --- a/src/image_to_hsv.rs +++ b/src/image_to_hsv.rs @@ -11,13 +11,7 @@ use crate::image_to_hsv_support::HsvTarget; use crate::neon::neon_channels_to_hsv_u16; #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] use crate::sse::sse_channels_to_hsv_u16; -use crate::Rgb; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -#[cfg(feature = "rayon")] -use std::slice; +use crate::{ColorError, ImageBuffer, ImageBufferMut, Rgb}; #[allow(clippy::type_complexity)] fn channels_to_hsv_u16< @@ -25,23 +19,24 @@ fn channels_to_hsv_u16< const USE_ALPHA: bool, const TARGET: u8, >( - src: &[u8], - src_stride: u32, - dst: &mut [u16], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u16>, scale: f32, -) { +) -> Result<(), ColorError> { + src.validate()?; + dst.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } let target: HsvTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); if USE_ALPHA && !image_configuration.has_alpha() { panic!("Alpha may be set only on images with alpha"); } - let mut _wide_row_handler: Option< - unsafe fn(usize, *const u8, usize, u32, *mut u16, usize, f32) -> usize, - > = None; + let mut _wide_row_handler: Option usize> = + None; #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] { @@ -55,158 +50,94 @@ fn channels_to_hsv_u16< Some(sse_channels_to_hsv_u16::); } - let channels = image_configuration.channel_count(); + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; + let width = src.width; - #[cfg(feature = "rayon")] + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) { - let dst_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - dst.as_mut_ptr() as *mut u8, - dst_stride as usize * height as usize, - ) - }; - dst_slice_safe_align - .par_chunks_exact_mut(dst_stride as usize) - .zip(src.par_chunks_exact(src_stride as usize)) - .for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; - - if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher( - _cx, - src.as_ptr(), - 0, - width, - dst.as_mut_ptr() as *mut u16, - 0, - scale, - ); - } + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; - let src_ptr = src.as_ptr(); - let dst_ptr = dst.as_mut_ptr() as *mut u16; - - for x in _cx..width as usize { - let px = x * channels; - let src = src_ptr.add(px); - let r = src - .add(image_configuration.r_index()) - .read_unaligned(); - let g = src - .add(image_configuration.g_index()) - .read_unaligned(); - let b = src - .add(image_configuration.b_index()) - .read_unaligned(); - - let rgb = Rgb::::new(r, g, b); - let hx = x * channels; - let dst = dst_ptr.add(hx); - match target { - HsvTarget::Hsv => { - let hsv = rgb.to_hsv(); - - dst.write_unaligned(hsv.h as u16); - dst.add(1).write_unaligned((hsv.s * scale).round() as u16); - dst.add(2).write_unaligned((hsv.v * scale).round() as u16); - } - HsvTarget::Hsl => { - let hsl = rgb.to_hsl(); - - dst.write_unaligned(hsl.h as u16); - dst.add(1).write_unaligned((hsl.s * scale).round() as u16); - dst.add(2).write_unaligned((hsl.l * scale).round() as u16); - } - } + let mut cx = 0usize; + + if let Some(dispatcher) = _wide_row_handler { + cx = unsafe { dispatcher(cx, src, width, dst, scale) }; + } - if image_configuration.has_alpha() { - let a = src - .add(image_configuration.a_index()) - .read_unaligned(); - dst.add(3).write_unaligned(a as u16); + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + let dst = &mut dst[cx * 3..]; + let src = &src[cx * 3..]; + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + let r = src[image_configuration.r_index()]; + let g = src[image_configuration.g_index()]; + let b = src[image_configuration.b_index()]; + let rgb = Rgb::::new(r, g, b); + match target { + HsvTarget::Hsv => { + let hsv = rgb.to_hsv(); + + dst[0] = hsv.h as u16; + dst[1] = (hsv.s * scale).round() as u16; + dst[2] = (hsv.v * scale).round() as u16; } - } - }); - } + HsvTarget::Hsl => { + let hsl = rgb.to_hsl(); - #[cfg(not(feature = "rayon"))] - { - let mut src_offset = 0usize; - let mut dst_offset = 0usize; - - for _ in 0..height as usize { - let mut _cx = 0usize; - - if let Some(dispatcher) = _wide_row_handler { - unsafe { - _cx = dispatcher( - _cx, - src.as_ptr(), - src_offset, - width, - dst.as_mut_ptr(), - dst_offset, - scale, - ) + dst[0] = hsl.h as u16; + dst[1] = (hsl.s * scale).round() as u16; + dst[2] = (hsl.l * scale).round() as u16; + } } } - - let src_ptr = unsafe { src.as_ptr().add(src_offset) }; - let dst_ptr = unsafe { (dst.as_mut_ptr() as *mut u8).add(dst_offset) as *mut u16 }; - - for x in _cx..width as usize { - let px = x * channels; - let src = unsafe { src_ptr.add(px) }; - let r = unsafe { - src.add(image_configuration.get_r_channel_offset()) - .read_unaligned() - }; - let g = unsafe { - src.add(image_configuration.get_g_channel_offset()) - .read_unaligned() - }; - let b = unsafe { - src.add(image_configuration.get_b_channel_offset()) - .read_unaligned() - }; - + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba + { + let dst = &mut dst[cx * 4..]; + let src = &src[cx * 4..]; + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + let r = src[image_configuration.r_index()]; + let g = src[image_configuration.g_index()]; + let b = src[image_configuration.b_index()]; let rgb = Rgb::::new(r, g, b); - let hx = x * channels; - let dst = unsafe { dst_ptr.add(hx) }; match target { HsvTarget::Hsv => { let hsv = rgb.to_hsv(); - unsafe { - dst.write_unaligned(hsv.h as u16); - dst.add(1).write_unaligned((hsv.s * scale).round() as u16); - dst.add(2).write_unaligned((hsv.v * scale).round() as u16); - } + + dst[0] = hsv.h as u16; + dst[1] = (hsv.s * scale).round() as u16; + dst[2] = (hsv.v * scale).round() as u16; } HsvTarget::Hsl => { let hsl = rgb.to_hsl(); - unsafe { - dst.write_unaligned(hsl.h as u16); - dst.add(1).write_unaligned((hsl.s * scale).round() as u16); - dst.add(2).write_unaligned((hsl.l * scale).round() as u16); - } - } - } - if image_configuration.has_alpha() { - let a = unsafe { - src.add(image_configuration.get_a_channel_offset()) - .read_unaligned() - }; - unsafe { - dst.add(3).write_unaligned(a as u16); + dst[0] = hsl.h as u16; + dst[1] = (hsl.s * scale).round() as u16; + dst[2] = (hsl.l * scale).round() as u16; } } + dst[3] = src[image_configuration.a_index()] as u16; } - - src_offset += src_stride as usize; - dst_offset += dst_stride as usize; } } + Ok(()) } /// This function converts RGB to HSV. This is much more effective than naive direct transformation @@ -220,17 +151,13 @@ fn channels_to_hsv_u16< /// * `dst_stride` - Bytes per row for dst data /// * `scale` - Natural range for S and V is [0,1] it may be more convenient and required for u16 transformation to scale it by 100 or any other number to keep S,V in range [0, scale] pub fn rgb_to_hsv( - src: &[u8], - src_stride: u32, - dst: &mut [u16], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u16>, scale: f32, -) { +) -> Result<(), ColorError> { channels_to_hsv_u16::<{ ImageConfiguration::Rgb as u8 }, false, { HsvTarget::Hsv as u8 }>( - src, src_stride, dst, dst_stride, width, height, scale, - ); + src, dst, scale, + ) } /// This function converts BGRA to HSV. Alpha channel is copied and leaved unchanged. This is much more effective than naive direct transformation @@ -244,17 +171,13 @@ pub fn rgb_to_hsv( /// * `dst_stride` - Bytes per row for dst data /// * `scale` - Natural range for S and V is [0,1] it may be more convenient and required for u16 transformation to scale it by 100 or any other number to keep S,V in range [0, scale] pub fn bgra_to_hsv( - src: &[u8], - src_stride: u32, - dst: &mut [u16], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u16>, scale: f32, -) { +) -> Result<(), ColorError> { channels_to_hsv_u16::<{ ImageConfiguration::Bgra as u8 }, true, { HsvTarget::Hsv as u8 }>( - src, src_stride, dst, dst_stride, width, height, scale, - ); + src, dst, scale, + ) } /// This function converts RGBA to HSV. Alpha channel is copied and leaved unchanged. This is much more effective than naive direct transformation @@ -268,17 +191,13 @@ pub fn bgra_to_hsv( /// * `dst_stride` - Bytes per row for dst data /// * `scale` - Natural range for S and V is [0,1] it may be more convenient and required for u16 transformation to scale it by 100 or any other number to keep S,V in range [0, scale] pub fn rgba_to_hsv( - src: &[u8], - src_stride: u32, - dst: &mut [u16], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u16>, scale: f32, -) { +) -> Result<(), ColorError> { channels_to_hsv_u16::<{ ImageConfiguration::Rgba as u8 }, true, { HsvTarget::Hsv as u8 }>( - src, src_stride, dst, dst_stride, width, height, scale, - ); + src, dst, scale, + ) } /// This function converts RGB to HSL. This is much more effective than naive direct transformation @@ -292,17 +211,13 @@ pub fn rgba_to_hsv( /// * `dst_stride` - Bytes per row for dst data /// * `scale` - Natural range for S and L is [0,1] it may be more convenient and required for u16 transformation to scale it by 100 or any other number to keep S,L in range [0, scale] pub fn rgb_to_hsl( - src: &[u8], - src_stride: u32, - dst: &mut [u16], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u16>, scale: f32, -) { +) -> Result<(), ColorError> { channels_to_hsv_u16::<{ ImageConfiguration::Rgb as u8 }, false, { HsvTarget::Hsl as u8 }>( - src, src_stride, dst, dst_stride, width, height, scale, - ); + src, dst, scale, + ) } /// This function converts BGRA to HSL. Alpha channel is copied and leaved unchanged. This is much more effective than naive direct transformation @@ -316,17 +231,13 @@ pub fn rgb_to_hsl( /// * `dst_stride` - Bytes per row for dst data /// * `scale` - Natural range for S and V is [0,1] it may be more convenient and required for u16 transformation to scale it by 100 or any other number to keep S,V in range [0, scale] pub fn bgra_to_hsl( - src: &[u8], - src_stride: u32, - dst: &mut [u16], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u16>, scale: f32, -) { +) -> Result<(), ColorError> { channels_to_hsv_u16::<{ ImageConfiguration::Bgra as u8 }, true, { HsvTarget::Hsl as u8 }>( - src, src_stride, dst, dst_stride, width, height, scale, - ); + src, dst, scale, + ) } /// This function converts RGBA to HSL. Alpha channel is copied and leaved unchanged. This is much more effective than naive direct transformation @@ -340,15 +251,198 @@ pub fn bgra_to_hsl( /// * `dst_stride` - Bytes per row for dst data /// * `scale` - Natural range for S and V is [0,1] it may be more convenient and required for u16 transformation to scale it by 100 or any other number to keep S,V in range [0, scale] pub fn rgba_to_hsl( - src: &[u8], - src_stride: u32, - dst: &mut [u16], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u16>, scale: f32, -) { +) -> Result<(), ColorError> { channels_to_hsv_u16::<{ ImageConfiguration::Rgba as u8 }, true, { HsvTarget::Hsl as u8 }>( - src, src_stride, dst, dst_stride, width, height, scale, - ); + src, dst, scale, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::BufferStore; + use std::borrow::Cow; + + fn make_src(data: &[u8], width: u32, height: u32, channels: u32) -> ImageBuffer<'static, u8> { + ImageBuffer { + data: Cow::Owned(data.to_vec()), + width, + height, + channels, + stride: width * channels, + } + } + + fn make_dst( + data: &[u16], + width: u32, + height: u32, + channels: u32, + ) -> ImageBufferMut<'static, u16> { + ImageBufferMut { + data: BufferStore::Owned(data.to_vec()), + width, + height, + channels, + stride: width * channels, + } + } + + #[test] + fn test_rgb_to_hsv_red() { + let src_data = [255u8, 0, 0]; + let mut dst_data = [0u16; 3]; + let src = make_src(&src_data, 1, 1, 3); + let mut dst = make_dst(&mut dst_data, 1, 1, 3); + rgb_to_hsv(&src, &mut dst, 100.0).unwrap(); + assert_eq!(dst.data[0], 0); // H = 0 + assert_eq!(dst.data[1], 100); // S = 100% + assert_eq!(dst.data[2], 100); // V = 100% + } + + #[test] + fn test_rgb_to_hsv_green() { + let src_data = [0u8, 255, 0]; + let mut dst_data = [0u16; 3]; + let src = make_src(&src_data, 1, 1, 3); + let mut dst = make_dst(&mut dst_data, 1, 1, 3); + rgb_to_hsv(&src, &mut dst, 100.0).unwrap(); + assert_eq!(dst.data[0], 120); + assert_eq!(dst.data[1], 100); + assert_eq!(dst.data[2], 100); + } + + #[test] + fn test_rgb_to_hsv_blue() { + let src_data = [0u8, 0, 255]; + let mut dst_data = [0u16; 3]; + let src = make_src(&src_data, 1, 1, 3); + let mut dst = make_dst(&mut dst_data, 1, 1, 3); + rgb_to_hsv(&src, &mut dst, 100.0).unwrap(); + assert_eq!(dst.data[0], 240); // H = 240 + assert_eq!(dst.data[1], 100); + assert_eq!(dst.data[2], 100); + } + + #[test] + fn test_rgb_to_hsv_white() { + let src_data = [255u8, 255, 255]; + let mut dst_data = [0u16; 3]; + let src = make_src(&src_data, 1, 1, 3); + let mut dst = make_dst(&mut dst_data, 1, 1, 3); + rgb_to_hsv(&src, &mut dst, 100.0).unwrap(); + assert_eq!(dst.data[1], 0); + assert_eq!(dst.data[2], 100); + } + + #[test] + fn test_rgb_to_hsv_black() { + let src_data = [0u8, 0, 0]; + let mut dst_data = [0u16; 3]; + let src = make_src(&src_data, 1, 1, 3); + let mut dst = make_dst(&mut dst_data, 1, 1, 3); + rgb_to_hsv(&src, &mut dst, 100.0).unwrap(); + assert_eq!(dst.data[1], 0); // S = 0 + assert_eq!(dst.data[2], 0); // V = 0 + } + + #[test] + fn test_rgb_to_hsv_scale() { + let src_data = [255u8, 0, 0]; + let mut dst_data = [0u16; 3]; + let src = make_src(&src_data, 1, 1, 3); + let mut dst = make_dst(&mut dst_data, 1, 1, 3); + rgb_to_hsv(&src, &mut dst, 255.0).unwrap(); + assert_eq!(dst.data[1], 255); // S scaled to 255 + assert_eq!(dst.data[2], 255); // V scaled to 255 + } + + #[test] + fn test_rgba_to_hsv_alpha_preserved() { + let src_data = [255u8, 0, 0, 128]; // red with alpha=128 + let mut dst_data = [0u16; 4]; + let src = make_src(&src_data, 1, 1, 4); + let mut dst = make_dst(&mut dst_data, 1, 1, 4); + rgba_to_hsv(&src, &mut dst, 100.0).unwrap(); + assert_eq!(dst.data[0], 0); // H + assert_eq!(dst.data[1], 100); // S + assert_eq!(dst.data[2], 100); // V + assert_eq!(dst.data[3], 128); + } + + #[test] + fn test_rgb_to_hsl_red() { + let src_data = [255u8, 0, 0]; + let mut dst_data = [0u16; 3]; + let src = make_src(&src_data, 1, 1, 3); + let mut dst = make_dst(&mut dst_data, 1, 1, 3); + rgb_to_hsl(&src, &mut dst, 100.0).unwrap(); + assert_eq!(dst.data[0], 0); + assert_eq!(dst.data[1], 100); + assert_eq!(dst.data[2], 50); + } + + #[test] + fn test_rgb_to_hsl_white() { + let src_data = [255u8, 255, 255]; + let mut dst_data = [0u16; 3]; + let src = make_src(&src_data, 1, 1, 3); + let mut dst = make_dst(&mut dst_data, 1, 1, 3); + rgb_to_hsl(&src, &mut dst, 100.0).unwrap(); + assert_eq!(dst.data[1], 0); // S = 0 + assert_eq!(dst.data[2], 100); // L = 100% + } + + #[test] + fn test_rgb_to_hsl_black() { + let src_data = [0u8, 0, 0]; + let mut dst_data = [0u16; 3]; + let src = make_src(&src_data, 1, 1, 3); + let mut dst = make_dst(&mut dst_data, 1, 1, 3); + rgb_to_hsl(&src, &mut dst, 100.0).unwrap(); + assert_eq!(dst.data[1], 0); // S = 0 + assert_eq!(dst.data[2], 0); // L = 0 + } + + #[test] + fn test_rgba_to_hsl_alpha_preserved() { + let src_data = [0u8, 255, 0, 200]; + let mut dst_data = [0u16; 4]; + let src = make_src(&src_data, 1, 1, 4); + let mut dst = make_dst(&mut dst_data, 1, 1, 4); + rgba_to_hsl(&src, &mut dst, 100.0).unwrap(); + assert_eq!(dst.data[0], 120); + assert_eq!(dst.data[3], 200); + } + + #[test] + fn test_rgb_to_hsv_multi_pixel() { + // red, green, blue side by side + let src_data = [255u8, 0, 0, 0, 255, 0, 0, 0, 255]; + let mut dst_data = [0u16; 9]; + let src = make_src(&src_data, 3, 1, 3); + let mut dst = make_dst(&mut dst_data, 3, 1, 3); + rgb_to_hsv(&src, &mut dst, 100.0).unwrap(); + assert_eq!(dst.data[0], 0); // red H + assert_eq!(dst.data[3], 120); // green H + assert_eq!(dst.data[6], 240); // blue H + } + + // --- bgra_to_hsv --- + + #[test] + fn test_bgra_to_hsv_red() { + let src_data = [0u8, 0, 255, 255]; + let mut dst_data = [0u16; 4]; + let src = make_src(&src_data, 1, 1, 4); + let mut dst = make_dst(&mut dst_data, 1, 1, 4); + bgra_to_hsv(&src, &mut dst, 100.0).unwrap(); + assert_eq!(dst.data[0], 0); // H = 0 (red) + assert_eq!(dst.data[1], 100); // S + assert_eq!(dst.data[2], 100); // V + assert_eq!(dst.data[3], 255); // alpha + } } diff --git a/src/image_to_linear.rs b/src/image_to_linear.rs index af8fec7..6c97f86 100644 --- a/src/image_to_linear.rs +++ b/src/image_to_linear.rs @@ -47,7 +47,7 @@ fn channels_to_linear( { let src = &src[..src_r_width as usize]; let dst = &mut dst[..dst_r_width as usize]; - + if image_configuration == ImageConfiguration::Bgr || image_configuration == ImageConfiguration::Rgb { diff --git a/src/image_to_sigmoidal.rs b/src/image_to_sigmoidal.rs index 801e6f7..955bb25 100644 --- a/src/image_to_sigmoidal.rs +++ b/src/image_to_sigmoidal.rs @@ -171,8 +171,8 @@ pub fn rgba_to_sigmoidal( #[cfg(test)] mod tests { - use crate::BufferStore; use super::*; + use crate::BufferStore; // ── helpers ─────────────────────────────────────────────────────────────── @@ -247,8 +247,6 @@ mod tests { assert!(result.is_err(), "Should fail with 2 channels"); } - // ── rgba_to_sigmoidal ───────────────────────────────────────────────────── - #[test] fn test_rgba_to_sigmoidal_alpha_normalized_full() { // Alpha 255 => 1.0 diff --git a/src/image_to_xyz_lab.rs b/src/image_to_xyz_lab.rs index 9d74a76..5ab8e62 100644 --- a/src/image_to_xyz_lab.rs +++ b/src/image_to_xyz_lab.rs @@ -4,35 +4,35 @@ * // Use of this source code is governed by a BSD-style * // license that can be found in the LICENSE file. */ -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -use crate::avx::avx2_image_to_xyz_lab; use crate::gamma_curves::TransferFunction; use crate::image::ImageConfiguration; -#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -use crate::neon::neon_channels_to_xyz_or_lab; -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -use crate::sse::sse_channels_to_xyz_or_lab; use crate::xyz_target::XyzTarget; -use crate::{LCh, Lab, Luv, Rgb, Xyz, SRGB_TO_XYZ_D65}; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -use std::slice; +use crate::{ + BufferStore, ColorError, ImageBuffer, ImageBufferMut, LCh, Lab, Luv, Rgb, Xyz, SRGB_TO_XYZ_D65, +}; #[allow(clippy::type_complexity)] fn channels_to_xyz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - a_channel: &mut [f32], - a_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, + a_plane: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { + src.validate()?; + dst.validate()?; + dst.try_match_immutable_with_channels(src)?; + if USE_ALPHA { + dst.try_match(a_plane)?; + } + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } + if USE_ALPHA { + if a_plane.channels != 1 { + return Err(ColorError::UnsupportedChannelsCount(a_plane.channels)); + } + } let target: XyzTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); if USE_ALPHA && !image_configuration.has_alpha() { @@ -42,428 +42,183 @@ fn channels_to_xyz usize, + unsafe fn(usize, &[f32], &mut [f32], &mut [f32], u32, &[[f32; 3]; 3]) -> usize, > = None; #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] { + use crate::neon::neon_channels_to_xyz_or_lab; _wide_row_handler = Some(neon_channels_to_xyz_or_lab::); } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] if std::arch::is_x86_feature_detected!("sse4.1") { + use crate::sse::sse_channels_to_xyz_or_lab; _wide_row_handler = Some(sse_channels_to_xyz_or_lab::); } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] if std::arch::is_x86_feature_detected!("avx2") { + use crate::avx::avx2_image_to_xyz_lab; _wide_row_handler = Some(avx2_image_to_xyz_lab::); } - let mut lut_table = [0f32; 256]; + let mut lut_table = [0.; 256]; for (i, lut) in lut_table.iter_mut().enumerate() { *lut = transfer_function.linearize(i as f32 * (1. / 255.0)); } - let dst_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - dst.as_mut_ptr() as *mut u8, - dst_stride as usize * height as usize, - ) - }; - - #[cfg(feature = "rayon")] - { - if USE_ALPHA { - let a_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - a_channel.as_mut_ptr() as *mut u8, - a_stride as usize * height as usize, - ) - }; - - dst_slice_safe_align - .par_chunks_exact_mut(dst_stride as usize) - .zip(src.par_chunks_exact(src_stride as usize)) - .zip(a_slice_safe_align.par_chunks_exact_mut(a_stride as usize)) - .for_each(|((dst, src), a_channel)| unsafe { - let mut _cx = 0usize; - - let mut transient_row = vec![0f32; width as usize * channels]; - - for (dst_chunk, src_chunks) in transient_row - .chunks_exact_mut(channels) - .zip(src.chunks_exact(channels)) - { - dst_chunk[image_configuration.r_index()] = *lut_table - .get_unchecked( - src_chunks[image_configuration.r_index()] as usize, - ); - dst_chunk[image_configuration.g_index()] = *lut_table - .get_unchecked( - src_chunks[image_configuration.g_index()] as usize, - ); - dst_chunk[image_configuration.b_index()] = *lut_table - .get_unchecked( - src_chunks[image_configuration.b_index()] as usize, - ); - dst_chunk[image_configuration.a_index()] = - src_chunks[image_configuration.a_index()] as f32 - * (1. / 255.0); - } - - if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher( - _cx, - transient_row.as_ptr(), - 0, - width, - dst.as_mut_ptr() as *mut f32, - 0, - a_channel.as_mut_ptr() as *mut f32, - 0, - matrix, - ); - } - - let dst_ptr = dst.as_mut_ptr().add(0) as *mut f32; - - for x in _cx..width as usize { - let px = x * channels; - let src = transient_row.get_unchecked(px..); - let r = *src.get_unchecked(image_configuration.r_index()); - let g = *src.get_unchecked(image_configuration.g_index()); - let b = *src.get_unchecked(image_configuration.b_index()); - - let rgb = Rgb::::new(r, g, b); - let ptr = dst_ptr.add(x * 3); - - let xyz = Xyz::from_linear_rgb(rgb, matrix); - - match target { - XyzTarget::Lab => { - let lab = Lab::from_xyz(xyz); - ptr.write_unaligned(lab.l); - ptr.add(1).write_unaligned(lab.a); - ptr.add(2).write_unaligned(lab.b); - } - XyzTarget::Xyz => { - ptr.write_unaligned(xyz.x); - ptr.add(1).write_unaligned(xyz.y); - ptr.add(2).write_unaligned(xyz.z); - } - XyzTarget::Luv => { - let luv = Luv::from_xyz(xyz); - ptr.write_unaligned(luv.l); - ptr.add(1).write_unaligned(luv.u); - ptr.add(2).write_unaligned(luv.v); - } - XyzTarget::Lch => { - let luv = Luv::from_xyz(xyz); - let lch = LCh::from_luv(luv); - ptr.write_unaligned(lch.l); - ptr.add(1).write_unaligned(lch.c); - ptr.add(2).write_unaligned(lch.h); - } - } - - if USE_ALPHA && image_configuration.has_alpha() { - let a = *src.get_unchecked(image_configuration.a_index()); - let a_ptr = a_channel.as_mut_ptr() as *mut f32; - a_ptr.add(x).write_unaligned(a); - } - } - }); - } else { - dst_slice_safe_align - .par_chunks_exact_mut(dst_stride as usize) - .zip(src.par_chunks_exact(src_stride as usize)) - .for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; - - let mut transient_row = vec![0f32; width as usize * channels]; - - for (dst_chunk, src_chunks) in transient_row - .chunks_exact_mut(channels) - .zip(src.chunks_exact(channels)) - { - dst_chunk[image_configuration.r_index()] = *lut_table - .get_unchecked( - src_chunks[image_configuration.r_index()] as usize, - ); - dst_chunk[image_configuration.g_index()] = *lut_table - .get_unchecked( - src_chunks[image_configuration.g_index()] as usize, - ); - dst_chunk[image_configuration.b_index()] = *lut_table - .get_unchecked( - src_chunks[image_configuration.b_index()] as usize, - ); - } - - if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher( - _cx, - transient_row.as_ptr(), - 0, - width, - dst.as_mut_ptr() as *mut f32, - 0, - std::ptr::null_mut(), - 0, - matrix, - ); - } + let mut a_lut_table = [0.; 256]; + for (i, lut) in a_lut_table.iter_mut().enumerate() { + *lut = i as f32 * (1. / 255.0); + } - let dst_ptr = dst.as_mut_ptr().add(0) as *mut f32; + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; + let width = src.width; - for x in _cx..width as usize { - let px = x * channels; - let src = transient_row.get_unchecked(px..); - let r = *src.get_unchecked(image_configuration.r_index()); - let g = *src.get_unchecked(image_configuration.g_index()); - let b = *src.get_unchecked(image_configuration.b_index()); + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) + { + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; - let rgb = Rgb::::new(r, g, b); - let ptr = dst_ptr.add(x * 3); + let mut _cx = 0usize; - let xyz = Xyz::from_linear_rgb(rgb, matrix); + let mut transient_row = vec![0f32; width as usize * channels]; - match target { - XyzTarget::Lab => { - let lab = Lab::from_xyz(xyz); - ptr.write_unaligned(lab.l); - ptr.add(1).write_unaligned(lab.a); - ptr.add(2).write_unaligned(lab.b); - } - XyzTarget::Xyz => { - ptr.write_unaligned(xyz.x); - ptr.add(1).write_unaligned(xyz.y); - ptr.add(2).write_unaligned(xyz.z); - } - XyzTarget::Luv => { - let luv = Luv::from_xyz(xyz); - ptr.write_unaligned(luv.l); - ptr.add(1).write_unaligned(luv.u); - ptr.add(2).write_unaligned(luv.v); - } - XyzTarget::Lch => { - let luv = Luv::from_xyz(xyz); - let lch = LCh::from_luv(luv); - ptr.write_unaligned(lch.l); - ptr.add(1).write_unaligned(lch.c); - ptr.add(2).write_unaligned(lch.h); - } - } - } - }); + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + for (dst, src) in transient_row + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + dst[0] = lut_table[src[0] as usize]; + dst[1] = lut_table[src[1] as usize]; + dst[2] = lut_table[src[2] as usize]; + } + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba + { + for (dst, src) in transient_row + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + dst[0] = lut_table[src[0] as usize]; + dst[1] = lut_table[src[1] as usize]; + dst[2] = lut_table[src[2] as usize]; + dst[3] = a_lut_table[src[3] as usize]; + } } - } - #[cfg(not(feature = "rayon"))] - { - if USE_ALPHA { - let a_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - a_channel.as_mut_ptr() as *mut u8, - a_stride as usize * height as usize, + if let Some(dispatcher) = _wide_row_handler { + _cx = unsafe { + dispatcher( + _cx, + &transient_row, + dst, + a_plane.data.borrow_mut(), + width, + matrix, ) }; + } - for ((dst, src), a_channel) in dst_slice_safe_align - .chunks_exact_mut(dst_stride as usize) - .zip(src.chunks_exact(src_stride as usize)) - .zip(a_slice_safe_align.chunks_exact_mut(a_stride as usize)) + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(transient_row.as_chunks::<3>().0.iter()) { - unsafe { - let mut _cx = 0usize; - - let mut transient_row = vec![0f32; width as usize * channels]; - - for (dst_chunk, src_chunks) in transient_row - .chunks_exact_mut(channels) - .zip(src.chunks_exact(channels)) - { - dst_chunk[image_configuration.get_r_channel_offset()] = *lut_table - .get_unchecked( - src_chunks[image_configuration.get_r_channel_offset()] as usize, - ); - dst_chunk[image_configuration.get_g_channel_offset()] = *lut_table - .get_unchecked( - src_chunks[image_configuration.get_g_channel_offset()] as usize, - ); - dst_chunk[image_configuration.get_b_channel_offset()] = *lut_table - .get_unchecked( - src_chunks[image_configuration.get_b_channel_offset()] as usize, - ); - dst_chunk[image_configuration.get_a_channel_offset()] = - src_chunks[image_configuration.get_a_channel_offset()] as f32 - * (1. / 255.0); + let rgb = Rgb::::new(src[0], src[1], src[2]); + let xyz = Xyz::from_linear_rgb(rgb, matrix); + + match target { + XyzTarget::Lab => { + let lab = Lab::from_xyz(xyz); + dst[0] = lab.l; + dst[1] = lab.a; + dst[2] = lab.b; } - - if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher( - _cx, - transient_row.as_ptr(), - 0, - width, - dst.as_mut_ptr() as *mut f32, - 0, - a_channel.as_mut_ptr() as *mut f32, - 0, - matrix, - ); + XyzTarget::Xyz => { + dst[0] = xyz.x; + dst[1] = xyz.y; + dst[2] = xyz.z; } - - let dst_ptr = dst.as_mut_ptr().add(0) as *mut f32; - - for x in _cx..width as usize { - let px = x * channels; - let src = transient_row.get_unchecked(px..); - let r = *src.get_unchecked(image_configuration.get_r_channel_offset()); - let g = *src.get_unchecked(image_configuration.get_g_channel_offset()); - let b = *src.get_unchecked(image_configuration.get_b_channel_offset()); - - let rgb = Rgb::::new(r, g, b); - let ptr = dst_ptr.add(x * 3); - - let xyz = Xyz::from_linear_rgb(rgb, matrix); - - match target { - XyzTarget::Lab => { - let lab = Lab::from_xyz(xyz); - ptr.write_unaligned(lab.l); - ptr.add(1).write_unaligned(lab.a); - ptr.add(2).write_unaligned(lab.b); - } - XyzTarget::Xyz => { - ptr.write_unaligned(xyz.x); - ptr.add(1).write_unaligned(xyz.y); - ptr.add(2).write_unaligned(xyz.z); - } - XyzTarget::Luv => { - let luv = Luv::from_xyz(xyz); - ptr.write_unaligned(luv.l); - ptr.add(1).write_unaligned(luv.u); - ptr.add(2).write_unaligned(luv.v); - } - XyzTarget::Lch => { - let luv = Luv::from_xyz(xyz); - let lch = LCh::from_luv(luv); - ptr.write_unaligned(lch.l); - ptr.add(1).write_unaligned(lch.c); - ptr.add(2).write_unaligned(lch.h); - } - } - - if USE_ALPHA && image_configuration.has_alpha() { - let a = *src.get_unchecked(image_configuration.get_a_channel_offset()); - let a_ptr = a_channel.as_mut_ptr() as *mut f32; - a_ptr.add(x).write_unaligned(a); - } + XyzTarget::Luv => { + let luv = Luv::from_xyz(xyz); + dst[0] = luv.l; + dst[1] = luv.u; + dst[2] = luv.v; + } + XyzTarget::Lch => { + let luv = Luv::from_xyz(xyz); + let lch = LCh::from_luv(luv); + dst[0] = lch.l; + dst[1] = lch.c; + dst[2] = lch.h; } } } - } else { - for (dst, src) in dst_slice_safe_align - .chunks_exact_mut(dst_stride as usize) - .zip(src.chunks_exact(src_stride as usize)) + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba + { + for ((dst, src), a_dst) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(transient_row.as_chunks::<4>().0.iter()) + .zip(a_plane.data.borrow_mut()) { - unsafe { - let mut _cx = 0usize; - - let mut transient_row = vec![0f32; width as usize * channels]; - - for (dst_chunk, src_chunks) in transient_row - .chunks_exact_mut(channels) - .zip(src.chunks_exact(channels)) - { - dst_chunk[image_configuration.get_r_channel_offset()] = *lut_table - .get_unchecked( - src_chunks[image_configuration.get_r_channel_offset()] as usize, - ); - dst_chunk[image_configuration.get_g_channel_offset()] = *lut_table - .get_unchecked( - src_chunks[image_configuration.get_g_channel_offset()] as usize, - ); - dst_chunk[image_configuration.get_b_channel_offset()] = *lut_table - .get_unchecked( - src_chunks[image_configuration.get_b_channel_offset()] as usize, - ); + let rgb = Rgb::::new(src[0], src[1], src[2]); + let xyz = Xyz::from_linear_rgb(rgb, matrix); + + match target { + XyzTarget::Lab => { + let lab = Lab::from_xyz(xyz); + dst[0] = lab.l; + dst[1] = lab.a; + dst[2] = lab.b; } - - if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher( - _cx, - transient_row.as_ptr(), - 0, - width, - dst.as_mut_ptr() as *mut f32, - 0, - std::ptr::null_mut(), - 0, - matrix, - ); + XyzTarget::Xyz => { + dst[0] = xyz.x; + dst[1] = xyz.y; + dst[2] = xyz.z; } - - let dst_ptr = dst.as_mut_ptr().add(0) as *mut f32; - - for x in _cx..width as usize { - let px = x * channels; - let src = transient_row.get_unchecked(px..); - let r = *src.get_unchecked(image_configuration.get_r_channel_offset()); - let g = *src.get_unchecked(image_configuration.get_g_channel_offset()); - let b = *src.get_unchecked(image_configuration.get_b_channel_offset()); - - let rgb = Rgb::::new(r, g, b); - let ptr = dst_ptr.add(x * 3); - - let xyz = Xyz::from_linear_rgb(rgb, matrix); - - match target { - XyzTarget::Lab => { - let lab = Lab::from_xyz(xyz); - ptr.write_unaligned(lab.l); - ptr.add(1).write_unaligned(lab.a); - ptr.add(2).write_unaligned(lab.b); - } - XyzTarget::Xyz => { - ptr.write_unaligned(xyz.x); - ptr.add(1).write_unaligned(xyz.y); - ptr.add(2).write_unaligned(xyz.z); - } - XyzTarget::Luv => { - let luv = Luv::from_xyz(xyz); - ptr.write_unaligned(luv.l); - ptr.add(1).write_unaligned(luv.u); - ptr.add(2).write_unaligned(luv.v); - } - XyzTarget::Lch => { - let luv = Luv::from_xyz(xyz); - let lch = LCh::from_luv(luv); - ptr.write_unaligned(lch.l); - ptr.add(1).write_unaligned(lch.c); - ptr.add(2).write_unaligned(lch.h); - } - } + XyzTarget::Luv => { + let luv = Luv::from_xyz(xyz); + dst[0] = luv.l; + dst[1] = luv.u; + dst[2] = luv.v; + } + XyzTarget::Lch => { + let luv = Luv::from_xyz(xyz); + let lch = LCh::from_luv(luv); + dst[0] = lch.l; + dst[1] = lch.c; + dst[2] = lch.h; } } + *a_dst = src[3]; } } } + Ok(()) } /// This function converts RGB to XYZ. This is much more effective than naive direct transformation @@ -478,28 +233,25 @@ fn channels_to_xyz, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let mut empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Xyz as u8 }>( src, - src_stride, dst, - dst_stride, - &mut empty_vec, - 0, - width, - height, + &mut ImageBufferMut::new( + BufferStore::Owned(empty_vec), + dst.width, + dst.height, + dst.width, + 1, + )?, matrix, transfer_function, - ); + ) } /// This function converts BGR to XYZ. This is much more effective than naive direct transformation @@ -514,28 +266,25 @@ pub fn rgb_to_xyz( /// * `matrix` - Transformation matrix from BGR to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn bgr_to_xyz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let mut empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Bgr as u8 }, false, { XyzTarget::Xyz as u8 }>( src, - src_stride, dst, - dst_stride, - &mut empty_vec, - 0, - width, - height, + &mut ImageBufferMut::new( + BufferStore::Owned(empty_vec), + dst.width, + dst.height, + dst.width, + 1, + )?, matrix, transfer_function, - ); + ) } /// This function converts sRGB D65 to XYZ. This is much more effective than naive direct transformation @@ -550,26 +299,23 @@ pub fn bgr_to_xyz( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn srgb_to_xyz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, -) { - let mut empty_vec = vec![]; + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, +) -> Result<(), ColorError> { + let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Xyz as u8 }>( src, - src_stride, dst, - dst_stride, - &mut empty_vec, - 0, - width, - height, + &mut ImageBufferMut::new( + BufferStore::Owned(empty_vec), + dst.width, + dst.height, + dst.width, + 1, + )?, &SRGB_TO_XYZ_D65, TransferFunction::Srgb, - ); + ) } /// This function converts RGB to CIE L*ab against D65 white point. This is much more effective than naive direct transformation @@ -584,28 +330,19 @@ pub fn srgb_to_xyz( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn rgb_to_lab( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let mut empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty_vec = vec![0.; 1]; channels_to_xyz::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Lab as u8 }>( src, - src_stride, dst, - dst_stride, - &mut empty_vec, - 0, - width, - height, + &mut ImageBufferMut::new(BufferStore::Owned(empty_vec), 1, 1, 1, 1)?, matrix, transfer_function, - ); + ) } /// This function converts RGBA to XYZ. This is much more effective than naive direct transformation @@ -620,28 +357,19 @@ pub fn rgb_to_lab( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn rgba_to_xyz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let mut empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty_vec = vec![0.; 1]; channels_to_xyz::<{ ImageConfiguration::Rgba as u8 }, false, { XyzTarget::Xyz as u8 }>( src, - src_stride, dst, - dst_stride, - &mut empty_vec, - 0, - width, - height, + &mut ImageBufferMut::new(BufferStore::Owned(empty_vec), 1, 1, 1, 1)?, matrix, transfer_function, - ); + ) } /// This function converts sRGB RGBA D65 to XYZ. This is much more effective than naive direct transformation @@ -656,26 +384,17 @@ pub fn rgba_to_xyz( /// * `matrix` - Transformation matrix from RGBA to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn srgba_to_xyz( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, -) { - let mut empty_vec = vec![]; + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, +) -> Result<(), ColorError> { + let empty_vec = vec![0.; 1]; channels_to_xyz::<{ ImageConfiguration::Rgba as u8 }, false, { XyzTarget::Xyz as u8 }>( src, - src_stride, dst, - dst_stride, - &mut empty_vec, - 0, - width, - height, + &mut ImageBufferMut::new(BufferStore::Owned(empty_vec), 1, 1, 1, 1)?, &SRGB_TO_XYZ_D65, TransferFunction::Srgb, - ); + ) } /// This function converts RGBA to XYZ with preserving and linearizing alpha channels. This is much more effective than naive direct transformation @@ -692,29 +411,19 @@ pub fn srgba_to_xyz( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn rgba_to_xyza( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - a_plane: &mut [f32], - a_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, + a_plane: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_xyz::<{ ImageConfiguration::Rgba as u8 }, true, { XyzTarget::Xyz as u8 }>( src, - src_stride, dst, - dst_stride, a_plane, - a_stride, - width, - height, matrix, transfer_function, - ); + ) } /// This function converts RGBA to XYZ with preserving and linearizing alpha channels. This is much more effective than naive direct transformation @@ -729,27 +438,17 @@ pub fn rgba_to_xyza( /// * `a_plane` - A mutable slice to receive XYZ data /// * `a_stride` - Bytes per row for dst data pub fn srgba_to_xyza( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - a_plane: &mut [f32], - a_stride: u32, - width: u32, - height: u32, -) { + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, + a_plane: &mut ImageBufferMut<'_, f32>, +) -> Result<(), ColorError> { channels_to_xyz::<{ ImageConfiguration::Rgba as u8 }, true, { XyzTarget::Xyz as u8 }>( src, - src_stride, dst, - dst_stride, a_plane, - a_stride, - width, - height, &SRGB_TO_XYZ_D65, TransferFunction::Srgb, - ); + ) } /// This function converts RGBA to CIE L*ab against D65 white point without alpha. This is much more effective than naive direct transformation @@ -766,29 +465,19 @@ pub fn srgba_to_xyza( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn rgba_to_lab( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - a_plane: &mut [f32], - a_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, + a_plane: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_xyz::<{ ImageConfiguration::Rgba as u8 }, false, { XyzTarget::Lab as u8 }>( src, - src_stride, dst, - dst_stride, a_plane, - a_stride, - width, - height, matrix, transfer_function, - ); + ) } /// This function converts RGBA to CIE L*ab against D65 white point and preserving and normalizing alpha channels. This is much more effective than naive direct transformation @@ -805,29 +494,19 @@ pub fn rgba_to_lab( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn rgba_to_laba( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - a_plane: &mut [f32], - a_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, + a_plane: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_xyz::<{ ImageConfiguration::Rgba as u8 }, true, { XyzTarget::Lab as u8 }>( src, - src_stride, dst, - dst_stride, a_plane, - a_stride, - width, - height, matrix, transfer_function, - ); + ) } /// This function converts BGRA to CIE L*ab against D65 white point and preserving and linearizing alpha channels. This is much more effective than naive direct transformation @@ -842,29 +521,19 @@ pub fn rgba_to_laba( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn bgra_to_laba( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - a_plane: &mut [f32], - a_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, + a_plane: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_xyz::<{ ImageConfiguration::Bgra as u8 }, true, { XyzTarget::Lab as u8 }>( src, - src_stride, dst, - dst_stride, a_plane, - a_stride, - width, - height, matrix, transfer_function, - ); + ) } /// This function converts BGR to CIE L*ab against D65 white point. This is much more effective than naive direct transformation @@ -879,28 +548,25 @@ pub fn bgra_to_laba( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn bgr_to_lab( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let mut empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Bgr as u8 }, false, { XyzTarget::Lab as u8 }>( src, - src_stride, dst, - dst_stride, - &mut empty_vec, - 0, - width, - height, + &mut ImageBufferMut::new( + BufferStore::Owned(empty_vec), + dst.width, + dst.height, + dst.width, + 1, + )?, matrix, transfer_function, - ); + ) } /// This function converts RGB to CIE L*uv against D65 white point. This is much more effective than naive direct transformation @@ -915,28 +581,25 @@ pub fn bgr_to_lab( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn rgb_to_luv( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let mut empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Luv as u8 }>( src, - src_stride, dst, - dst_stride, - &mut empty_vec, - 0, - width, - height, + &mut ImageBufferMut::new( + BufferStore::Owned(empty_vec), + dst.width, + dst.height, + dst.width, + 1, + )?, matrix, transfer_function, - ); + ) } /// This function converts BGR to CIE L*ab against D65 white point. This is much more effective than naive direct transformation @@ -951,28 +614,25 @@ pub fn rgb_to_luv( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn bgr_to_luv( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let mut empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Bgr as u8 }, false, { XyzTarget::Luv as u8 }>( src, - src_stride, dst, - dst_stride, - &mut empty_vec, - 0, - width, - height, + &mut ImageBufferMut::new( + BufferStore::Owned(empty_vec), + dst.width, + dst.height, + dst.width, + 1, + )?, matrix, transfer_function, - ); + ) } /// This function converts RGB to CIE L\*C\*h against D65 white point. This is much more effective than naive direct transformation @@ -987,28 +647,25 @@ pub fn bgr_to_luv( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn rgb_to_lch( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let mut empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Lch as u8 }>( src, - src_stride, dst, - dst_stride, - &mut empty_vec, - 0, - width, - height, + &mut ImageBufferMut::new( + BufferStore::Owned(empty_vec), + dst.width, + dst.height, + dst.width, + 1, + )?, matrix, transfer_function, - ); + ) } /// This function converts BGR to CIE L\*C\*h against D65 white point. This is much more effective than naive direct transformation @@ -1023,26 +680,23 @@ pub fn rgb_to_lch( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `SRGB_TO_XYZ_D65` /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn bgr_to_lch( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let mut empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Bgr as u8 }, false, { XyzTarget::Lch as u8 }>( src, - src_stride, dst, - dst_stride, - &mut empty_vec, - 0, - width, - height, + &mut ImageBufferMut::new( + BufferStore::Owned(empty_vec), + dst.width, + dst.height, + dst.width, + 1, + )?, matrix, transfer_function, - ); + ) } diff --git a/src/image_xyza_laba.rs b/src/image_xyza_laba.rs index bf803e2..8dec094 100644 --- a/src/image_xyza_laba.rs +++ b/src/image_xyza_laba.rs @@ -89,12 +89,12 @@ fn channels_to_xyz_with_alpha( start_cx: usize, - src: *const u8, - src_offset: usize, + src: &[u8], width: u32, - dst: *mut u16, - dst_offset: usize, + dst: &mut [u16], scale: f32, ) -> usize { let target: HsvTarget = TARGET.into(); @@ -190,10 +188,8 @@ pub unsafe fn neon_channels_to_hsv_u16< let v_scale = vdupq_n_f32(scale); - let dst_ptr = (dst as *mut u8).add(dst_offset) as *mut u16; - - while cx + 16 < width as usize { - let src_ptr = src.add(src_offset + cx * channels); + while cx + 16 <= width as usize { + let src_ptr = src.get_unchecked(cx * channels); let (r_chan, g_chan, b_chan, a_chan) = load_u8_and_deinterleave!(src_ptr, image_configuration); @@ -236,10 +232,16 @@ pub unsafe fn neon_channels_to_hsv_u16< if USE_ALPHA { let xyz_low_low = uint16x8x4_t(x_low, y_low, z_low, a_low); - vst4q_u16(dst_ptr.add(cx * channels), xyz_low_low); + vst4q_u16( + dst.get_unchecked_mut(cx * channels..).as_mut_ptr(), + xyz_low_low, + ); } else { let xyz_low_low = uint16x8x3_t(x_low, y_low, z_low); - vst3q_u16(dst_ptr.add(cx * channels), xyz_low_low); + vst3q_u16( + dst.get_unchecked_mut(cx * channels..).as_mut_ptr(), + xyz_low_low, + ); } let r_high = vmovl_high_u8(r_chan); @@ -281,17 +283,25 @@ pub unsafe fn neon_channels_to_hsv_u16< if USE_ALPHA { let xyz_low_low = uint16x8x4_t(x_high, y_high, z_high, a_high); - vst4q_u16(dst_ptr.add(cx * channels + 8 * channels), xyz_low_low); + vst4q_u16( + dst.get_unchecked_mut(cx * channels + 8 * channels..) + .as_mut_ptr(), + xyz_low_low, + ); } else { let xyz_low_low = uint16x8x3_t(x_high, y_high, z_high); - vst3q_u16(dst_ptr.add(cx * channels + 8 * channels), xyz_low_low); + vst3q_u16( + dst.get_unchecked_mut(cx * channels + 8 * channels..) + .as_mut_ptr(), + xyz_low_low, + ); } cx += 16; } - while cx + 8 < width as usize { - let src_ptr = src.add(src_offset + cx * channels); + while cx + 8 <= width as usize { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); let (r_chan, g_chan, b_chan, a_chan) = load_u8_and_deinterleave_half!(src_ptr, image_configuration); @@ -335,10 +345,16 @@ pub unsafe fn neon_channels_to_hsv_u16< if USE_ALPHA { let xyz_low_low = uint16x8x4_t(x_low, y_low, z_low, a_low); - vst4q_u16(dst_ptr.add(cx * channels), xyz_low_low); + vst4q_u16( + dst.get_unchecked_mut(cx * channels..).as_mut_ptr(), + xyz_low_low, + ); } else { let xyz_low_low = uint16x8x3_t(x_low, y_low, z_low); - vst3q_u16(dst_ptr.add(cx * channels), xyz_low_low); + vst3q_u16( + dst.get_unchecked_mut(cx * channels..).as_mut_ptr(), + xyz_low_low, + ); } cx += 8; diff --git a/src/neon/mod.rs b/src/neon/mod.rs index 2006ae2..424b552 100644 --- a/src/neon/mod.rs +++ b/src/neon/mod.rs @@ -27,13 +27,13 @@ mod xyza_laba_to_image; pub use colors::*; pub use from_sigmoidal::neon_from_sigmoidal_row; pub use hsv_to_image::*; -pub use image_to_hsv::*; +pub(crate) use image_to_hsv::neon_channels_to_hsv_u16; pub(crate) use image_to_jzazbz::neon_image_to_jzazbz; pub use image_to_oklab::neon_image_to_oklab; pub use jzazbz_to_image::neon_jzazbz_to_image; pub(crate) use oklab_to_image::neon_oklab_to_image; pub(crate) use to_sigmoidal::neon_image_to_sigmoidal; -pub use to_xyz_lab::*; +pub(crate) use to_xyz_lab::neon_channels_to_xyz_or_lab; pub use to_xyza_laba::*; pub use xyz_lab_to_image::*; pub use xyza_laba_to_image::*; diff --git a/src/neon/to_xyz_lab.rs b/src/neon/to_xyz_lab.rs index e10096e..6b70091 100644 --- a/src/neon/to_xyz_lab.rs +++ b/src/neon/to_xyz_lab.rs @@ -14,22 +14,19 @@ use crate::xyz_target::XyzTarget; use std::arch::aarch64::*; #[inline(always)] -pub unsafe fn neon_channels_to_xyz_or_lab< +pub(crate) unsafe fn neon_channels_to_xyz_or_lab< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, const TARGET: u8, >( start_cx: usize, - src: *const f32, - src_offset: usize, + src: &[f32], + dst: &mut [f32], + a_linearized: &mut [f32], width: u32, - dst: *mut f32, - dst_offset: usize, - a_linearized: *mut f32, - a_offset: usize, matrix: &[[f32; 3]; 3], ) -> usize { - if USE_ALPHA && a_linearized.is_null() { + if USE_ALPHA && a_linearized.is_empty() { panic!("Null alpha channel with requirements of linearized alpha if not supported"); } let target: XyzTarget = TARGET.into(); @@ -47,10 +44,8 @@ pub unsafe fn neon_channels_to_xyz_or_lab< let cq8 = vdupq_n_f32(*matrix.get_unchecked(2).get_unchecked(1)); let cq9 = vdupq_n_f32(*matrix.get_unchecked(2).get_unchecked(2)); - let dst_ptr = (dst as *mut u8).add(dst_offset) as *mut f32; - - while cx + 4 < width as usize { - let src_ptr = ((src as *const u8).add(src_offset) as *const f32).add(cx * channels); + while cx + 4 <= width as usize { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); let (r_chan, g_chan, b_chan, a_chan) = load_f32_and_deinterleave!(src_ptr, image_configuration); @@ -81,12 +76,10 @@ pub unsafe fn neon_channels_to_xyz_or_lab< } let xyz_low_low = float32x4x3_t(x_low_low, y_low_low, z_low_low); - vst3q_f32(dst_ptr.add(cx * 3), xyz_low_low); + vst3q_f32(dst.get_unchecked_mut(cx * 3..).as_mut_ptr(), xyz_low_low); if USE_ALPHA { - let a_ptr = (a_linearized as *mut u8).add(a_offset) as *mut f32; - - vst1q_f32(a_ptr.add(cx), a_chan); + vst1q_f32(a_linearized.get_unchecked_mut(cx..).as_mut_ptr(), a_chan); } cx += 4; diff --git a/src/sse/cie.rs b/src/sse/cie.rs index d62e30e..916214d 100644 --- a/src/sse/cie.rs +++ b/src/sse/cie.rs @@ -16,8 +16,9 @@ use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; -#[inline(always)] -pub unsafe fn sse_triple_to_xyz( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_triple_to_xyz( r: __m128, g: __m128, b: __m128, @@ -35,129 +36,145 @@ pub unsafe fn sse_triple_to_xyz( (x, y, z) } -#[inline(always)] -pub unsafe fn sse_triple_to_luv(x: __m128, y: __m128, z: __m128) -> (__m128, __m128, __m128) { - let zeros = _mm_setzero_ps(); - let den = _mm_prefer_fma_ps( - _mm_prefer_fma_ps(x, z, _mm_set1_ps(3f32)), - y, - _mm_set1_ps(15f32), - ); - let nan_mask = _mm_cmpeq_ps(den, _mm_set1_ps(0f32)); - let l_low_mask = _mm_cmplt_ps(y, _mm_set1_ps(LUV_CUTOFF_FORWARD_Y)); - let y_cbrt = _mm_cbrt_fast_ps(y); - let l = _mm_select_ps( - l_low_mask, - _mm_mul_ps(y, _mm_set1_ps(LUV_MULTIPLIER_FORWARD_Y)), - _mm_prefer_fma_ps(_mm_set1_ps(-16f32), y_cbrt, _mm_set1_ps(116f32)), - ); - let u_prime = _mm_div_ps(_mm_mul_ps(x, _mm_set1_ps(4f32)), den); - let v_prime = _mm_div_ps(_mm_mul_ps(y, _mm_set1_ps(9f32)), den); - let sub_u_prime = _mm_sub_ps(u_prime, _mm_set1_ps(crate::luv::LUV_WHITE_U_PRIME)); - let sub_v_prime = _mm_sub_ps(v_prime, _mm_set1_ps(crate::luv::LUV_WHITE_V_PRIME)); - let l13 = _mm_mul_ps(l, _mm_set1_ps(13f32)); - let u = _mm_select_ps(nan_mask, zeros, _mm_mul_ps(l13, sub_u_prime)); - let v = _mm_select_ps(nan_mask, zeros, _mm_mul_ps(l13, sub_v_prime)); - (l, u, v) +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_triple_to_luv(x: __m128, y: __m128, z: __m128) -> (__m128, __m128, __m128) { + unsafe { + let zeros = _mm_setzero_ps(); + let den = _mm_prefer_fma_ps( + _mm_prefer_fma_ps(x, z, _mm_set1_ps(3f32)), + y, + _mm_set1_ps(15f32), + ); + let nan_mask = _mm_cmpeq_ps(den, _mm_set1_ps(0f32)); + let l_low_mask = _mm_cmplt_ps(y, _mm_set1_ps(LUV_CUTOFF_FORWARD_Y)); + let y_cbrt = _mm_cbrt_fast_ps(y); + let l = _mm_select_ps( + l_low_mask, + _mm_mul_ps(y, _mm_set1_ps(LUV_MULTIPLIER_FORWARD_Y)), + _mm_prefer_fma_ps(_mm_set1_ps(-16f32), y_cbrt, _mm_set1_ps(116f32)), + ); + let u_prime = _mm_div_ps(_mm_mul_ps(x, _mm_set1_ps(4f32)), den); + let v_prime = _mm_div_ps(_mm_mul_ps(y, _mm_set1_ps(9f32)), den); + let sub_u_prime = _mm_sub_ps(u_prime, _mm_set1_ps(crate::luv::LUV_WHITE_U_PRIME)); + let sub_v_prime = _mm_sub_ps(v_prime, _mm_set1_ps(crate::luv::LUV_WHITE_V_PRIME)); + let l13 = _mm_mul_ps(l, _mm_set1_ps(13f32)); + let u = _mm_select_ps(nan_mask, zeros, _mm_mul_ps(l13, sub_u_prime)); + let v = _mm_select_ps(nan_mask, zeros, _mm_mul_ps(l13, sub_v_prime)); + (l, u, v) + } } -#[inline(always)] -pub unsafe fn sse_triple_to_lab(x: __m128, y: __m128, z: __m128) -> (__m128, __m128, __m128) { - let x = _mm_mul_ps(x, _mm_set1_ps(100f32 / 95.047f32)); - let z = _mm_mul_ps(z, _mm_set1_ps(100f32 / 108.883f32)); - let cbrt_x = _mm_cbrt_fast_ps(x); - let cbrt_y = _mm_cbrt_fast_ps(y); - let cbrt_z = _mm_cbrt_fast_ps(z); - let s_1 = _mm_set1_ps(16.0 / 116.0); - let s_2 = _mm_set1_ps(7.787); - let lower_x = _mm_prefer_fma_ps(s_1, s_2, x); - let lower_y = _mm_prefer_fma_ps(s_1, s_2, y); - let lower_z = _mm_prefer_fma_ps(s_1, s_2, z); - let cutoff = _mm_set1_ps(0.008856f32); - let x = _mm_select_ps(_mm_cmpgt_ps(x, cutoff), cbrt_x, lower_x); - let y = _mm_select_ps(_mm_cmpgt_ps(y, cutoff), cbrt_y, lower_y); - let z = _mm_select_ps(_mm_cmpgt_ps(z, cutoff), cbrt_z, lower_z); - let l = _mm_prefer_fma_ps(_mm_set1_ps(-16.0f32), y, _mm_set1_ps(116.0f32)); - let a = _mm_mul_ps(_mm_sub_ps(x, y), _mm_set1_ps(500f32)); - let b = _mm_mul_ps(_mm_sub_ps(y, z), _mm_set1_ps(200f32)); - (l, a, b) +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_triple_to_lab(x: __m128, y: __m128, z: __m128) -> (__m128, __m128, __m128) { + unsafe { + let x = _mm_mul_ps(x, _mm_set1_ps(100f32 / 95.047f32)); + let z = _mm_mul_ps(z, _mm_set1_ps(100f32 / 108.883f32)); + let cbrt_x = _mm_cbrt_fast_ps(x); + let cbrt_y = _mm_cbrt_fast_ps(y); + let cbrt_z = _mm_cbrt_fast_ps(z); + let s_1 = _mm_set1_ps(16.0 / 116.0); + let s_2 = _mm_set1_ps(7.787); + let lower_x = _mm_prefer_fma_ps(s_1, s_2, x); + let lower_y = _mm_prefer_fma_ps(s_1, s_2, y); + let lower_z = _mm_prefer_fma_ps(s_1, s_2, z); + let cutoff = _mm_set1_ps(0.008856f32); + let x = _mm_select_ps(_mm_cmpgt_ps(x, cutoff), cbrt_x, lower_x); + let y = _mm_select_ps(_mm_cmpgt_ps(y, cutoff), cbrt_y, lower_y); + let z = _mm_select_ps(_mm_cmpgt_ps(z, cutoff), cbrt_z, lower_z); + let l = _mm_prefer_fma_ps(_mm_set1_ps(-16.0f32), y, _mm_set1_ps(116.0f32)); + let a = _mm_mul_ps(_mm_sub_ps(x, y), _mm_set1_ps(500f32)); + let b = _mm_mul_ps(_mm_sub_ps(y, z), _mm_set1_ps(200f32)); + (l, a, b) + } } -#[inline(always)] -pub unsafe fn sse_triple_to_lch(x: __m128, y: __m128, z: __m128) -> (__m128, __m128, __m128) { - let (luv_l, luv_u, luv_v) = sse_triple_to_luv(x, y, z); - let lch_c = _mm_hypot_ps(luv_u, luv_v); - let lch_h = _mm_atan2_ps(luv_v, luv_u); - (luv_l, lch_c, lch_h) +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_triple_to_lch(x: __m128, y: __m128, z: __m128) -> (__m128, __m128, __m128) { + unsafe { + let (luv_l, luv_u, luv_v) = sse_triple_to_luv(x, y, z); + let lch_c = _mm_hypot_ps(luv_u, luv_v); + let lch_h = _mm_atan2_ps(luv_v, luv_u); + (luv_l, lch_c, lch_h) + } } -#[inline(always)] -pub unsafe fn sse_lab_to_xyz(l: __m128, a: __m128, b: __m128) -> (__m128, __m128, __m128) { - let y = _mm_mul_ps( - _mm_add_ps(l, _mm_set1_ps(16f32)), - _mm_set1_ps(1f32 / 116f32), - ); - let x = _mm_add_ps(_mm_mul_ps(a, _mm_set1_ps(1f32 / 500f32)), y); - let z = _mm_sub_ps(y, _mm_mul_ps(b, _mm_set1_ps(1f32 / 200f32))); - let x3 = _mm_cube_ps(x); - let y3 = _mm_cube_ps(y); - let z3 = _mm_cube_ps(z); - let kappa = _mm_set1_ps(0.008856f32); - let k_sub = _mm_set1_ps(16f32 / 116f32); - let mult_1 = _mm_set1_ps(1f32 / 7.787f32); - let low_x = _mm_mul_ps(_mm_sub_ps(x, k_sub), mult_1); - let low_y = _mm_mul_ps(_mm_sub_ps(y, k_sub), mult_1); - let low_z = _mm_mul_ps(_mm_sub_ps(z, k_sub), mult_1); +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_lab_to_xyz(l: __m128, a: __m128, b: __m128) -> (__m128, __m128, __m128) { + unsafe { + let y = _mm_mul_ps( + _mm_add_ps(l, _mm_set1_ps(16f32)), + _mm_set1_ps(1f32 / 116f32), + ); + let x = _mm_add_ps(_mm_mul_ps(a, _mm_set1_ps(1f32 / 500f32)), y); + let z = _mm_sub_ps(y, _mm_mul_ps(b, _mm_set1_ps(1f32 / 200f32))); + let x3 = _mm_cube_ps(x); + let y3 = _mm_cube_ps(y); + let z3 = _mm_cube_ps(z); + let kappa = _mm_set1_ps(0.008856f32); + let k_sub = _mm_set1_ps(16f32 / 116f32); + let mult_1 = _mm_set1_ps(1f32 / 7.787f32); + let low_x = _mm_mul_ps(_mm_sub_ps(x, k_sub), mult_1); + let low_y = _mm_mul_ps(_mm_sub_ps(y, k_sub), mult_1); + let low_z = _mm_mul_ps(_mm_sub_ps(z, k_sub), mult_1); - let x = _mm_select_ps(_mm_cmpgt_ps(x3, kappa), x3, low_x); - let y = _mm_select_ps(_mm_cmpgt_ps(y3, kappa), y3, low_y); - let z = _mm_select_ps(_mm_cmpgt_ps(z3, kappa), z3, low_z); - let x = _mm_mul_ps(x, _mm_set1_ps(95.047f32 / 100f32)); - let z = _mm_mul_ps(z, _mm_set1_ps(108.883f32 / 100f32)); - (x, y, z) + let x = _mm_select_ps(_mm_cmpgt_ps(x3, kappa), x3, low_x); + let y = _mm_select_ps(_mm_cmpgt_ps(y3, kappa), y3, low_y); + let z = _mm_select_ps(_mm_cmpgt_ps(z3, kappa), z3, low_z); + let x = _mm_mul_ps(x, _mm_set1_ps(95.047f32 / 100f32)); + let z = _mm_mul_ps(z, _mm_set1_ps(108.883f32 / 100f32)); + (x, y, z) + } } -#[inline(always)] -pub unsafe fn sse_luv_to_xyz(l: __m128, u: __m128, v: __m128) -> (__m128, __m128, __m128) { - let zeros = _mm_setzero_ps(); - let zero_mask = _mm_cmpeq_ps(l, zeros); - let l13 = _mm_rcp_ps(_mm_mul_ps(l, _mm_set1_ps(13f32))); - let u = _mm_prefer_fma_ps(_mm_set1_ps(LUV_WHITE_U_PRIME), l13, u); - let v = _mm_prefer_fma_ps(_mm_set1_ps(LUV_WHITE_V_PRIME), l13, v); - let l_h = _mm_mul_ps( - _mm_add_ps(l, _mm_set1_ps(16f32)), - _mm_set1_ps(1f32 / 116f32), - ); - let y_high = _mm_mul_ps(_mm_mul_ps(l_h, l_h), l_h); - let y_low = _mm_mul_ps(l, _mm_set1_ps(LUV_MULTIPLIER_INVERSE_Y)); - let y = _mm_select_ps( - zero_mask, - zeros, - _mm_select_ps(_mm_cmpgt_ps(l, _mm_set1_ps(8f32)), y_high, y_low), - ); - let zero_mask_2 = _mm_cmpeq_ps(v, zeros); - let den = _mm_rcp_ps(_mm_mul_ps(v, _mm_set1_ps(4f32))); - let mut x = _mm_mul_ps(_mm_mul_ps(_mm_mul_ps(y, u), den), _mm_set1_ps(9f32)); - x = _mm_select_ps(zero_mask, zeros, x); - x = _mm_select_ps(zero_mask_2, zeros, x); - let mut z = _mm_mul_ps( - _mm_mul_ps( - _mm_prefer_fma_ps( - _mm_prefer_fma_ps(_mm_set1_ps(12f32), _mm_set1_ps(-3f32), u), - v, - _mm_set1_ps(-20f32), +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_luv_to_xyz(l: __m128, u: __m128, v: __m128) -> (__m128, __m128, __m128) { + unsafe { + let zeros = _mm_setzero_ps(); + let zero_mask = _mm_cmpeq_ps(l, zeros); + let l13 = _mm_rcp_ps(_mm_mul_ps(l, _mm_set1_ps(13f32))); + let u = _mm_prefer_fma_ps(_mm_set1_ps(LUV_WHITE_U_PRIME), l13, u); + let v = _mm_prefer_fma_ps(_mm_set1_ps(LUV_WHITE_V_PRIME), l13, v); + let l_h = _mm_mul_ps( + _mm_add_ps(l, _mm_set1_ps(16f32)), + _mm_set1_ps(1f32 / 116f32), + ); + let y_high = _mm_mul_ps(_mm_mul_ps(l_h, l_h), l_h); + let y_low = _mm_mul_ps(l, _mm_set1_ps(LUV_MULTIPLIER_INVERSE_Y)); + let y = _mm_select_ps( + zero_mask, + zeros, + _mm_select_ps(_mm_cmpgt_ps(l, _mm_set1_ps(8f32)), y_high, y_low), + ); + let zero_mask_2 = _mm_cmpeq_ps(v, zeros); + let den = _mm_rcp_ps(_mm_mul_ps(v, _mm_set1_ps(4f32))); + let mut x = _mm_mul_ps(_mm_mul_ps(_mm_mul_ps(y, u), den), _mm_set1_ps(9f32)); + x = _mm_select_ps(zero_mask, zeros, x); + x = _mm_select_ps(zero_mask_2, zeros, x); + let mut z = _mm_mul_ps( + _mm_mul_ps( + _mm_prefer_fma_ps( + _mm_prefer_fma_ps(_mm_set1_ps(12f32), _mm_set1_ps(-3f32), u), + v, + _mm_set1_ps(-20f32), + ), + y, ), - y, - ), - den, - ); - z = _mm_select_ps(zero_mask, zeros, z); - z = _mm_select_ps(zero_mask_2, zeros, z); - (x, y, z) + den, + ); + z = _mm_select_ps(zero_mask, zeros, z); + z = _mm_select_ps(zero_mask_2, zeros, z); + (x, y, z) + } } -#[inline(always)] -pub unsafe fn sse_lch_to_xyz(l: __m128, c: __m128, h: __m128) -> (__m128, __m128, __m128) { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) unsafe fn sse_lch_to_xyz(l: __m128, c: __m128, h: __m128) -> (__m128, __m128, __m128) { let u = _mm_mul_ps(c, _mm_cos_ps(h)); let v = _mm_mul_ps(c, _mm_sin_ps(h)); sse_luv_to_xyz(l, u, v) diff --git a/src/sse/image_to_hsv.rs b/src/sse/image_to_hsv.rs index a247a1a..59f2902 100644 --- a/src/sse/image_to_hsv.rs +++ b/src/sse/image_to_hsv.rs @@ -25,11 +25,9 @@ pub unsafe fn sse_channels_to_hsv_u16< const TARGET: u8, >( start_cx: usize, - src: *const u8, - src_offset: usize, + src: &[u8], width: u32, - dst: *mut u16, - dst_offset: usize, + dst: &mut [u16], scale: f32, ) -> usize { let target: HsvTarget = TARGET.into(); @@ -43,10 +41,8 @@ pub unsafe fn sse_channels_to_hsv_u16< let v_scale = _mm_set1_ps(scale); - let dst_ptr = (dst as *mut u8).add(dst_offset) as *mut u16; - - while cx + 16 < width as usize { - let src_ptr = src.add(src_offset + cx * channels); + while cx + 16 <= width as usize { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); let (r_chan, g_chan, b_chan, a_chan) = load_u8_and_deinterleave!(src_ptr, image_configuration); @@ -91,10 +87,10 @@ pub unsafe fn sse_channels_to_hsv_u16< ); if USE_ALPHA { - let ptr = dst_ptr.add(cx * channels); + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); store_and_interleave_v4_u16!(ptr, x_low, y_low, z_low, a_low); } else { - let ptr = dst_ptr.add(cx * channels); + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); store_and_interleave_v3_u16!(ptr, x_low, y_low, z_low); } @@ -136,10 +132,14 @@ pub unsafe fn sse_channels_to_hsv_u16< ); if USE_ALPHA { - let ptr = dst_ptr.add(cx * channels + 8 * channels); + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels..) + .as_mut_ptr(); store_and_interleave_v4_u16!(ptr, x_high, y_high, z_high, a_high); } else { - let ptr = dst_ptr.add(cx * channels + 8 * channels); + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels..) + .as_mut_ptr(); store_and_interleave_v3_u16!(ptr, x_high, y_high, z_high); } diff --git a/src/sse/math.rs b/src/sse/math.rs index 83bf978..22981b0 100644 --- a/src/sse/math.rs +++ b/src/sse/math.rs @@ -125,8 +125,9 @@ pub unsafe fn _mm_cmplt_epi32(a: __m128i, b: __m128i) -> __m128i { _mm_cmpgt_epi32(b, a) } -#[inline(always)] -pub unsafe fn _mm_color_matrix_ps( +#[inline] +#[target_feature(enable = "sse4.1")] +pub fn _mm_color_matrix_ps( r: __m128, g: __m128, b: __m128, @@ -140,8 +141,10 @@ pub unsafe fn _mm_color_matrix_ps( c8: __m128, c9: __m128, ) -> (__m128, __m128, __m128) { - let new_r = _mm_prefer_fma_ps(_mm_prefer_fma_ps(_mm_mul_ps(g, c2), b, c3), r, c1); - let new_g = _mm_prefer_fma_ps(_mm_prefer_fma_ps(_mm_mul_ps(g, c5), b, c6), r, c4); - let new_b = _mm_prefer_fma_ps(_mm_prefer_fma_ps(_mm_mul_ps(g, c8), b, c9), r, c7); - (new_r, new_g, new_b) + unsafe { + let new_r = _mm_prefer_fma_ps(_mm_prefer_fma_ps(_mm_mul_ps(g, c2), b, c3), r, c1); + let new_g = _mm_prefer_fma_ps(_mm_prefer_fma_ps(_mm_mul_ps(g, c5), b, c6), r, c4); + let new_b = _mm_prefer_fma_ps(_mm_prefer_fma_ps(_mm_mul_ps(g, c8), b, c9), r, c7); + (new_r, new_g, new_b) + } } diff --git a/src/sse/mod.rs b/src/sse/mod.rs index 5e73b86..d8d203f 100644 --- a/src/sse/mod.rs +++ b/src/sse/mod.rs @@ -44,7 +44,7 @@ pub use math::*; pub(crate) use oklab_to_image::sse_oklab_to_image; pub use support::*; pub(crate) use to_sigmoidal::sse_image_to_sigmoidal_row; -pub use to_xyz_lab::*; +pub(crate) use to_xyz_lab::sse_channels_to_xyz_or_lab; pub use to_xyza_laba::*; pub use xyz_lab_to_image::*; pub use xyza_laba_to_image::*; diff --git a/src/sse/support.rs b/src/sse/support.rs index b75f96e..a146c90 100644 --- a/src/sse/support.rs +++ b/src/sse/support.rs @@ -34,8 +34,9 @@ pub unsafe fn sse_interleave_rgba( (rgba_0_lo, rgba_0_hi, rgba_1_lo, rgba_1_hi) } -#[inline(always)] -pub unsafe fn sse_interleave_ps_rgb(a: __m128, b: __m128, c: __m128) -> (__m128, __m128, __m128) { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_interleave_ps_rgb(a: __m128, b: __m128, c: __m128) -> (__m128, __m128, __m128) { const MASK_U0: i32 = shuffle(0, 0, 0, 0); let u0 = _mm_shuffle_ps::(a, b); const MASK_U1: i32 = shuffle(1, 1, 0, 0); diff --git a/src/sse/to_xyz_lab.rs b/src/sse/to_xyz_lab.rs index a4c45fe..742858a 100644 --- a/src/sse/to_xyz_lab.rs +++ b/src/sse/to_xyz_lab.rs @@ -16,22 +16,19 @@ use std::arch::x86::*; use std::arch::x86_64::*; #[target_feature(enable = "sse4.1")] -pub unsafe fn sse_channels_to_xyz_or_lab< +pub(crate) unsafe fn sse_channels_to_xyz_or_lab< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, const TARGET: u8, >( start_cx: usize, - src: *const f32, - src_offset: usize, + src: &[f32], + dst: &mut [f32], + a_linearized: &mut [f32], width: u32, - dst: *mut f32, - dst_offset: usize, - a_linearized: *mut f32, - a_offset: usize, matrix: &[[f32; 3]; 3], ) -> usize { - if USE_ALPHA && a_linearized.is_null() { + if USE_ALPHA && a_linearized.is_empty() { panic!("Null alpha channel with requirements of linearized alpha if not supported"); } let target: XyzTarget = TARGET.into(); @@ -49,10 +46,8 @@ pub unsafe fn sse_channels_to_xyz_or_lab< let cq8 = _mm_set1_ps(*matrix.get_unchecked(2).get_unchecked(1)); let cq9 = _mm_set1_ps(*matrix.get_unchecked(2).get_unchecked(2)); - let dst_ptr = (dst as *mut u8).add(dst_offset) as *mut f32; - - while cx + 4 < width as usize { - let src_ptr = ((src as *const u8).add(src_offset) as *const f32).add(cx * channels); + while cx + 4 <= width as usize { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); let (r_chan, g_chan, b_chan, a_chan) = load_f32_and_deinterleave!(src_ptr, image_configuration); @@ -83,14 +78,12 @@ pub unsafe fn sse_channels_to_xyz_or_lab< } let (v0, v1, v2) = sse_interleave_ps_rgb(x_low_low, y_low_low, z_low_low); - _mm_storeu_ps(dst_ptr.add(cx * 3), v0); - _mm_storeu_ps(dst_ptr.add(cx * 3 + 4), v1); - _mm_storeu_ps(dst_ptr.add(cx * 3 + 8), v2); + _mm_storeu_ps(dst.get_unchecked_mut(cx * 3), v0); + _mm_storeu_ps(dst.get_unchecked_mut(cx * 3 + 4), v1); + _mm_storeu_ps(dst.get_unchecked_mut(cx * 3 + 8), v2); if USE_ALPHA { - let a_ptr = (a_linearized as *mut u8).add(a_offset) as *mut f32; - - _mm_storeu_ps(a_ptr.add(cx), a_chan); + _mm_storeu_ps(a_linearized.get_unchecked_mut(cx), a_chan); } cx += 4; diff --git a/src/xyz_lab_to_image.rs b/src/xyz_lab_to_image.rs index 7c6e709..e4e280c 100644 --- a/src/xyz_lab_to_image.rs +++ b/src/xyz_lab_to_image.rs @@ -148,8 +148,7 @@ fn xyz_to_channels Date: Sat, 11 Apr 2026 17:09:51 +0100 Subject: [PATCH 4/9] API improvements --- Cargo.toml | 2 +- src/app/src/main.rs | 37 +- src/avx/cie.rs | 156 ++-- src/avx/from_sigmoidal.rs | 101 ++- src/avx/gamma_curves.rs | 150 ---- src/avx/image_to_oklab.rs | 46 +- src/avx/math.rs | 51 +- src/avx/mod.rs | 11 +- src/avx/oklab_to_image.rs | 69 +- src/avx/routines.rs | 197 +++-- src/avx/sigmoidal.rs | 54 +- src/avx/support.rs | 102 ++- src/avx/to_sigmoidal.rs | 324 +++---- src/avx/to_xyz_lab.rs | 180 ++-- src/avx/utils.rs | 7 +- src/avx/xyz_lab_to_image.rs | 186 ++-- src/avx/xyza_laba_to_image.rs | 146 +-- src/image_to_jzazbz.rs | 4 +- src/image_to_lalphabeta.rs | 2 +- src/image_to_linear.rs | 2 +- src/image_to_oklab.rs | 6 +- src/image_to_xyz_lab.rs | 385 ++++++-- src/image_xyza_laba.rs | 734 +++++++++++----- src/jzazbz.rs | 10 +- src/jzazbz_to_image.rs | 830 +++++++++++++----- src/jzczhz.rs | 2 +- src/lab.rs | 2 +- src/lalphabeta.rs | 2 +- src/lalphabeta_to_image.rs | 262 +++--- src/lib.rs | 2 +- src/linear_to_image.rs | 545 +++++++++--- src/linear_to_image_u8.rs | 461 +++++++--- src/linear_to_planar.rs | 289 ++++-- src/neon/cie.rs | 137 +-- src/neon/colors.rs | 94 +- src/neon/from_sigmoidal.rs | 189 ++-- src/neon/gamma_curves.rs | 132 --- src/neon/image_to_jzazbz.rs | 36 +- src/neon/image_to_oklab.rs | 134 +-- src/neon/jzazbz_to_image.rs | 168 ++-- src/neon/math.rs | 31 +- src/neon/mod.rs | 15 +- src/neon/oklab_to_image.rs | 95 +- src/neon/to_xyz_lab.rs | 82 +- src/neon/to_xyza_laba.rs | 40 +- src/neon/xyz_lab_to_image.rs | 111 +-- src/neon/xyza_laba_to_image.rs | 46 +- src/oklab_to_image.rs | 86 +- src/oklch.rs | 2 +- src/planar_to_linear.rs | 274 ++++-- src/rgb.rs | 14 +- src/rgb_expand.rs | 186 +--- src/rgba.rs | 8 +- src/routines.rs | 38 +- src/sigmoidal_to_image.rs | 498 ++++++++--- src/sse/cie.rs | 124 ++- src/sse/color.rs | 528 +++++------ src/sse/from_sigmoidal.rs | 193 ++-- src/sse/gamma_curves.rs | 145 --- src/sse/image_to_hsv.rs | 200 ++--- src/sse/image_to_jzazbz.rs | 32 +- src/sse/image_to_oklab.rs | 108 +-- src/sse/jzazbz_to_image.rs | 105 ++- src/sse/math.rs | 96 +- src/sse/mod.rs | 14 +- src/sse/oklab_to_image.rs | 86 +- src/sse/sigmoidal.rs | 22 +- src/sse/support.rs | 123 +-- src/sse/to_sigmoidal.rs | 440 +++++----- src/sse/to_xyz_lab.rs | 85 +- src/sse/to_xyza_laba.rs | 44 +- src/sse/xyz_lab_to_image.rs | 137 +-- src/sse/xyza_laba_to_image.rs | 117 +-- src/xyz_lab_to_image.rs | 1511 +++++++++++++++++--------------- src/xyza_laba_to_image.rs | 393 +++------ 75 files changed, 6928 insertions(+), 5348 deletions(-) delete mode 100644 src/avx/gamma_curves.rs delete mode 100644 src/neon/gamma_curves.rs delete mode 100644 src/sse/gamma_curves.rs diff --git a/Cargo.toml b/Cargo.toml index 2dd46a5..a56dfb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ workspace = { members = ["src/app"] } [package] name = "colorutils-rs" version = "0.7.6" -edition = "2021" +edition = "2024" description = "High performance utilities for color format handling and conversion." readme = "README.md" keywords = ["lab", "hsv", "xyz", "color", "colorspace"] diff --git a/src/app/src/main.rs b/src/app/src/main.rs index 0866d0d..368c9bc 100644 --- a/src/app/src/main.rs +++ b/src/app/src/main.rs @@ -64,19 +64,22 @@ fn main() { { let mut lab_store: Vec = vec![]; - let store_stride = width as usize * components * std::mem::size_of::(); lab_store.resize(width as usize * components * height as usize, 0.); let src_stride = width * components as u32; let start_time = Instant::now(); rgba_to_oklab( - src_bytes, - src_stride, - &mut lab_store, - store_stride as u32, - width, - height, + &ImageBuffer::new(src_bytes, width, height, src_stride, 4).unwrap(), + &mut ImageBufferMut::new( + BufferStore::Borrowed(&mut lab_store), + width, + height, + src_stride, + 4, + ) + .unwrap(), TransferFunction::Srgb, - ); + ) + .unwrap(); let elapsed_time = start_time.elapsed(); // Print the elapsed time in milliseconds println!("RGBA To HSV: {:.2?}", elapsed_time); @@ -104,14 +107,18 @@ fn main() { let start_time = Instant::now(); oklab_to_rgba( - &lab_store, - store_stride as u32, - &mut dst_slice, - src_stride, - width, - height, + &ImageBuffer::new(&lab_store, width, height, src_stride, 4).unwrap(), + &mut ImageBufferMut::new( + BufferStore::Borrowed(&mut dst_slice), + width, + height, + src_stride, + 4, + ) + .unwrap(), TransferFunction::Srgb, - ); + ) + .unwrap(); let elapsed_time = start_time.elapsed(); // Print the elapsed time in milliseconds diff --git a/src/avx/cie.rs b/src/avx/cie.rs index 747a3a0..7e3bc5e 100644 --- a/src/avx/cie.rs +++ b/src/avx/cie.rs @@ -20,87 +20,97 @@ use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; -#[inline(always)] -pub(crate) unsafe fn avx_lab_to_xyz(l: __m256, a: __m256, b: __m256) -> (__m256, __m256, __m256) { - let y = _mm256_mul_ps( - _mm256_add_ps(l, _mm256_set1_ps(16f32)), - _mm256_set1_ps(1f32 / 116f32), - ); - let x = _mm256_add_ps(_mm256_mul_ps(a, _mm256_set1_ps(1f32 / 500f32)), y); - let z = _mm256_sub_ps(y, _mm256_mul_ps(b, _mm256_set1_ps(1f32 / 200f32))); - let x3 = _mm256_cube_ps(x); - let y3 = _mm256_cube_ps(y); - let z3 = _mm256_cube_ps(z); - let kappa = _mm256_set1_ps(0.008856f32); - let k_sub = _mm256_set1_ps(16f32 / 116f32); - let mult_1 = _mm256_set1_ps(1f32 / 7.787f32); - let low_x = _mm256_mul_ps(_mm256_sub_ps(x, k_sub), mult_1); - let low_y = _mm256_mul_ps(_mm256_sub_ps(y, k_sub), mult_1); - let low_z = _mm256_mul_ps(_mm256_sub_ps(z, k_sub), mult_1); +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx_lab_to_xyz(l: __m256, a: __m256, b: __m256) -> (__m256, __m256, __m256) { + unsafe { + let y = _mm256_mul_ps( + _mm256_add_ps(l, _mm256_set1_ps(16f32)), + _mm256_set1_ps(1f32 / 116f32), + ); + let x = _mm256_add_ps(_mm256_mul_ps(a, _mm256_set1_ps(1f32 / 500f32)), y); + let z = _mm256_sub_ps(y, _mm256_mul_ps(b, _mm256_set1_ps(1f32 / 200f32))); + let x3 = _mm256_cube_ps(x); + let y3 = _mm256_cube_ps(y); + let z3 = _mm256_cube_ps(z); + let kappa = _mm256_set1_ps(0.008856f32); + let k_sub = _mm256_set1_ps(16f32 / 116f32); + let mult_1 = _mm256_set1_ps(1f32 / 7.787f32); + let low_x = _mm256_mul_ps(_mm256_sub_ps(x, k_sub), mult_1); + let low_y = _mm256_mul_ps(_mm256_sub_ps(y, k_sub), mult_1); + let low_z = _mm256_mul_ps(_mm256_sub_ps(z, k_sub), mult_1); - let x = _mm256_select_ps(_mm256_cmp_ps::<_CMP_GT_OS>(x3, kappa), x3, low_x); - let y = _mm256_select_ps(_mm256_cmp_ps::<_CMP_GT_OS>(y3, kappa), y3, low_y); - let z = _mm256_select_ps(_mm256_cmp_ps::<_CMP_GT_OS>(z3, kappa), z3, low_z); - let x = _mm256_mul_ps(x, _mm256_set1_ps(95.047f32 / 100f32)); - let z = _mm256_mul_ps(z, _mm256_set1_ps(108.883f32 / 100f32)); - (x, y, z) + let x = _mm256_select_ps(_mm256_cmp_ps::<_CMP_GT_OS>(x3, kappa), x3, low_x); + let y = _mm256_select_ps(_mm256_cmp_ps::<_CMP_GT_OS>(y3, kappa), y3, low_y); + let z = _mm256_select_ps(_mm256_cmp_ps::<_CMP_GT_OS>(z3, kappa), z3, low_z); + let x = _mm256_mul_ps(x, _mm256_set1_ps(95.047f32 / 100f32)); + let z = _mm256_mul_ps(z, _mm256_set1_ps(108.883f32 / 100f32)); + (x, y, z) + } } -#[inline(always)] -pub(crate) unsafe fn avx_luv_to_xyz(l: __m256, u: __m256, v: __m256) -> (__m256, __m256, __m256) { - let zeros = _mm256_setzero_ps(); - let zero_mask = _mm256_cmp_ps::<_CMP_EQ_OS>(l, zeros); - let l13 = _mm256_rcp_ps(_mm256_mul_ps(l, _mm256_set1_ps(13f32))); - let u = _mm256_prefer_fma_ps(_mm256_set1_ps(LUV_WHITE_U_PRIME), l13, u); - let v = _mm256_prefer_fma_ps(_mm256_set1_ps(LUV_WHITE_V_PRIME), l13, v); - let l_h = _mm256_mul_ps( - _mm256_add_ps(l, _mm256_set1_ps(16f32)), - _mm256_set1_ps(1f32 / 116f32), - ); - let y_high = _mm256_mul_ps(_mm256_mul_ps(l_h, l_h), l_h); - let y_low = _mm256_mul_ps(l, _mm256_set1_ps(LUV_MULTIPLIER_INVERSE_Y)); - let y = _mm256_select_ps( - zero_mask, - zeros, - _mm256_select_ps( - _mm256_cmp_ps::<_CMP_GT_OS>(l, _mm256_set1_ps(8f32)), - y_high, - y_low, - ), - ); - let zero_mask_2 = _mm256_cmp_ps::<_CMP_EQ_OS>(v, zeros); - let den = _mm256_rcp_ps(_mm256_mul_ps(v, _mm256_set1_ps(4f32))); - let mut x = _mm256_mul_ps( - _mm256_mul_ps(_mm256_mul_ps(y, u), den), - _mm256_set1_ps(9f32), - ); - x = _mm256_select_ps(zero_mask, zeros, x); - x = _mm256_select_ps(zero_mask_2, zeros, x); - let mut z = _mm256_mul_ps( - _mm256_mul_ps( - _mm256_prefer_fma_ps( - _mm256_prefer_fma_ps(_mm256_set1_ps(12f32), _mm256_set1_ps(-3f32), u), - v, - _mm256_set1_ps(-20f32), +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx_luv_to_xyz(l: __m256, u: __m256, v: __m256) -> (__m256, __m256, __m256) { + unsafe { + let zeros = _mm256_setzero_ps(); + let zero_mask = _mm256_cmp_ps::<_CMP_EQ_OS>(l, zeros); + let l13 = _mm256_rcp_ps(_mm256_mul_ps(l, _mm256_set1_ps(13f32))); + let u = _mm256_prefer_fma_ps(_mm256_set1_ps(LUV_WHITE_U_PRIME), l13, u); + let v = _mm256_prefer_fma_ps(_mm256_set1_ps(LUV_WHITE_V_PRIME), l13, v); + let l_h = _mm256_mul_ps( + _mm256_add_ps(l, _mm256_set1_ps(16f32)), + _mm256_set1_ps(1f32 / 116f32), + ); + let y_high = _mm256_mul_ps(_mm256_mul_ps(l_h, l_h), l_h); + let y_low = _mm256_mul_ps(l, _mm256_set1_ps(LUV_MULTIPLIER_INVERSE_Y)); + let y = _mm256_select_ps( + zero_mask, + zeros, + _mm256_select_ps( + _mm256_cmp_ps::<_CMP_GT_OS>(l, _mm256_set1_ps(8f32)), + y_high, + y_low, ), - y, - ), - den, - ); - z = _mm256_select_ps(zero_mask, zeros, z); - z = _mm256_select_ps(zero_mask_2, zeros, z); - (x, y, z) + ); + let zero_mask_2 = _mm256_cmp_ps::<_CMP_EQ_OS>(v, zeros); + let den = _mm256_rcp_ps(_mm256_mul_ps(v, _mm256_set1_ps(4f32))); + let mut x = _mm256_mul_ps( + _mm256_mul_ps(_mm256_mul_ps(y, u), den), + _mm256_set1_ps(9f32), + ); + x = _mm256_select_ps(zero_mask, zeros, x); + x = _mm256_select_ps(zero_mask_2, zeros, x); + let mut z = _mm256_mul_ps( + _mm256_mul_ps( + _mm256_prefer_fma_ps( + _mm256_prefer_fma_ps(_mm256_set1_ps(12f32), _mm256_set1_ps(-3f32), u), + v, + _mm256_set1_ps(-20f32), + ), + y, + ), + den, + ); + z = _mm256_select_ps(zero_mask, zeros, z); + z = _mm256_select_ps(zero_mask_2, zeros, z); + (x, y, z) + } } -#[inline(always)] -pub(crate) unsafe fn avx_lch_to_xyz(l: __m256, c: __m256, h: __m256) -> (__m256, __m256, __m256) { - let u = _mm256_mul_ps(c, _mm256_cos_ps(h)); - let v = _mm256_mul_ps(c, _mm256_sin_ps(h)); - avx_luv_to_xyz(l, u, v) +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx_lch_to_xyz(l: __m256, c: __m256, h: __m256) -> (__m256, __m256, __m256) { + unsafe { + let u = _mm256_mul_ps(c, _mm256_cos_ps(h)); + let v = _mm256_mul_ps(c, _mm256_sin_ps(h)); + avx_luv_to_xyz(l, u, v) + } } -#[inline(always)] -pub(crate) unsafe fn avx2_triple_to_xyz( +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx2_triple_to_xyz( r: __m256, g: __m256, b: __m256, diff --git a/src/avx/from_sigmoidal.rs b/src/avx/from_sigmoidal.rs index 562dc22..d304c51 100644 --- a/src/avx/from_sigmoidal.rs +++ b/src/avx/from_sigmoidal.rs @@ -17,76 +17,81 @@ use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; -#[inline(always)] -unsafe fn vld_sigmoidal( +#[inline] +#[target_feature(enable = "avx2")] +fn vld_sigmoidal( src: *const f32, ) -> (__m256i, __m256i, __m256i, __m256i) { - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let v_scale_color = _mm256_set1_ps(255f32); - let pixel_0 = _mm256_loadu_ps(src); - let pixel_1 = _mm256_loadu_ps(src.add(8)); - let pixel_2 = _mm256_loadu_ps(src.add(16)); - if image_configuration.has_alpha() { - let pixel_3 = _mm256_loadu_ps(src.add(24)); - let (sr, sg, sb, sa) = avx2_deinterleave_rgba_ps(pixel_0, pixel_1, pixel_2, pixel_3); - - let (r, g, b) = avx_sigmoidal_to_rgb(sr, sg, sb); - let a_f32 = _mm256_mul_ps(sa, v_scale_color); - (r, g, b, _mm256_cvtps_epi32(_mm256_round_ps::<0>(a_f32))) - } else { - let (sr, sg, sb) = avx2_deinterleave_rgb_ps(pixel_0, pixel_1, pixel_2); - - let (r, g, b) = avx_sigmoidal_to_rgb(sr, sg, sb); - (r, g, b, _mm256_setzero_si256()) + unsafe { + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + let v_scale_color = _mm256_set1_ps(255f32); + let pixel_0 = _mm256_loadu_ps(src); + let pixel_1 = _mm256_loadu_ps(src.add(8)); + let pixel_2 = _mm256_loadu_ps(src.add(16)); + if image_configuration.has_alpha() { + let pixel_3 = _mm256_loadu_ps(src.add(24)); + let (sr, sg, sb, sa) = avx2_deinterleave_rgba_ps(pixel_0, pixel_1, pixel_2, pixel_3); + + let (r, g, b) = avx_sigmoidal_to_rgb(sr, sg, sb); + let a_f32 = _mm256_mul_ps(sa, v_scale_color); + (r, g, b, _mm256_cvtps_epi32(_mm256_round_ps::<0>(a_f32))) + } else { + let (sr, sg, sb) = avx2_deinterleave_rgb_ps(pixel_0, pixel_1, pixel_2); + + let (r, g, b) = avx_sigmoidal_to_rgb(sr, sg, sb); + (r, g, b, _mm256_setzero_si256()) + } } } #[target_feature(enable = "avx2")] -pub unsafe fn avx_from_sigmoidal_row( +pub(crate) fn avx_from_sigmoidal_row( start_cx: usize, - src: *const f32, - dst: *mut u8, + src: &[f32], + dst: &mut [u8], width: u32, ) -> usize { - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + unsafe { + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.channel_count(); + let channels = image_configuration.channel_count(); - let mut cx = start_cx; + let mut cx = start_cx; - while cx + 32 < width as usize { - let offset_src_ptr = src.add(cx * channels); + while cx + 32 <= width as usize { + let offset_src_ptr = src.get_unchecked(cx * channels..).as_ptr(); - let src_ptr_0 = offset_src_ptr; + let src_ptr_0 = offset_src_ptr; - let (r_row0_, g_row0_, b_row0_, a_row0_) = - vld_sigmoidal::(src_ptr_0); + let (r_row0_, g_row0_, b_row0_, a_row0_) = + vld_sigmoidal::(src_ptr_0); - let src_ptr_1 = offset_src_ptr.add(8 * channels); + let src_ptr_1 = offset_src_ptr.add(8 * channels); - let (r_row1_, g_row1_, b_row1_, a_row1_) = - vld_sigmoidal::(src_ptr_1); + let (r_row1_, g_row1_, b_row1_, a_row1_) = + vld_sigmoidal::(src_ptr_1); - let src_ptr_2 = offset_src_ptr.add(8 * 2 * channels); + let src_ptr_2 = offset_src_ptr.add(8 * 2 * channels); - let (r_row2_, g_row2_, b_row2_, a_row2_) = - vld_sigmoidal::(src_ptr_2); + let (r_row2_, g_row2_, b_row2_, a_row2_) = + vld_sigmoidal::(src_ptr_2); - let src_ptr_3 = offset_src_ptr.add(8 * 3 * channels); + let src_ptr_3 = offset_src_ptr.add(8 * 3 * channels); - let (r_row3_, g_row3_, b_row3_, a_row3_) = - vld_sigmoidal::(src_ptr_3); + let (r_row3_, g_row3_, b_row3_, a_row3_) = + vld_sigmoidal::(src_ptr_3); - let r_row = _mm256_packus_four_epi32(r_row0_, r_row1_, r_row2_, r_row3_); - let g_row = _mm256_packus_four_epi32(g_row0_, g_row1_, g_row2_, g_row3_); - let b_row = _mm256_packus_four_epi32(b_row0_, b_row1_, b_row2_, b_row3_); - let a_row = _mm256_packus_four_epi32(a_row0_, a_row1_, a_row2_, a_row3_); + let r_row = _mm256_packus_four_epi32(r_row0_, r_row1_, r_row2_, r_row3_); + let g_row = _mm256_packus_four_epi32(g_row0_, g_row1_, g_row2_, g_row3_); + let b_row = _mm256_packus_four_epi32(b_row0_, b_row1_, b_row2_, b_row3_); + let a_row = _mm256_packus_four_epi32(a_row0_, a_row1_, a_row2_, a_row3_); - let dst_ptr = dst.add(cx * channels); + let dst_ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); - avx_store_and_interleave_u8!(dst_ptr, image_configuration, r_row, g_row, b_row, a_row); - cx += 32; - } + avx_store_and_interleave_u8!(dst_ptr, image_configuration, r_row, g_row, b_row, a_row); + cx += 32; + } - cx + cx + } } diff --git a/src/avx/gamma_curves.rs b/src/avx/gamma_curves.rs deleted file mode 100644 index e7d8e43..0000000 --- a/src/avx/gamma_curves.rs +++ /dev/null @@ -1,150 +0,0 @@ -/* - * // Copyright 2024 (c) the Radzivon Bartoshyk. All rights reserved. - * // - * // Use of this source code is governed by a BSD-style - * // license that can be found in the LICENSE file. - */ -#![allow(dead_code)] -use crate::avx::math::*; -#[allow(unused_imports)] -use crate::gamma_curves::TransferFunction; -use erydanos::_mm256_select_ps; -#[cfg(target_arch = "x86")] -use std::arch::x86::*; -#[cfg(target_arch = "x86_64")] -use std::arch::x86_64::*; - -#[inline(always)] -pub unsafe fn avx2_srgb_from_linear(linear: __m256) -> __m256 { - let linear = _mm256_max_ps(linear, _mm256_setzero_ps()); - let linear = _mm256_min_ps(linear, _mm256_set1_ps(1f32)); - let low_cut_off = _mm256_set1_ps(0.0030412825601275209f32); - let mask = _mm256_cmp_ps::<_CMP_GE_OS>(linear, low_cut_off); - - let mut low = linear; - let mut high = linear; - low = _mm256_mul_ps(low, _mm256_set1_ps(12.92f32)); - - high = _mm256_sub_ps( - _mm256_mul_ps( - _mm256_pow_n_ps(high, 1.0f32 / 2.4f32), - _mm256_set1_ps(1.0550107189475866f32), - ), - _mm256_set1_ps(0.0550107189475866f32), - ); - _mm256_select_ps(mask, high, low) -} - -#[inline(always)] -pub unsafe fn avx2_srgb_to_linear(gamma: __m256) -> __m256 { - let gamma = _mm256_max_ps(gamma, _mm256_setzero_ps()); - let gamma = _mm256_min_ps(gamma, _mm256_set1_ps(1f32)); - let low_cut_off = _mm256_set1_ps(12.92f32 * 0.0030412825601275209f32); - let mask = _mm256_cmp_ps::<_CMP_GE_OS>(gamma, low_cut_off); - - let mut low = gamma; - let high = _mm256_pow_n_ps( - _mm256_mul_ps( - _mm256_add_ps(gamma, _mm256_set1_ps(0.0550107189475866f32)), - _mm256_set1_ps(1f32 / 1.0550107189475866f32), - ), - 2.4f32, - ); - low = _mm256_mul_ps(low, _mm256_set1_ps(1f32 / 12.92f32)); - _mm256_select_ps(mask, high, low) -} - -#[inline(always)] -pub unsafe fn avx2_rec709_from_linear(linear: __m256) -> __m256 { - let linear = _mm256_max_ps(linear, _mm256_setzero_ps()); - let linear = _mm256_min_ps(linear, _mm256_set1_ps(1f32)); - let low_cut_off = _mm256_set1_ps(0.018053968510807f32); - let mask = _mm256_cmp_ps::<_CMP_GE_OS>(linear, low_cut_off); - - let mut low = linear; - let mut high = linear; - low = _mm256_mul_ps(low, _mm256_set1_ps(4.5f32)); - - high = _mm256_sub_ps( - _mm256_mul_ps( - _mm256_pow_n_ps(high, 0.45f32), - _mm256_set1_ps(1.09929682680944f32), - ), - _mm256_set1_ps(0.09929682680944f32), - ); - _mm256_select_ps(mask, high, low) -} - -#[inline(always)] -pub unsafe fn avx2_rec709_to_linear(gamma: __m256) -> __m256 { - let gamma = _mm256_max_ps(gamma, _mm256_setzero_ps()); - let gamma = _mm256_min_ps(gamma, _mm256_set1_ps(1f32)); - let low_cut_off = _mm256_set1_ps(4.5f32 * 0.018053968510807f32); - let mask = _mm256_cmp_ps::<_CMP_GE_OS>(gamma, low_cut_off); - - let mut low = gamma; - let high = _mm256_pow_n_ps( - _mm256_mul_ps( - _mm256_add_ps(gamma, _mm256_set1_ps(0.09929682680944f32)), - _mm256_set1_ps(1f32 / 1.09929682680944f32), - ), - 1.0f32 / 0.45f32, - ); - low = _mm256_mul_ps(low, _mm256_set1_ps(1f32 / 4.5f32)); - _mm256_select_ps(mask, high, low) -} - -#[inline(always)] -pub unsafe fn avx2_pure_gamma(x: __m256, value: f32) -> __m256 { - let zeros = _mm256_setzero_ps(); - let ones = _mm256_set1_ps(1f32); - let zero_mask = _mm256_cmp_ps::<_CMP_LE_OS>(x, zeros); - let ones_mask = _mm256_cmp_ps::<_CMP_GE_OS>(x, ones); - let mut rs = _mm256_pow_n_ps(x, value); - rs = _mm256_select_ps(zero_mask, zeros, rs); - _mm256_select_ps(ones_mask, ones, rs) -} - -#[inline(always)] -pub unsafe fn avx2_smpte428_from_linear(linear: __m256) -> __m256 { - const POWER_VALUE: f32 = 1.0f32 / 2.6f32; - _mm256_pow_ps( - _mm256_mul_ps( - _mm256_max_ps(linear, _mm256_setzero_ps()), - _mm256_set1_ps(0.91655527974030934f32), - ), - _mm256_set1_ps(POWER_VALUE), - ) -} - -#[inline(always)] -pub unsafe fn avx2_smpte428_to_linear(gamma: __m256) -> __m256 { - const SCALE: f32 = 1. / 0.91655527974030934f32; - _mm256_mul_ps( - _mm256_pow_ps( - _mm256_max_ps(gamma, _mm256_setzero_ps()), - _mm256_set1_ps(2.6f32), - ), - _mm256_set1_ps(SCALE), - ) -} - -#[inline(always)] -pub unsafe fn avx2_gamma2p2_to_linear(gamma: __m256) -> __m256 { - avx2_pure_gamma(gamma, 2.2f32) -} - -#[inline(always)] -pub unsafe fn avx2_gamma2p8_to_linear(gamma: __m256) -> __m256 { - avx2_pure_gamma(gamma, 2.8f32) -} - -#[inline(always)] -pub unsafe fn avx2_gamma2p2_from_linear(linear: __m256) -> __m256 { - avx2_pure_gamma(linear, 1f32 / 2.2f32) -} - -#[inline(always)] -pub unsafe fn avx2_gamma2p8_from_linear(linear: __m256) -> __m256 { - avx2_pure_gamma(linear, 1f32 / 2.8f32) -} diff --git a/src/avx/image_to_oklab.rs b/src/avx/image_to_oklab.rs index 451950b..bfcccb2 100644 --- a/src/avx/image_to_oklab.rs +++ b/src/avx/image_to_oklab.rs @@ -77,30 +77,32 @@ pub(crate) unsafe fn avx_image_to_oklab(in_place_ptr.as_ptr()); + unsafe { + let in_place_ptr = dst.get_unchecked_mut(cx * channels..); + let (r_chan, g_chan, b_chan, a_chan) = + avx_vld_f32_and_deinterleave::(in_place_ptr.as_ptr()); - let (x_low_low, y_low_low, z_low_low) = triple_to_oklab!( - r_chan, g_chan, b_chan, target, c0, c1, c2, c3, c4, c5, c6, c7, c8, m0, m1, m2, m3, m4, - m5, m6, m7, m8 - ); - - if image_configuration.has_alpha() { - avx_store_and_interleave_v4_direct_f32!( - in_place_ptr.as_mut_ptr(), - x_low_low, - y_low_low, - z_low_low, - a_chan - ); - } else { - avx_store_and_interleave_v3_direct_f32!( - in_place_ptr.as_mut_ptr(), - x_low_low, - y_low_low, - z_low_low + let (x_low_low, y_low_low, z_low_low) = triple_to_oklab!( + r_chan, g_chan, b_chan, target, c0, c1, c2, c3, c4, c5, c6, c7, c8, m0, m1, m2, m3, + m4, m5, m6, m7, m8 ); + + if image_configuration.has_alpha() { + avx_store_and_interleave_v4_direct_f32!( + in_place_ptr.as_mut_ptr(), + x_low_low, + y_low_low, + z_low_low, + a_chan + ); + } else { + avx_store_and_interleave_v3_direct_f32!( + in_place_ptr.as_mut_ptr(), + x_low_low, + y_low_low, + z_low_low + ); + } } cx += 8; diff --git a/src/avx/math.rs b/src/avx/math.rs index 46e04e3..42e592c 100644 --- a/src/avx/math.rs +++ b/src/avx/math.rs @@ -12,40 +12,47 @@ use std::arch::x86_64::*; use erydanos::{_mm256_pow_fast_ps, _mm256_prefer_fma_ps}; -#[inline(always)] -pub unsafe fn _mm256_cube_ps(x: __m256) -> __m256 { +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn _mm256_cube_ps(x: __m256) -> __m256 { _mm256_mul_ps(_mm256_mul_ps(x, x), x) } -#[inline(always)] -pub unsafe fn _mm256_pow_ps(x: __m256, n: __m256) -> __m256 { - _mm256_pow_fast_ps(x, n) +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn _mm256_pow_ps(x: __m256, n: __m256) -> __m256 { + unsafe { _mm256_pow_fast_ps(x, n) } } -#[inline(always)] -pub unsafe fn _mm256_pow_n_ps(x: __m256, n: f32) -> __m256 { - _mm256_pow_fast_ps(x, _mm256_set1_ps(n)) +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn _mm256_pow_n_ps(x: __m256, n: f32) -> __m256 { + unsafe { _mm256_pow_fast_ps(x, _mm256_set1_ps(n)) } } -#[inline(always)] -pub(crate) unsafe fn _mm256_fmaf_ps(a: __m256, b: __m256, c: __m256) -> __m256 { - _mm256_prefer_fma_ps(c, b, a) +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn _mm256_fmaf_ps(a: __m256, b: __m256, c: __m256) -> __m256 { + unsafe { _mm256_prefer_fma_ps(c, b, a) } } -#[inline(always)] -pub unsafe fn _mm256_cmpge_epi32(a: __m256i, b: __m256i) -> __m256i { +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn _mm256_cmpge_epi32(a: __m256i, b: __m256i) -> __m256i { let gt = _mm256_cmpgt_epi32(a, b); let eq = _mm256_cmpeq_epi32(a, b); _mm256_or_si256(gt, eq) } -#[inline(always)] -pub unsafe fn _mm256_cmplt_epi32(a: __m256i, b: __m256i) -> __m256i { +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn _mm256_cmplt_epi32(a: __m256i, b: __m256i) -> __m256i { _mm256_cmpgt_epi32(b, a) } -#[inline(always)] -pub unsafe fn _mm256_color_matrix_ps( +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn _mm256_color_matrix_ps( r: __m256, g: __m256, b: __m256, @@ -59,8 +66,10 @@ pub unsafe fn _mm256_color_matrix_ps( c8: __m256, c9: __m256, ) -> (__m256, __m256, __m256) { - let new_r = _mm256_prefer_fma_ps(_mm256_prefer_fma_ps(_mm256_mul_ps(g, c2), b, c3), r, c1); - let new_g = _mm256_prefer_fma_ps(_mm256_prefer_fma_ps(_mm256_mul_ps(g, c5), b, c6), r, c4); - let new_b = _mm256_prefer_fma_ps(_mm256_prefer_fma_ps(_mm256_mul_ps(g, c8), b, c9), r, c7); - (new_r, new_g, new_b) + unsafe { + let new_r = _mm256_prefer_fma_ps(_mm256_prefer_fma_ps(_mm256_mul_ps(g, c2), b, c3), r, c1); + let new_g = _mm256_prefer_fma_ps(_mm256_prefer_fma_ps(_mm256_mul_ps(g, c5), b, c6), r, c4); + let new_b = _mm256_prefer_fma_ps(_mm256_prefer_fma_ps(_mm256_mul_ps(g, c8), b, c9), r, c7); + (new_r, new_g, new_b) + } } diff --git a/src/avx/mod.rs b/src/avx/mod.rs index de60f08..1c5da0a 100644 --- a/src/avx/mod.rs +++ b/src/avx/mod.rs @@ -7,7 +7,6 @@ mod cie; mod from_sigmoidal; -mod gamma_curves; mod image_to_oklab; mod math; mod oklab_to_image; @@ -20,13 +19,13 @@ mod utils; mod xyz_lab_to_image; mod xyza_laba_to_image; -pub use from_sigmoidal::avx_from_sigmoidal_row; +pub(crate) use from_sigmoidal::avx_from_sigmoidal_row; pub(crate) use image_to_oklab::avx_image_to_oklab; -pub use math::*; +pub(crate) use math::*; pub(crate) use oklab_to_image::avx_oklab_to_image; pub use support::*; pub(crate) use to_sigmoidal::avx_image_to_sigmoidal_row; pub(crate) use to_xyz_lab::avx2_image_to_xyz_lab; -pub use utils::*; -pub use xyz_lab_to_image::*; -pub use xyza_laba_to_image::*; +pub(crate) use utils::*; +pub(crate) use xyz_lab_to_image::avx_xyz_to_channels; +pub(crate) use xyza_laba_to_image::avx_xyza_to_image; diff --git a/src/avx/oklab_to_image.rs b/src/avx/oklab_to_image.rs index d5c7734..d9a404a 100644 --- a/src/avx/oklab_to_image.rs +++ b/src/avx/oklab_to_image.rs @@ -18,8 +18,9 @@ use crate::image::ImageConfiguration; use crate::image_to_oklab::OklabTarget; use crate::{avx_store_and_interleave_v3_f32, avx_store_and_interleave_v4_f32}; -#[inline(always)] -unsafe fn avx_oklab_vld( +#[inline] +#[target_feature(enable = "avx2")] +fn avx_oklab_vld( src: &[f32], oklab_target: OklabTarget, m0: __m256, @@ -45,10 +46,12 @@ unsafe fn avx_oklab_vld( avx_vld_f32_and_deinterleave_direct::(src.as_ptr()); if oklab_target == OklabTarget::Oklch { - let a0 = _mm256_mul_ps(a, _mm256_cos_ps(b)); - let b0 = _mm256_mul_ps(a, _mm256_sin_ps(b)); - a = a0; - b = b0; + unsafe { + let a0 = _mm256_mul_ps(a, _mm256_cos_ps(b)); + let b0 = _mm256_mul_ps(a, _mm256_sin_ps(b)); + a = a0; + b = b0; + } } let (mut l_l, mut l_m, mut l_s) = @@ -63,7 +66,7 @@ unsafe fn avx_oklab_vld( } #[target_feature(enable = "avx2")] -pub(crate) unsafe fn avx_oklab_to_image( +pub(crate) fn avx_oklab_to_image( start_cx: usize, src: &[f32], dst: &mut [f32], @@ -99,34 +102,36 @@ pub(crate) unsafe fn avx_oklab_to_image( - src_ptr_0, target, m0, m1, m2, m3, m4, m5, m6, m7, m8, c0, c1, c2, c3, c4, c5, c6, c7, - c8, - ); + let src_ptr_0 = offset_src_ptr; - let dst_ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); - - if image_configuration.has_alpha() { - avx_store_and_interleave_v4_f32!( - dst_ptr, - image_configuration, - r_row0_, - g_row0_, - b_row0_, - a_row0_ - ); - } else { - avx_store_and_interleave_v3_f32!( - dst_ptr, - image_configuration, - r_row0_, - g_row0_, - b_row0_ + let (r_row0_, g_row0_, b_row0_, a_row0_) = avx_oklab_vld::( + src_ptr_0, target, m0, m1, m2, m3, m4, m5, m6, m7, m8, c0, c1, c2, c3, c4, c5, c6, + c7, c8, ); + + let dst_ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); + + if image_configuration.has_alpha() { + avx_store_and_interleave_v4_f32!( + dst_ptr, + image_configuration, + r_row0_, + g_row0_, + b_row0_, + a_row0_ + ); + } else { + avx_store_and_interleave_v3_f32!( + dst_ptr, + image_configuration, + r_row0_, + g_row0_, + b_row0_ + ); + } } cx += 8; diff --git a/src/avx/routines.rs b/src/avx/routines.rs index a5e3731..55b389e 100644 --- a/src/avx/routines.rs +++ b/src/avx/routines.rs @@ -14,124 +14,133 @@ use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; -#[inline(always)] -pub(crate) unsafe fn avx_vld_u8_and_deinterleave( +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx_vld_u8_and_deinterleave( ptr: *const u8, ) -> (__m256i, __m256i, __m256i, __m256i) { - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let (r_chan, g_chan, b_chan, a_chan); + unsafe { + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + let (r_chan, g_chan, b_chan, a_chan); - let row1 = _mm256_loadu_si256(ptr as *const __m256i); - let row2 = _mm256_loadu_si256(ptr.add(32) as *const __m256i); - let row3 = _mm256_loadu_si256(ptr.add(64) as *const __m256i); - match image_configuration { - ImageConfiguration::Rgb | ImageConfiguration::Bgr => { - let (c1, c2, c3) = avx2_deinterleave_rgb_epi8(row1, row2, row3); - if image_configuration == ImageConfiguration::Rgb { - r_chan = c1; - g_chan = c2; - b_chan = c3; - } else { - r_chan = c3; - g_chan = c2; - b_chan = c1; - } - a_chan = _mm256_set1_epi8(-128); - } - ImageConfiguration::Rgba | ImageConfiguration::Bgra => { - let row4 = _mm256_loadu_si256(ptr.add(96) as *const __m256i); - let (c1, c2, c3, c4) = avx2_deinterleave_rgba_epi8(row1, row2, row3, row4); - if image_configuration == ImageConfiguration::Rgba { - r_chan = c1; - g_chan = c2; - b_chan = c3; - a_chan = c4; - } else { - r_chan = c3; - g_chan = c2; - b_chan = c1; - a_chan = c4; + let row1 = _mm256_loadu_si256(ptr as *const __m256i); + let row2 = _mm256_loadu_si256(ptr.add(32) as *const __m256i); + let row3 = _mm256_loadu_si256(ptr.add(64) as *const __m256i); + match image_configuration { + ImageConfiguration::Rgb | ImageConfiguration::Bgr => { + let (c1, c2, c3) = avx2_deinterleave_rgb_epi8(row1, row2, row3); + if image_configuration == ImageConfiguration::Rgb { + r_chan = c1; + g_chan = c2; + b_chan = c3; + } else { + r_chan = c3; + g_chan = c2; + b_chan = c1; + } + a_chan = _mm256_set1_epi8(-128); + } + ImageConfiguration::Rgba | ImageConfiguration::Bgra => { + let row4 = _mm256_loadu_si256(ptr.add(96) as *const __m256i); + let (c1, c2, c3, c4) = avx2_deinterleave_rgba_epi8(row1, row2, row3, row4); + if image_configuration == ImageConfiguration::Rgba { + r_chan = c1; + g_chan = c2; + b_chan = c3; + a_chan = c4; + } else { + r_chan = c3; + g_chan = c2; + b_chan = c1; + a_chan = c4; + } } } + + (r_chan, g_chan, b_chan, a_chan) } +} + +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx_vld_f32_and_deinterleave( + ptr: *const f32, +) -> (__m256, __m256, __m256, __m256) { + unsafe { + let (r_f32, g_f32, b_f32, a_f32); + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - (r_chan, g_chan, b_chan, a_chan) + let row0 = _mm256_loadu_ps(ptr); + let row1 = _mm256_loadu_ps(ptr.add(8)); + let row2 = _mm256_loadu_ps(ptr.add(16)); + + match image_configuration { + ImageConfiguration::Rgba | ImageConfiguration::Bgra => { + let row3 = _mm256_loadu_ps(ptr.add(24)); + let (v0, v1, v2, v3) = avx2_deinterleave_rgba_ps(row0, row1, row2, row3); + if image_configuration == ImageConfiguration::Rgba { + r_f32 = v0; + g_f32 = v1; + b_f32 = v2; + } else { + r_f32 = v2; + g_f32 = v1; + b_f32 = v0; + } + a_f32 = v3; + } + ImageConfiguration::Bgr | ImageConfiguration::Rgb => { + let rgb_pixels = avx2_deinterleave_rgb_ps(row0, row1, row2); + if image_configuration == ImageConfiguration::Rgb { + r_f32 = rgb_pixels.0; + g_f32 = rgb_pixels.1; + b_f32 = rgb_pixels.2; + } else { + r_f32 = rgb_pixels.2; + g_f32 = rgb_pixels.1; + b_f32 = rgb_pixels.0; + } + a_f32 = _mm256_set1_ps(1.); + } + } + + (r_f32, g_f32, b_f32, a_f32) + } } -#[inline(always)] -pub(crate) unsafe fn avx_vld_f32_and_deinterleave( +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx_vld_f32_and_deinterleave_direct( ptr: *const f32, ) -> (__m256, __m256, __m256, __m256) { - let (r_f32, g_f32, b_f32, a_f32); - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + unsafe { + let (r_f32, g_f32, b_f32, a_f32); + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let row0 = _mm256_loadu_ps(ptr); - let row1 = _mm256_loadu_ps(ptr.add(8)); - let row2 = _mm256_loadu_ps(ptr.add(16)); + let row0 = _mm256_loadu_ps(ptr); + let row1 = _mm256_loadu_ps(ptr.add(8)); + let row2 = _mm256_loadu_ps(ptr.add(16)); - match image_configuration { - ImageConfiguration::Rgba | ImageConfiguration::Bgra => { - let row3 = _mm256_loadu_ps(ptr.add(24)); - let (v0, v1, v2, v3) = avx2_deinterleave_rgba_ps(row0, row1, row2, row3); - if image_configuration == ImageConfiguration::Rgba { + match image_configuration { + ImageConfiguration::Rgba | ImageConfiguration::Bgra => { + let row3 = _mm256_loadu_ps(ptr.add(24)); + let (v0, v1, v2, v3) = avx2_deinterleave_rgba_ps(row0, row1, row2, row3); r_f32 = v0; g_f32 = v1; b_f32 = v2; - } else { - r_f32 = v2; - g_f32 = v1; - b_f32 = v0; + a_f32 = v3; } - a_f32 = v3; - } - ImageConfiguration::Bgr | ImageConfiguration::Rgb => { - let rgb_pixels = avx2_deinterleave_rgb_ps(row0, row1, row2); - if image_configuration == ImageConfiguration::Rgb { + ImageConfiguration::Bgr | ImageConfiguration::Rgb => { + let rgb_pixels = avx2_deinterleave_rgb_ps(row0, row1, row2); r_f32 = rgb_pixels.0; g_f32 = rgb_pixels.1; b_f32 = rgb_pixels.2; - } else { - r_f32 = rgb_pixels.2; - g_f32 = rgb_pixels.1; - b_f32 = rgb_pixels.0; + a_f32 = _mm256_set1_ps(1.); } - a_f32 = _mm256_set1_ps(1.); } - } - - (r_f32, g_f32, b_f32, a_f32) -} - -#[inline(always)] -pub(crate) unsafe fn avx_vld_f32_and_deinterleave_direct( - ptr: *const f32, -) -> (__m256, __m256, __m256, __m256) { - let (r_f32, g_f32, b_f32, a_f32); - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - - let row0 = _mm256_loadu_ps(ptr); - let row1 = _mm256_loadu_ps(ptr.add(8)); - let row2 = _mm256_loadu_ps(ptr.add(16)); - match image_configuration { - ImageConfiguration::Rgba | ImageConfiguration::Bgra => { - let row3 = _mm256_loadu_ps(ptr.add(24)); - let (v0, v1, v2, v3) = avx2_deinterleave_rgba_ps(row0, row1, row2, row3); - r_f32 = v0; - g_f32 = v1; - b_f32 = v2; - a_f32 = v3; - } - ImageConfiguration::Bgr | ImageConfiguration::Rgb => { - let rgb_pixels = avx2_deinterleave_rgb_ps(row0, row1, row2); - r_f32 = rgb_pixels.0; - g_f32 = rgb_pixels.1; - b_f32 = rgb_pixels.2; - a_f32 = _mm256_set1_ps(1.); - } + (r_f32, g_f32, b_f32, a_f32) } - - (r_f32, g_f32, b_f32, a_f32) } #[macro_export] diff --git a/src/avx/sigmoidal.rs b/src/avx/sigmoidal.rs index cac31ee..8be0a82 100644 --- a/src/avx/sigmoidal.rs +++ b/src/avx/sigmoidal.rs @@ -11,41 +11,45 @@ use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; -#[inline(always)] -pub(crate) unsafe fn avx_color_to_sigmoidal(x: __m256) -> __m256 { - let x = _mm256_mul_ps(x, _mm256_set1_ps(1f32 / 255f32)); - let negg = _mm256_neg_ps(x); - let den = _mm256_add_ps(_mm256_set1_ps(1f32), _mm256_exp_ps(negg)); - let erase_nan_mask = _mm256_cmp_ps::<_CMP_EQ_OS>(den, _mm256_setzero_ps()); - let rcp = _mm256_rcp_ps(den); - _mm256_select_ps(erase_nan_mask, _mm256_setzero_ps(), rcp) +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx_color_to_sigmoidal(x: __m256) -> __m256 { + unsafe { + let x = _mm256_mul_ps(x, _mm256_set1_ps(1f32 / 255f32)); + let negg = _mm256_neg_ps(x); + let den = _mm256_add_ps(_mm256_set1_ps(1f32), _mm256_exp_ps(negg)); + let erase_nan_mask = _mm256_cmp_ps::<_CMP_EQ_OS>(den, _mm256_setzero_ps()); + let rcp = _mm256_rcp_ps(den); + _mm256_select_ps(erase_nan_mask, _mm256_setzero_ps(), rcp) + } } -#[inline(always)] -pub(crate) unsafe fn avx_sigmoidal_to_color(x: __m256) -> __m256 { - let den = _mm256_sub_ps(_mm256_set1_ps(1f32), x); - let zero_mask_1 = _mm256_cmp_ps::<_CMP_EQ_OS>(den, _mm256_setzero_ps()); - let k = _mm256_mul_ps(x, _mm256_rcp_ps(den)); - let zeros = _mm256_setzero_ps(); - let zero_mask_2 = _mm256_cmp_ps::<_CMP_LT_OS>(k, zeros); - let ln = _mm256_ln_fast_ps(k); - _mm256_select_ps(_mm256_and_ps(zero_mask_1, zero_mask_2), zeros, ln) +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx_sigmoidal_to_color(x: __m256) -> __m256 { + unsafe { + let den = _mm256_sub_ps(_mm256_set1_ps(1f32), x); + let zero_mask_1 = _mm256_cmp_ps::<_CMP_EQ_OS>(den, _mm256_setzero_ps()); + let k = _mm256_mul_ps(x, _mm256_rcp_ps(den)); + let zeros = _mm256_setzero_ps(); + let zero_mask_2 = _mm256_cmp_ps::<_CMP_LT_OS>(k, zeros); + let ln = _mm256_ln_fast_ps(k); + _mm256_select_ps(_mm256_and_ps(zero_mask_1, zero_mask_2), zeros, ln) + } } -#[inline(always)] -pub(crate) unsafe fn avx_rgb_to_sigmoidal( - r: __m256i, - g: __m256i, - b: __m256i, -) -> (__m256, __m256, __m256) { +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx_rgb_to_sigmoidal(r: __m256i, g: __m256i, b: __m256i) -> (__m256, __m256, __m256) { let sr = avx_color_to_sigmoidal(_mm256_cvtepi32_ps(r)); let sg = avx_color_to_sigmoidal(_mm256_cvtepi32_ps(g)); let sb = avx_color_to_sigmoidal(_mm256_cvtepi32_ps(b)); (sr, sg, sb) } -#[inline(always)] -pub(crate) unsafe fn avx_sigmoidal_to_rgb( +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx_sigmoidal_to_rgb( sr: __m256, sg: __m256, sb: __m256, diff --git a/src/avx/support.rs b/src/avx/support.rs index cf813fc..606d384 100644 --- a/src/avx/support.rs +++ b/src/avx/support.rs @@ -19,7 +19,8 @@ pub const fn shuffle(z: u32, y: u32, x: u32, w: u32) -> i32 { } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[inline(always)] +#[inline] +#[target_feature(enable = "avx2")] #[allow(dead_code)] pub unsafe fn demote_i16_to_u8(s_1: __m256i, s_2: __m256i) -> __m256i { let packed = _mm256_packus_epi16(s_1, s_2); @@ -28,7 +29,8 @@ pub unsafe fn demote_i16_to_u8(s_1: __m256i, s_2: __m256i) -> __m256i { } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[inline(always)] +#[inline] +#[target_feature(enable = "avx2")] #[allow(dead_code)] pub unsafe fn avx2_interleave_even(x: __m256i) -> __m256i { #[rustfmt::skip] @@ -44,7 +46,8 @@ pub unsafe fn avx2_interleave_even(x: __m256i) -> __m256i { } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[inline(always)] +#[inline] +#[target_feature(enable = "avx2")] #[allow(dead_code)] pub unsafe fn avx2_interleave_even_2_epi8(a: __m256i, b: __m256i) -> __m256i { let mask_a = _mm256_slli_epi16::<8>(_mm256_srli_epi16::<8>(a)); @@ -54,9 +57,10 @@ pub unsafe fn avx2_interleave_even_2_epi8(a: __m256i, b: __m256i) -> __m256i { } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[inline(always)] +#[inline] +#[target_feature(enable = "avx2")] #[allow(dead_code)] -pub unsafe fn avx2_interleave_odd_2_epi8(a: __m256i, b: __m256i) -> __m256i { +pub(crate) fn avx2_interleave_odd_2_epi8(a: __m256i, b: __m256i) -> __m256i { let mask_a = _mm256_set1_epi16(0x00FF); let masked_a = _mm256_slli_epi16::<8>(_mm256_and_si256(a, mask_a)); let b_s = _mm256_and_si256(b, mask_a); @@ -64,9 +68,10 @@ pub unsafe fn avx2_interleave_odd_2_epi8(a: __m256i, b: __m256i) -> __m256i { } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[inline(always)] +#[inline] +#[target_feature(enable = "avx2")] #[allow(dead_code)] -pub unsafe fn avx2_interleave_odd(x: __m256i) -> __m256i { +pub(crate) fn avx2_interleave_odd(x: __m256i) -> __m256i { #[rustfmt::skip] let shuffle = _mm256_setr_epi8(1, 1, 3, 3, 5, 5, 7, 7, @@ -80,7 +85,8 @@ pub unsafe fn avx2_interleave_odd(x: __m256i) -> __m256i { } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[inline(always)] +#[inline] +#[target_feature(enable = "sse4.1")] #[allow(dead_code)] pub unsafe fn sse_interleave_odd(x: __m128i) -> __m128i { #[rustfmt::skip] @@ -89,8 +95,9 @@ pub unsafe fn sse_interleave_odd(x: __m128i) -> __m128i { _mm_shuffle_epi8(x, shuffle) } -#[inline(always)] -pub unsafe fn avx2_interleave_rgb( +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx2_interleave_rgb( r: __m256i, g: __m256i, b: __m256i, @@ -134,7 +141,7 @@ pub unsafe fn avx2_interleave_rgb( #[inline] #[target_feature(enable = "avx2")] -pub unsafe fn avx2_deinterleave_rgb_epi32( +pub(crate) fn avx2_deinterleave_rgb_epi32( bgr0: __m256i, bgr1: __m256i, bgr2: __m256i, @@ -152,8 +159,9 @@ pub unsafe fn avx2_deinterleave_rgb_epi32( (b0, g0, r0) } -#[inline(always)] -pub unsafe fn avx2_deinterleave_rgb_epi8( +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx2_deinterleave_rgb_epi8( rgb0: __m256i, rgb1: __m256i, rgb2: __m256i, @@ -227,7 +235,7 @@ pub unsafe fn avx2_deinterleave_rgb_epi8( #[inline] #[target_feature(enable = "avx2")] -pub unsafe fn avx2_deinterleave_rgb_ps( +pub(crate) fn avx2_deinterleave_rgb_ps( rgb0: __m256, rgb1: __m256, rgb2: __m256, @@ -251,8 +259,9 @@ pub unsafe fn avx2_deinterleave_rgb_ps( // _mm256_permute4x64_epi64::(v) // } -#[inline(always)] -pub unsafe fn avx2_deinterleave_rgba_epi8( +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx2_deinterleave_rgba_epi8( rgba0: __m256i, rgba1: __m256i, rgba2: __m256i, @@ -292,18 +301,22 @@ pub unsafe fn avx2_deinterleave_rgba_epi8( } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[inline(always)] +#[inline] +#[target_feature(enable = "avx2")] #[allow(dead_code)] -pub unsafe fn avx2_store_u8_rgb(ptr: *mut u8, r: __m256i, g: __m256i, b: __m256i) { +pub(crate) fn avx2_store_u8_rgb(ptr: *mut u8, r: __m256i, g: __m256i, b: __m256i) { let (rgb1, rgb2, rgb3) = avx2_interleave_rgb(r, g, b); - _mm256_storeu_si256(ptr as *mut __m256i, rgb1); - _mm256_storeu_si256(ptr.add(32) as *mut __m256i, rgb2); - _mm256_storeu_si256(ptr.add(64) as *mut __m256i, rgb3); + unsafe { + _mm256_storeu_si256(ptr as *mut __m256i, rgb1); + _mm256_storeu_si256(ptr.add(32) as *mut __m256i, rgb2); + _mm256_storeu_si256(ptr.add(64) as *mut __m256i, rgb3); + } } -#[inline(always)] -pub unsafe fn avx2_interleave_rgba_epi8( +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx2_interleave_rgba_epi8( a: __m256i, b: __m256i, c: __m256i, @@ -327,9 +340,10 @@ pub unsafe fn avx2_interleave_rgba_epi8( } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[inline(always)] +#[inline] +#[target_feature(enable = "avx2")] #[allow(dead_code)] -pub unsafe fn avx2_deinterleave_rgba_epi32( +pub(crate) fn avx2_deinterleave_rgba_epi32( p0: __m256i, p1: __m256i, p2: __m256i, @@ -353,9 +367,10 @@ pub unsafe fn avx2_deinterleave_rgba_epi32( } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[inline(always)] +#[inline] +#[target_feature(enable = "avx2")] #[allow(dead_code)] -pub unsafe fn avx2_interleave_rgba_epi32( +pub(crate) fn avx2_interleave_rgba_epi32( a: __m256i, b: __m256i, c: __m256i, @@ -378,8 +393,9 @@ pub unsafe fn avx2_interleave_rgba_epi32( (bgra0, bgra1, bgra2, bgra3) } -#[inline(always)] -pub unsafe fn avx2_interleave_rgba_ps( +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx2_interleave_rgba_ps( r: __m256, g: __m256, b: __m256, @@ -399,8 +415,9 @@ pub unsafe fn avx2_interleave_rgba_ps( ) } -#[inline(always)] -pub unsafe fn avx2_deinterleave_rgba_ps( +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx2_deinterleave_rgba_ps( r: __m256, g: __m256, b: __m256, @@ -421,19 +438,23 @@ pub unsafe fn avx2_deinterleave_rgba_ps( } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[inline(always)] +#[inline] +#[target_feature(enable = "avx2")] #[allow(dead_code)] -pub unsafe fn avx2_store_u8_rgba(ptr: *mut u8, r: __m256i, g: __m256i, b: __m256i, a: __m256i) { +pub(crate) fn avx2_store_u8_rgba(ptr: *mut u8, r: __m256i, g: __m256i, b: __m256i, a: __m256i) { let (rgba0, rgba1, rgba2, rgba3) = avx2_interleave_rgba_epi8(r, g, b, a); - _mm256_storeu_si256(ptr as *mut __m256i, rgba0); - _mm256_storeu_si256(ptr.add(32) as *mut __m256i, rgba1); - _mm256_storeu_si256(ptr.add(64) as *mut __m256i, rgba2); - _mm256_storeu_si256(ptr.add(96) as *mut __m256i, rgba3); + unsafe { + _mm256_storeu_si256(ptr as *mut __m256i, rgba0); + _mm256_storeu_si256(ptr.add(32) as *mut __m256i, rgba1); + _mm256_storeu_si256(ptr.add(64) as *mut __m256i, rgba2); + _mm256_storeu_si256(ptr.add(96) as *mut __m256i, rgba3); + } } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[inline(always)] +#[inline] +#[target_feature(enable = "avx2")] #[allow(dead_code)] pub unsafe fn avx2_pairwise_add(v: __m256i) -> __m256i { let sums = _mm256_maddubs_epi16(v, _mm256_set1_epi8(1)); @@ -444,9 +465,10 @@ pub unsafe fn avx2_pairwise_add(v: __m256i) -> __m256i { } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[inline(always)] +#[inline] +#[target_feature(enable = "avx2")] #[allow(dead_code)] -pub unsafe fn avx2_div_by255(v: __m256i) -> __m256i { +pub(crate) fn avx2_div_by255(v: __m256i) -> __m256i { let rounding = _mm256_set1_epi16(1 << 7); let x = _mm256_adds_epi16(v, rounding); let multiplier = _mm256_set1_epi16(-32640); diff --git a/src/avx/to_sigmoidal.rs b/src/avx/to_sigmoidal.rs index 6e326a9..0218e4b 100644 --- a/src/avx/to_sigmoidal.rs +++ b/src/avx/to_sigmoidal.rs @@ -17,7 +17,7 @@ use crate::image::ImageConfiguration; use crate::{avx_store_and_interleave_v3_f32, avx_store_and_interleave_v4_f32}; #[target_feature(enable = "avx2")] -pub(crate) unsafe fn avx_image_to_sigmoidal_row< +pub(crate) fn avx_image_to_sigmoidal_row< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, >( @@ -35,165 +35,169 @@ pub(crate) unsafe fn avx_image_to_sigmoidal_row< let channels = image_configuration.channel_count(); while cx + 32 <= width as usize { - let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); - let (r_chan, g_chan, b_chan, a_chan) = - avx_vld_u8_and_deinterleave::(src_ptr); - - let r_low = _mm256_cvtepu8_epi16(_mm256_castsi256_si128(r_chan)); - let g_low = _mm256_cvtepu8_epi16(_mm256_castsi256_si128(g_chan)); - let b_low = _mm256_cvtepu8_epi16(_mm256_castsi256_si128(b_chan)); - let a_low = _mm256_cvtepu8_epi16(_mm256_castsi256_si128(a_chan)); - - let r_low_low = _mm256_cvtepu16_epi32(_mm256_castsi256_si128(r_low)); - let g_low_low = _mm256_cvtepu16_epi32(_mm256_castsi256_si128(g_low)); - let b_low_low = _mm256_cvtepu16_epi32(_mm256_castsi256_si128(b_low)); - - let (x_low_low, y_low_low, z_low_low) = - avx_rgb_to_sigmoidal(r_low_low, g_low_low, b_low_low); - - let u8_scale = _mm256_set1_ps(1f32 / 255f32); - - if USE_ALPHA { - let a_low_low = _mm256_mul_ps( - _mm256_cvtepi32_ps(_mm256_cvtepi16_epi32(_mm256_castsi256_si128(a_low))), - u8_scale, - ); - - let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); - avx_store_and_interleave_v4_f32!( - ptr, - image_configuration, - x_low_low, - y_low_low, - z_low_low, - a_low_low - ); - } else { - let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); - avx_store_and_interleave_v3_f32!( - ptr, - image_configuration, - x_low_low, - y_low_low, - z_low_low - ); - } - - let r_low_high = _mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(r_low)); - let g_low_high = _mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(g_low)); - let b_low_high = _mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(b_low)); - - let (x_low_high, y_low_high, z_low_high) = - avx_rgb_to_sigmoidal(r_low_high, g_low_high, b_low_high); - - if USE_ALPHA { - let a_low_high = _mm256_mul_ps( - _mm256_cvtepi32_ps(_mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(a_low))), - u8_scale, - ); - - let ptr = dst - .get_unchecked_mut(cx * channels + 8 * channels..) - .as_mut_ptr(); - avx_store_and_interleave_v4_f32!( - ptr, - image_configuration, - x_low_high, - y_low_high, - z_low_high, - a_low_high - ); - } else { - let ptr = dst - .get_unchecked_mut(cx * channels + 8 * channels..) - .as_mut_ptr(); - avx_store_and_interleave_v3_f32!( - ptr, - image_configuration, - x_low_high, - y_low_high, - z_low_high - ); - } - - let r_high = _mm256_cvtepu8_epi16(_mm256_extracti128_si256::<1>(r_chan)); - let g_high = _mm256_cvtepu8_epi16(_mm256_extracti128_si256::<1>(g_chan)); - let b_high = _mm256_cvtepu8_epi16(_mm256_extracti128_si256::<1>(b_chan)); - - let r_high_low = _mm256_cvtepu16_epi32(_mm256_castsi256_si128(r_high)); - let g_high_low = _mm256_cvtepu16_epi32(_mm256_castsi256_si128(g_high)); - let b_high_low = _mm256_cvtepu16_epi32(_mm256_castsi256_si128(b_high)); - - let (x_high_low, y_high_low, z_high_low) = - avx_rgb_to_sigmoidal(r_high_low, g_high_low, b_high_low); - - let a_high = _mm256_cvtepu8_epi16(_mm256_extracti128_si256::<1>(a_chan)); - - if USE_ALPHA { - let a_high_low = _mm256_mul_ps( - _mm256_cvtepi32_ps(_mm256_cvtepi16_epi32(_mm256_castsi256_si128(a_high))), - u8_scale, - ); - - let ptr = dst - .get_unchecked_mut(cx * channels + 8 * channels * 2..) - .as_mut_ptr(); - avx_store_and_interleave_v4_f32!( - ptr, - image_configuration, - x_high_low, - y_high_low, - z_high_low, - a_high_low - ); - } else { - let ptr = dst - .get_unchecked_mut(cx * channels + 8 * channels * 2..) - .as_mut_ptr(); - avx_store_and_interleave_v3_f32!( - ptr, - image_configuration, - x_high_low, - y_high_low, - z_high_low - ); - } - - let r_high_high = _mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(r_high)); - let g_high_high = _mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(g_high)); - let b_high_high = _mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(b_high)); - - let (x_high_high, y_high_high, z_high_high) = - avx_rgb_to_sigmoidal(r_high_high, g_high_high, b_high_high); - - if USE_ALPHA { - let a_high_high = _mm256_mul_ps( - _mm256_cvtepi32_ps(_mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(a_high))), - u8_scale, - ); - - let ptr = dst - .get_unchecked_mut(cx * channels + 8 * channels * 3..) - .as_mut_ptr(); - avx_store_and_interleave_v4_f32!( - ptr, - image_configuration, - x_high_high, - y_high_high, - z_high_high, - a_high_high - ); - } else { - let ptr = dst - .get_unchecked_mut(cx * channels + 8 * channels * 3..) - .as_mut_ptr(); - avx_store_and_interleave_v3_f32!( - ptr, - image_configuration, - x_high_high, - y_high_high, - z_high_high - ); + unsafe { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); + let (r_chan, g_chan, b_chan, a_chan) = + avx_vld_u8_and_deinterleave::(src_ptr); + + let r_low = _mm256_cvtepu8_epi16(_mm256_castsi256_si128(r_chan)); + let g_low = _mm256_cvtepu8_epi16(_mm256_castsi256_si128(g_chan)); + let b_low = _mm256_cvtepu8_epi16(_mm256_castsi256_si128(b_chan)); + let a_low = _mm256_cvtepu8_epi16(_mm256_castsi256_si128(a_chan)); + + let r_low_low = _mm256_cvtepu16_epi32(_mm256_castsi256_si128(r_low)); + let g_low_low = _mm256_cvtepu16_epi32(_mm256_castsi256_si128(g_low)); + let b_low_low = _mm256_cvtepu16_epi32(_mm256_castsi256_si128(b_low)); + + let (x_low_low, y_low_low, z_low_low) = + avx_rgb_to_sigmoidal(r_low_low, g_low_low, b_low_low); + + let u8_scale = _mm256_set1_ps(1f32 / 255f32); + + if USE_ALPHA { + let a_low_low = _mm256_mul_ps( + _mm256_cvtepi32_ps(_mm256_cvtepi16_epi32(_mm256_castsi256_si128(a_low))), + u8_scale, + ); + + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); + avx_store_and_interleave_v4_f32!( + ptr, + image_configuration, + x_low_low, + y_low_low, + z_low_low, + a_low_low + ); + } else { + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); + avx_store_and_interleave_v3_f32!( + ptr, + image_configuration, + x_low_low, + y_low_low, + z_low_low + ); + } + + let r_low_high = _mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(r_low)); + let g_low_high = _mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(g_low)); + let b_low_high = _mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(b_low)); + + let (x_low_high, y_low_high, z_low_high) = + avx_rgb_to_sigmoidal(r_low_high, g_low_high, b_low_high); + + if USE_ALPHA { + let a_low_high = _mm256_mul_ps( + _mm256_cvtepi32_ps(_mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(a_low))), + u8_scale, + ); + + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels..) + .as_mut_ptr(); + avx_store_and_interleave_v4_f32!( + ptr, + image_configuration, + x_low_high, + y_low_high, + z_low_high, + a_low_high + ); + } else { + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels..) + .as_mut_ptr(); + avx_store_and_interleave_v3_f32!( + ptr, + image_configuration, + x_low_high, + y_low_high, + z_low_high + ); + } + + let r_high = _mm256_cvtepu8_epi16(_mm256_extracti128_si256::<1>(r_chan)); + let g_high = _mm256_cvtepu8_epi16(_mm256_extracti128_si256::<1>(g_chan)); + let b_high = _mm256_cvtepu8_epi16(_mm256_extracti128_si256::<1>(b_chan)); + + let r_high_low = _mm256_cvtepu16_epi32(_mm256_castsi256_si128(r_high)); + let g_high_low = _mm256_cvtepu16_epi32(_mm256_castsi256_si128(g_high)); + let b_high_low = _mm256_cvtepu16_epi32(_mm256_castsi256_si128(b_high)); + + let (x_high_low, y_high_low, z_high_low) = + avx_rgb_to_sigmoidal(r_high_low, g_high_low, b_high_low); + + let a_high = _mm256_cvtepu8_epi16(_mm256_extracti128_si256::<1>(a_chan)); + + if USE_ALPHA { + let a_high_low = _mm256_mul_ps( + _mm256_cvtepi32_ps(_mm256_cvtepi16_epi32(_mm256_castsi256_si128(a_high))), + u8_scale, + ); + + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels * 2..) + .as_mut_ptr(); + avx_store_and_interleave_v4_f32!( + ptr, + image_configuration, + x_high_low, + y_high_low, + z_high_low, + a_high_low + ); + } else { + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels * 2..) + .as_mut_ptr(); + avx_store_and_interleave_v3_f32!( + ptr, + image_configuration, + x_high_low, + y_high_low, + z_high_low + ); + } + + let r_high_high = _mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(r_high)); + let g_high_high = _mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(g_high)); + let b_high_high = _mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>(b_high)); + + let (x_high_high, y_high_high, z_high_high) = + avx_rgb_to_sigmoidal(r_high_high, g_high_high, b_high_high); + + if USE_ALPHA { + let a_high_high = _mm256_mul_ps( + _mm256_cvtepi32_ps(_mm256_cvtepu16_epi32(_mm256_extracti128_si256::<1>( + a_high, + ))), + u8_scale, + ); + + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels * 3..) + .as_mut_ptr(); + avx_store_and_interleave_v4_f32!( + ptr, + image_configuration, + x_high_high, + y_high_high, + z_high_high, + a_high_high + ); + } else { + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels * 3..) + .as_mut_ptr(); + avx_store_and_interleave_v3_f32!( + ptr, + image_configuration, + x_high_high, + y_high_high, + z_high_high + ); + } } cx += 32; diff --git a/src/avx/to_xyz_lab.rs b/src/avx/to_xyz_lab.rs index 942910a..af50cb3 100644 --- a/src/avx/to_xyz_lab.rs +++ b/src/avx/to_xyz_lab.rs @@ -11,7 +11,7 @@ use std::arch::x86::*; use std::arch::x86_64::*; use crate::avx::cie::{ - avx2_triple_to_lab, avx2_triple_to_luv, avx2_triple_to_xyz, avx_triple_to_lch, + avx_triple_to_lch, avx2_triple_to_lab, avx2_triple_to_luv, avx2_triple_to_xyz, }; use crate::avx::routines::avx_vld_f32_and_deinterleave; use crate::avx::*; @@ -25,7 +25,7 @@ use crate::xyz_target::XyzTarget; use crate::{avx_store_and_interleave_v3_direct_f32, load_f32_and_deinterleave}; #[target_feature(enable = "avx2")] -pub(crate) unsafe fn avx2_image_to_xyz_lab< +pub(crate) fn avx2_image_to_xyz_lab< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, const TARGET: u8, @@ -45,106 +45,110 @@ pub(crate) unsafe fn avx2_image_to_xyz_lab< let channels = image_configuration.channel_count(); let mut cx = start_cx; - let cq1 = _mm256_set1_ps(*matrix.get_unchecked(0).get_unchecked(0)); - let cq2 = _mm256_set1_ps(*matrix.get_unchecked(0).get_unchecked(1)); - let cq3 = _mm256_set1_ps(*matrix.get_unchecked(0).get_unchecked(2)); - let cq4 = _mm256_set1_ps(*matrix.get_unchecked(1).get_unchecked(0)); - let cq5 = _mm256_set1_ps(*matrix.get_unchecked(1).get_unchecked(1)); - let cq6 = _mm256_set1_ps(*matrix.get_unchecked(1).get_unchecked(2)); - let cq7 = _mm256_set1_ps(*matrix.get_unchecked(2).get_unchecked(0)); - let cq8 = _mm256_set1_ps(*matrix.get_unchecked(2).get_unchecked(1)); - let cq9 = _mm256_set1_ps(*matrix.get_unchecked(2).get_unchecked(2)); + let cq1 = _mm256_set1_ps(matrix[0][0]); + let cq2 = _mm256_set1_ps(matrix[0][1]); + let cq3 = _mm256_set1_ps(matrix[0][2]); + let cq4 = _mm256_set1_ps(matrix[1][0]); + let cq5 = _mm256_set1_ps(matrix[1][1]); + let cq6 = _mm256_set1_ps(matrix[1][2]); + let cq7 = _mm256_set1_ps(matrix[2][0]); + let cq8 = _mm256_set1_ps(matrix[2][1]); + let cq9 = _mm256_set1_ps(matrix[2][2]); while cx + 8 <= width as usize { - let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); - let (r_chan, g_chan, b_chan, a_chan) = - avx_vld_f32_and_deinterleave::(src_ptr); - - let (mut x_low_low, mut y_low_low, mut z_low_low) = avx2_triple_to_xyz( - r_chan, g_chan, b_chan, cq1, cq2, cq3, cq4, cq5, cq6, cq7, cq8, cq9, - ); - - match target { - XyzTarget::Lab => { - let (l, a, b) = avx2_triple_to_lab(x_low_low, y_low_low, z_low_low); - x_low_low = l; - y_low_low = a; - z_low_low = b; + unsafe { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); + let (r_chan, g_chan, b_chan, a_chan) = + avx_vld_f32_and_deinterleave::(src_ptr); + + let (mut x_low_low, mut y_low_low, mut z_low_low) = avx2_triple_to_xyz( + r_chan, g_chan, b_chan, cq1, cq2, cq3, cq4, cq5, cq6, cq7, cq8, cq9, + ); + + match target { + XyzTarget::Lab => { + let (l, a, b) = avx2_triple_to_lab(x_low_low, y_low_low, z_low_low); + x_low_low = l; + y_low_low = a; + z_low_low = b; + } + XyzTarget::Xyz => {} + XyzTarget::Luv => { + let (l, u, v) = avx2_triple_to_luv(x_low_low, y_low_low, z_low_low); + x_low_low = l; + y_low_low = u; + z_low_low = v; + } + XyzTarget::Lch => { + let (l, c, h) = avx_triple_to_lch(x_low_low, y_low_low, z_low_low); + x_low_low = l; + y_low_low = c; + z_low_low = h; + } } - XyzTarget::Xyz => {} - XyzTarget::Luv => { - let (l, u, v) = avx2_triple_to_luv(x_low_low, y_low_low, z_low_low); - x_low_low = l; - y_low_low = u; - z_low_low = v; - } - XyzTarget::Lch => { - let (l, c, h) = avx_triple_to_lch(x_low_low, y_low_low, z_low_low); - x_low_low = l; - y_low_low = c; - z_low_low = h; - } - } - let write_dst_ptr = dst.get_unchecked_mut(cx * 3..).as_mut_ptr(); - avx_store_and_interleave_v3_direct_f32!(write_dst_ptr, x_low_low, y_low_low, z_low_low); + let write_dst_ptr = dst.get_unchecked_mut(cx * 3..).as_mut_ptr(); + avx_store_and_interleave_v3_direct_f32!(write_dst_ptr, x_low_low, y_low_low, z_low_low); - if USE_ALPHA { - _mm256_storeu_ps(a_linearized.get_unchecked_mut(cx), a_chan); + if USE_ALPHA { + _mm256_storeu_ps(a_linearized.get_unchecked_mut(cx), a_chan); + } } cx += 8; } while cx + 4 <= width as usize { - let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); - let (r_chan, g_chan, b_chan, a_chan) = - load_f32_and_deinterleave!(src_ptr, image_configuration); - - let (mut x_low_low, mut y_low_low, mut z_low_low) = sse_triple_to_xyz( - r_chan, - g_chan, - b_chan, - _mm256_castps256_ps128(cq1), - _mm256_castps256_ps128(cq2), - _mm256_castps256_ps128(cq3), - _mm256_castps256_ps128(cq4), - _mm256_castps256_ps128(cq5), - _mm256_castps256_ps128(cq6), - _mm256_castps256_ps128(cq7), - _mm256_castps256_ps128(cq8), - _mm256_castps256_ps128(cq9), - ); - - match target { - XyzTarget::Lab => { - let (l, a, b) = sse_triple_to_lab(x_low_low, y_low_low, z_low_low); - x_low_low = l; - y_low_low = a; - z_low_low = b; + unsafe { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); + let (r_chan, g_chan, b_chan, a_chan) = + load_f32_and_deinterleave!(src_ptr, image_configuration); + + let (mut x_low_low, mut y_low_low, mut z_low_low) = sse_triple_to_xyz( + r_chan, + g_chan, + b_chan, + _mm256_castps256_ps128(cq1), + _mm256_castps256_ps128(cq2), + _mm256_castps256_ps128(cq3), + _mm256_castps256_ps128(cq4), + _mm256_castps256_ps128(cq5), + _mm256_castps256_ps128(cq6), + _mm256_castps256_ps128(cq7), + _mm256_castps256_ps128(cq8), + _mm256_castps256_ps128(cq9), + ); + + match target { + XyzTarget::Lab => { + let (l, a, b) = sse_triple_to_lab(x_low_low, y_low_low, z_low_low); + x_low_low = l; + y_low_low = a; + z_low_low = b; + } + XyzTarget::Xyz => {} + XyzTarget::Luv => { + let (l, u, v) = sse_triple_to_luv(x_low_low, y_low_low, z_low_low); + x_low_low = l; + y_low_low = u; + z_low_low = v; + } + XyzTarget::Lch => { + let (l, c, h) = sse_triple_to_lch(x_low_low, y_low_low, z_low_low); + x_low_low = l; + y_low_low = c; + z_low_low = h; + } } - XyzTarget::Xyz => {} - XyzTarget::Luv => { - let (l, u, v) = sse_triple_to_luv(x_low_low, y_low_low, z_low_low); - x_low_low = l; - y_low_low = u; - z_low_low = v; - } - XyzTarget::Lch => { - let (l, c, h) = sse_triple_to_lch(x_low_low, y_low_low, z_low_low); - x_low_low = l; - y_low_low = c; - z_low_low = h; - } - } - let (v0, v1, v2) = sse_interleave_ps_rgb(x_low_low, y_low_low, z_low_low); - _mm_storeu_ps(dst.get_unchecked_mut(cx * 3), v0); - _mm_storeu_ps(dst.get_unchecked_mut(cx * 3 + 4), v1); - _mm_storeu_ps(dst.get_unchecked_mut(cx * 3 + 8), v2); + let (v0, v1, v2) = sse_interleave_ps_rgb(x_low_low, y_low_low, z_low_low); + _mm_storeu_ps(dst.get_unchecked_mut(cx * 3), v0); + _mm_storeu_ps(dst.get_unchecked_mut(cx * 3 + 4), v1); + _mm_storeu_ps(dst.get_unchecked_mut(cx * 3 + 8), v2); - if USE_ALPHA { - _mm_storeu_ps(a_linearized.get_unchecked_mut(cx), a_chan); + if USE_ALPHA { + _mm_storeu_ps(a_linearized.get_unchecked_mut(cx), a_chan); + } } cx += 4; diff --git a/src/avx/utils.rs b/src/avx/utils.rs index 0592a60..2b5a090 100644 --- a/src/avx/utils.rs +++ b/src/avx/utils.rs @@ -11,8 +11,9 @@ use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; -#[inline(always)] -pub unsafe fn avx2_interleave_rgb_ps(a: __m256, b: __m256, c: __m256) -> (__m256, __m256, __m256) { +#[inline] +#[target_feature(enable = "avx2")] +pub(crate) fn avx2_interleave_rgb_ps(a: __m256, b: __m256, c: __m256) -> (__m256, __m256, __m256) { let b0 = _mm256_shuffle_epi32::<0x6c>(_mm256_castps_si256(a)); let g0 = _mm256_shuffle_epi32::<0xb1>(_mm256_castps_si256(b)); let r0 = _mm256_shuffle_epi32::<0xc6>(_mm256_castps_si256(c)); @@ -32,7 +33,7 @@ pub unsafe fn avx2_interleave_rgb_ps(a: __m256, b: __m256, c: __m256) -> (__m256 #[inline] #[target_feature(enable = "avx2")] -pub unsafe fn _mm256_packus_four_epi32(a: __m256i, b: __m256i, c: __m256i, d: __m256i) -> __m256i { +pub(crate) fn _mm256_packus_four_epi32(a: __m256i, b: __m256i, c: __m256i, d: __m256i) -> __m256i { let ab = _mm256_packs_epi32(a, b); let cd = _mm256_packs_epi32(c, d); diff --git a/src/avx/xyz_lab_to_image.rs b/src/avx/xyz_lab_to_image.rs index 8530934..b3fda3e 100644 --- a/src/avx/xyz_lab_to_image.rs +++ b/src/avx/xyz_lab_to_image.rs @@ -22,12 +22,9 @@ use crate::{ store_and_interleave_v4_f32, }; -#[inline(always)] -unsafe fn avx_xyz_lab_vld< - const CHANNELS_CONFIGURATION: u8, - const USE_ALPHA: bool, - const TARGET: u8, ->( +#[inline] +#[target_feature(enable = "avx2")] +fn avx_xyz_lab_vld( src: *const f32, c1: __m256, c2: __m256, @@ -39,54 +36,53 @@ unsafe fn avx_xyz_lab_vld< c8: __m256, c9: __m256, ) -> (__m256, __m256, __m256) { - let target: XyzTarget = TARGET.into(); - let lab_pixel_0 = _mm256_loadu_ps(src); - let lab_pixel_1 = _mm256_loadu_ps(src.add(8)); - let lab_pixel_2 = _mm256_loadu_ps(src.add(16)); - let (mut r_f32, mut g_f32, mut b_f32) = - avx2_deinterleave_rgb_ps(lab_pixel_0, lab_pixel_1, lab_pixel_2); - - match target { - XyzTarget::Lab => { - let (x, y, z) = avx_lab_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; - } - XyzTarget::Luv => { - let (x, y, z) = avx_luv_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; - } - XyzTarget::Lch => { - let (x, y, z) = avx_lch_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; + unsafe { + let target: XyzTarget = TARGET.into(); + let lab_pixel_0 = _mm256_loadu_ps(src); + let lab_pixel_1 = _mm256_loadu_ps(src.add(8)); + let lab_pixel_2 = _mm256_loadu_ps(src.add(16)); + let (mut r_f32, mut g_f32, mut b_f32) = + avx2_deinterleave_rgb_ps(lab_pixel_0, lab_pixel_1, lab_pixel_2); + + match target { + XyzTarget::Lab => { + let (x, y, z) = avx_lab_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + XyzTarget::Luv => { + let (x, y, z) = avx_luv_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + XyzTarget::Lch => { + let (x, y, z) = avx_lch_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + _ => {} } - _ => {} - } - let (linear_r, linear_g, linear_b) = - _mm256_color_matrix_ps(r_f32, g_f32, b_f32, c1, c2, c3, c4, c5, c6, c7, c8, c9); + let (linear_r, linear_g, linear_b) = + _mm256_color_matrix_ps(r_f32, g_f32, b_f32, c1, c2, c3, c4, c5, c6, c7, c8, c9); - (linear_r, linear_g, linear_b) + (linear_r, linear_g, linear_b) + } } #[target_feature(enable = "avx2")] -pub unsafe fn avx_xyz_to_channels< +pub(crate) fn avx_xyz_to_channels< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, const TARGET: u8, >( start_cx: usize, - src: *const f32, - src_offset: usize, - a_channel: *const f32, - a_offset: usize, - dst: *mut f32, - dst_offset: usize, + src: &[f32], + a_channel: &[f32], + dst: &mut [f32], width: u32, matrix: &[[f32; 3]; 3], ) -> usize { @@ -99,20 +95,20 @@ pub unsafe fn avx_xyz_to_channels< let mut cx = start_cx; - let c1 = _mm256_set1_ps(*matrix.get_unchecked(0).get_unchecked(0)); - let c2 = _mm256_set1_ps(*matrix.get_unchecked(0).get_unchecked(1)); - let c3 = _mm256_set1_ps(*matrix.get_unchecked(0).get_unchecked(2)); - let c4 = _mm256_set1_ps(*matrix.get_unchecked(1).get_unchecked(0)); - let c5 = _mm256_set1_ps(*matrix.get_unchecked(1).get_unchecked(1)); - let c6 = _mm256_set1_ps(*matrix.get_unchecked(1).get_unchecked(2)); - let c7 = _mm256_set1_ps(*matrix.get_unchecked(2).get_unchecked(0)); - let c8 = _mm256_set1_ps(*matrix.get_unchecked(2).get_unchecked(1)); - let c9 = _mm256_set1_ps(*matrix.get_unchecked(2).get_unchecked(2)); + let c1 = _mm256_set1_ps(matrix[0][0]); + let c2 = _mm256_set1_ps(matrix[0][1]); + let c3 = _mm256_set1_ps(matrix[0][2]); + let c4 = _mm256_set1_ps(matrix[1][0]); + let c5 = _mm256_set1_ps(matrix[1][1]); + let c6 = _mm256_set1_ps(matrix[1][2]); + let c7 = _mm256_set1_ps(matrix[2][0]); + let c8 = _mm256_set1_ps(matrix[2][1]); + let c9 = _mm256_set1_ps(matrix[2][2]); const CHANNELS: usize = 3usize; - while cx + 8 < width as usize { - let offset_src_ptr = ((src as *const u8).add(src_offset) as *const f32).add(cx * CHANNELS); + while cx + 8 <= width as usize { + let offset_src_ptr = unsafe { src.get_unchecked(cx * CHANNELS..).as_ptr() }; let src_ptr_0 = offset_src_ptr; @@ -121,35 +117,39 @@ pub unsafe fn avx_xyz_to_channels< src_ptr_0, c1, c2, c3, c4, c5, c6, c7, c8, c9, ); - let dst_ptr = ((dst as *mut u8).add(dst_offset) as *mut f32).add(cx * channels); + let dst_ptr = unsafe { dst.get_unchecked_mut(cx * channels..).as_mut_ptr() }; if USE_ALPHA { - let offset_a_src_ptr = ((a_channel as *const u8).add(a_offset) as *const f32).add(cx); - let a_row = _mm256_loadu_ps(offset_a_src_ptr); - - avx_store_and_interleave_v4_f32!( - dst_ptr, - image_configuration, - r_row0_, - g_row0_, - b_row0_, - a_row - ); + let offset_a_src_ptr = unsafe { a_channel.get_unchecked(cx..).as_ptr() }; + let a_row = unsafe { _mm256_loadu_ps(offset_a_src_ptr) }; + + unsafe { + avx_store_and_interleave_v4_f32!( + dst_ptr, + image_configuration, + r_row0_, + g_row0_, + b_row0_, + a_row + ); + } } else { - avx_store_and_interleave_v3_f32!( - dst_ptr, - image_configuration, - r_row0_, - g_row0_, - b_row0_ - ); + unsafe { + avx_store_and_interleave_v3_f32!( + dst_ptr, + image_configuration, + r_row0_, + g_row0_, + b_row0_ + ); + } } cx += 8; } - while cx + 4 < width as usize { - let offset_src_ptr = ((src as *const u8).add(src_offset) as *const f32).add(cx * channels); + while cx + 4 <= width as usize { + let offset_src_ptr = unsafe { src.get_unchecked(cx * channels..).as_ptr() }; let src_ptr_0 = offset_src_ptr; @@ -167,22 +167,32 @@ pub unsafe fn avx_xyz_to_channels< _mm256_castps256_ps128(c9), ); - let dst_ptr = ((dst as *mut u8).add(dst_offset) as *mut f32).add(cx * channels); + let dst_ptr = unsafe { dst.get_unchecked_mut(cx * channels..).as_mut_ptr() }; if USE_ALPHA { - let offset_a_src_ptr = ((a_channel as *const u8).add(a_offset) as *const f32).add(cx); - let a_row = _mm_loadu_ps(offset_a_src_ptr); - - store_and_interleave_v4_f32!( - dst_ptr, - image_configuration, - r_row0_, - g_row0_, - b_row0_, - a_row - ); + unsafe { + let offset_a_src_ptr = a_channel.get_unchecked(cx..).as_ptr(); + let a_row = _mm_loadu_ps(offset_a_src_ptr); + + store_and_interleave_v4_f32!( + dst_ptr, + image_configuration, + r_row0_, + g_row0_, + b_row0_, + a_row + ); + } } else { - store_and_interleave_v3_f32!(dst_ptr, image_configuration, r_row0_, g_row0_, b_row0_); + unsafe { + store_and_interleave_v3_f32!( + dst_ptr, + image_configuration, + r_row0_, + g_row0_, + b_row0_ + ); + } } cx += 4; diff --git a/src/avx/xyza_laba_to_image.rs b/src/avx/xyza_laba_to_image.rs index 00b73e2..b24e29d 100644 --- a/src/avx/xyza_laba_to_image.rs +++ b/src/avx/xyza_laba_to_image.rs @@ -18,8 +18,9 @@ use crate::image::ImageConfiguration; use crate::sse::{sse_interleave_ps_rgba, sse_xyza_lab_vld}; use crate::xyz_target::XyzTarget; -#[inline(always)] -unsafe fn avx_xyza_lab_vld( +#[inline] +#[target_feature(enable = "avx2")] +fn avx_xyza_lab_vld( src: *const f32, c1: __m256, c2: __m256, @@ -31,49 +32,49 @@ unsafe fn avx_xyza_lab_vld( c8: __m256, c9: __m256, ) -> (__m256, __m256, __m256, __m256) { - let target: XyzTarget = TARGET.into(); - let pixel_0 = _mm256_loadu_ps(src); - let pixel_1 = _mm256_loadu_ps(src.add(8)); - let pixel_2 = _mm256_loadu_ps(src.add(16)); - let pixel_3 = _mm256_loadu_ps(src.add(24)); - let (mut r_f32, mut g_f32, mut b_f32, a_f32) = - avx2_deinterleave_rgba_ps(pixel_0, pixel_1, pixel_2, pixel_3); - - match target { - XyzTarget::Lab => { - let (x, y, z) = avx_lab_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; - } - XyzTarget::Luv => { - let (x, y, z) = avx_luv_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; - } - XyzTarget::Lch => { - let (x, y, z) = avx_lch_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; + unsafe { + let target: XyzTarget = TARGET.into(); + let pixel_0 = _mm256_loadu_ps(src); + let pixel_1 = _mm256_loadu_ps(src.add(8)); + let pixel_2 = _mm256_loadu_ps(src.add(16)); + let pixel_3 = _mm256_loadu_ps(src.add(24)); + let (mut r_f32, mut g_f32, mut b_f32, a_f32) = + avx2_deinterleave_rgba_ps(pixel_0, pixel_1, pixel_2, pixel_3); + + match target { + XyzTarget::Lab => { + let (x, y, z) = avx_lab_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + XyzTarget::Luv => { + let (x, y, z) = avx_luv_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + XyzTarget::Lch => { + let (x, y, z) = avx_lch_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + _ => {} } - _ => {} - } - let (linear_r, linear_g, linear_b) = - _mm256_color_matrix_ps(r_f32, g_f32, b_f32, c1, c2, c3, c4, c5, c6, c7, c8, c9); + let (linear_r, linear_g, linear_b) = + _mm256_color_matrix_ps(r_f32, g_f32, b_f32, c1, c2, c3, c4, c5, c6, c7, c8, c9); - (linear_r, linear_g, linear_b, a_f32) + (linear_r, linear_g, linear_b, a_f32) + } } -#[target_feature(enable = "sse4.1")] -pub unsafe fn avx_xyza_to_image( +#[target_feature(enable = "avx2")] +pub fn avx_xyza_to_image( start_cx: usize, - src: *const f32, - src_offset: usize, - dst: *mut f32, - dst_offset: usize, + src: &[f32], + dst: &mut [f32], width: u32, matrix: &[[f32; 3]; 3], ) -> usize { @@ -86,43 +87,46 @@ pub unsafe fn avx_xyza_to_image( - src_ptr_0, c1, c2, c3, c4, c5, c6, c7, c8, c9, - ); + let (r_row0_, g_row0_, b_row0_, a_row0_) = + avx_xyza_lab_vld::( + src_ptr_0, c1, c2, c3, c4, c5, c6, c7, c8, c9, + ); - let dst_ptr = ((dst as *mut u8).add(dst_offset) as *mut f32).add(cx * channels); + let dst_ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); - avx_store_and_interleave_v4_f32!( - dst_ptr, - image_configuration, - r_row0_, - g_row0_, - b_row0_, - a_row0_ - ); + avx_store_and_interleave_v4_f32!( + dst_ptr, + image_configuration, + r_row0_, + g_row0_, + b_row0_, + a_row0_ + ); + } cx += 8; } - while cx + 4 < width as usize { - let offset_src_ptr = ((src as *const u8).add(src_offset) as *const f32).add(cx * CHANNELS); + while cx + 4 <= width as usize { + let offset_src_ptr = unsafe { src.get_unchecked(cx * CHANNELS..).as_ptr() }; let src_ptr_0 = offset_src_ptr; @@ -139,7 +143,7 @@ pub unsafe fn avx_xyza_to_image { @@ -150,10 +154,12 @@ pub unsafe fn avx_xyza_to_image( *lut = transfer_function.linearize(i as f32 * (1. / 255.0)); } - let mut a_lut_table = [0f32; 256]; + let mut a_lut_table = [0.; 256]; for (i, lut) in a_lut_table.iter_mut().enumerate() { *lut = i as f32 * (1. / 255.0); } diff --git a/src/image_to_oklab.rs b/src/image_to_oklab.rs index 432aa24..e1ae01c 100644 --- a/src/image_to_oklab.rs +++ b/src/image_to_oklab.rs @@ -13,8 +13,8 @@ use crate::oklch::Oklch; #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] use crate::sse::sse_image_to_oklab; use crate::{ - bgr_to_linear, bgra_to_linear, rgb_to_linear, rgba_to_linear, ColorError, ImageBuffer, - ImageBufferMut, Oklab, Rgb, TransferFunction, + ColorError, ImageBuffer, ImageBufferMut, Oklab, Rgb, TransferFunction, bgr_to_linear, + bgra_to_linear, rgb_to_linear, rgba_to_linear, }; #[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] @@ -92,7 +92,7 @@ fn channels_to_oklab( cx = unsafe { dispatcher(cx, width, dst) }; } - let rem = &mut dst_row[cx..]; + let rem = &mut dst_row[cx * image_configuration.channel_count()..]; if image_configuration.has_alpha() { for dst in rem.as_chunks_mut::<4>().0.iter_mut() { let rgb = Rgb::::new( diff --git a/src/image_to_xyz_lab.rs b/src/image_to_xyz_lab.rs index 5ab8e62..23eb4ee 100644 --- a/src/image_to_xyz_lab.rs +++ b/src/image_to_xyz_lab.rs @@ -8,7 +8,7 @@ use crate::gamma_curves::TransferFunction; use crate::image::ImageConfiguration; use crate::xyz_target::XyzTarget; use crate::{ - BufferStore, ColorError, ImageBuffer, ImageBufferMut, LCh, Lab, Luv, Rgb, Xyz, SRGB_TO_XYZ_D65, + BufferStore, ColorError, ImageBuffer, ImageBufferMut, LCh, Lab, Luv, Rgb, SRGB_TO_XYZ_D65, Xyz, }; #[allow(clippy::type_complexity)] @@ -21,13 +21,16 @@ fn channels_to_xyz Result<(), ColorError> { src.validate()?; dst.validate()?; - dst.try_match_immutable_with_channels(src)?; + dst.try_match_immutable(src)?; if USE_ALPHA { dst.try_match(a_plane)?; } if src.channels != 3 && src.channels != 4 { return Err(ColorError::UnsupportedChannelsCount(src.channels)); } + if dst.channels != 3 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } if USE_ALPHA { if a_plane.channels != 1 { return Err(ColorError::UnsupportedChannelsCount(a_plane.channels)); @@ -103,9 +106,9 @@ fn channels_to_xyz().0.iter()) { - dst[0] = lut_table[src[0] as usize]; - dst[1] = lut_table[src[1] as usize]; - dst[2] = lut_table[src[2] as usize]; + dst[0] = lut_table[src[image_configuration.r_index()] as usize]; + dst[1] = lut_table[src[image_configuration.g_index()] as usize]; + dst[2] = lut_table[src[image_configuration.b_index()] as usize]; } } else if image_configuration == ImageConfiguration::Bgra || image_configuration == ImageConfiguration::Rgba @@ -116,10 +119,10 @@ fn channels_to_xyz().0.iter()) { - dst[0] = lut_table[src[0] as usize]; - dst[1] = lut_table[src[1] as usize]; - dst[2] = lut_table[src[2] as usize]; - dst[3] = a_lut_table[src[3] as usize]; + dst[0] = lut_table[src[image_configuration.r_index()] as usize]; + dst[1] = lut_table[src[image_configuration.g_index()] as usize]; + dst[2] = lut_table[src[image_configuration.b_index()] as usize]; + dst[3] = a_lut_table[src[image_configuration.a_index()] as usize]; } } @@ -238,17 +241,10 @@ pub fn rgb_to_xyz( matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, ) -> Result<(), ColorError> { - let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Xyz as u8 }>( src, dst, - &mut ImageBufferMut::new( - BufferStore::Owned(empty_vec), - dst.width, - dst.height, - dst.width, - 1, - )?, + &mut ImageBufferMut::new(BufferStore::Owned(vec![0.; 1]), 1, 1, 1, 1)?, matrix, transfer_function, ) @@ -271,17 +267,10 @@ pub fn bgr_to_xyz( matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, ) -> Result<(), ColorError> { - let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Bgr as u8 }, false, { XyzTarget::Xyz as u8 }>( src, dst, - &mut ImageBufferMut::new( - BufferStore::Owned(empty_vec), - dst.width, - dst.height, - dst.width, - 1, - )?, + &mut ImageBufferMut::new(BufferStore::Owned(vec![0.; 1]), 1, 1, 1, 1)?, matrix, transfer_function, ) @@ -302,17 +291,10 @@ pub fn srgb_to_xyz( src: &ImageBuffer<'_, u8>, dst: &mut ImageBufferMut<'_, f32>, ) -> Result<(), ColorError> { - let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Xyz as u8 }>( src, dst, - &mut ImageBufferMut::new( - BufferStore::Owned(empty_vec), - dst.width, - dst.height, - dst.width, - 1, - )?, + &mut ImageBufferMut::new(BufferStore::Owned(vec![0.; 1]), 1, 1, 1, 1)?, &SRGB_TO_XYZ_D65, TransferFunction::Srgb, ) @@ -335,11 +317,10 @@ pub fn rgb_to_lab( matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, ) -> Result<(), ColorError> { - let empty_vec = vec![0.; 1]; channels_to_xyz::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Lab as u8 }>( src, dst, - &mut ImageBufferMut::new(BufferStore::Owned(empty_vec), 1, 1, 1, 1)?, + &mut ImageBufferMut::new(BufferStore::Owned(vec![0.; 1]), 1, 1, 1, 1)?, matrix, transfer_function, ) @@ -362,11 +343,10 @@ pub fn rgba_to_xyz( matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, ) -> Result<(), ColorError> { - let empty_vec = vec![0.; 1]; channels_to_xyz::<{ ImageConfiguration::Rgba as u8 }, false, { XyzTarget::Xyz as u8 }>( src, dst, - &mut ImageBufferMut::new(BufferStore::Owned(empty_vec), 1, 1, 1, 1)?, + &mut ImageBufferMut::new(BufferStore::Owned(vec![0.; 1]), 1, 1, 1, 1)?, matrix, transfer_function, ) @@ -387,11 +367,10 @@ pub fn srgba_to_xyz( src: &ImageBuffer<'_, u8>, dst: &mut ImageBufferMut<'_, f32>, ) -> Result<(), ColorError> { - let empty_vec = vec![0.; 1]; channels_to_xyz::<{ ImageConfiguration::Rgba as u8 }, false, { XyzTarget::Xyz as u8 }>( src, dst, - &mut ImageBufferMut::new(BufferStore::Owned(empty_vec), 1, 1, 1, 1)?, + &mut ImageBufferMut::new(BufferStore::Owned(vec![0.; 1]), 1, 1, 1, 1)?, &SRGB_TO_XYZ_D65, TransferFunction::Srgb, ) @@ -553,17 +532,10 @@ pub fn bgr_to_lab( matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, ) -> Result<(), ColorError> { - let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Bgr as u8 }, false, { XyzTarget::Lab as u8 }>( src, dst, - &mut ImageBufferMut::new( - BufferStore::Owned(empty_vec), - dst.width, - dst.height, - dst.width, - 1, - )?, + &mut ImageBufferMut::new(BufferStore::Owned(vec![0.; 1]), 1, 1, 1, 1)?, matrix, transfer_function, ) @@ -586,17 +558,10 @@ pub fn rgb_to_luv( matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, ) -> Result<(), ColorError> { - let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Luv as u8 }>( src, dst, - &mut ImageBufferMut::new( - BufferStore::Owned(empty_vec), - dst.width, - dst.height, - dst.width, - 1, - )?, + &mut ImageBufferMut::new(BufferStore::Owned(vec![0.; 1]), 1, 1, 1, 1)?, matrix, transfer_function, ) @@ -619,17 +584,10 @@ pub fn bgr_to_luv( matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, ) -> Result<(), ColorError> { - let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Bgr as u8 }, false, { XyzTarget::Luv as u8 }>( src, dst, - &mut ImageBufferMut::new( - BufferStore::Owned(empty_vec), - dst.width, - dst.height, - dst.width, - 1, - )?, + &mut ImageBufferMut::new(BufferStore::Owned(vec![0.; 1]), 1, 1, 1, 1)?, matrix, transfer_function, ) @@ -652,17 +610,10 @@ pub fn rgb_to_lch( matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, ) -> Result<(), ColorError> { - let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Lch as u8 }>( src, dst, - &mut ImageBufferMut::new( - BufferStore::Owned(empty_vec), - dst.width, - dst.height, - dst.width, - 1, - )?, + &mut ImageBufferMut::new(BufferStore::Owned(vec![0.; 1]), 1, 1, 1, 1)?, matrix, transfer_function, ) @@ -685,18 +636,294 @@ pub fn bgr_to_lch( matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, ) -> Result<(), ColorError> { - let empty_vec = vec![0.; dst.width as usize * dst.height as usize]; channels_to_xyz::<{ ImageConfiguration::Bgr as u8 }, false, { XyzTarget::Lch as u8 }>( src, dst, - &mut ImageBufferMut::new( - BufferStore::Owned(empty_vec), - dst.width, - dst.height, - dst.width, - 1, - )?, + &mut ImageBufferMut::new(BufferStore::Owned(vec![0.; 1]), 1, 1, 1, 1)?, matrix, transfer_function, ) } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_src(data: Vec, width: u32, height: u32, channels: u32) -> ImageBuffer<'static, u8> { + ImageBuffer::from_vec(data, width, height, width * channels, channels).unwrap() + } + + fn make_dst(width: u32, height: u32, channels: u32) -> ImageBufferMut<'static, f32> { + let len = (width * height * channels) as usize; + ImageBufferMut::new( + BufferStore::Owned(vec![0f32; len]), + width, + height, + width * channels, + channels, + ) + .unwrap() + } + + fn make_alpha(width: u32, height: u32) -> ImageBufferMut<'static, f32> { + let len = (width * height) as usize; + ImageBufferMut::new(BufferStore::Owned(vec![0f32; len]), width, height, width, 1).unwrap() + } + + // ── helpers ─────────────────────────────────────────────────────────────── + + fn assert_approx(a: f32, b: f32, eps: f32, label: &str) { + assert!((a - b).abs() < eps, "{label}: expected {b}, got {a}"); + } + + // ── black / white sanity ────────────────────────────────────────────────── + + #[test] + fn rgb_to_xyz_black() { + let src = make_src(vec![0u8; 3], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + rgb_to_xyz(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 0.0, 1e-5, "X"); + assert_approx(d[1], 0.0, 1e-5, "Y"); + assert_approx(d[2], 0.0, 1e-5, "Z"); + } + + #[test] + fn rgb_to_xyz_white() { + let src = make_src(vec![255u8; 3], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + rgb_to_xyz(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + // D65 white point + assert_approx(d[0], 0.9505, 1e-3, "X"); + assert_approx(d[1], 1.0000, 1e-3, "Y"); + assert_approx(d[2], 1.0890, 1e-3, "Z"); + } + + #[test] + fn bgr_to_xyz_white() { + // BGR white is identical to RGB white + let src = make_src(vec![255u8; 3], 1, 1, 3); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + rgb_to_xyz(&src, &mut dst_rgb, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + bgr_to_xyz(&src, &mut dst_bgr, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let r = dst_rgb.data.borrow(); + let b = dst_bgr.data.borrow(); + for i in 0..3 { + assert_approx(b[i], r[i], 1e-5, &format!("channel {i}")); + } + } + + // ── sRGB convenience wrappers ───────────────────────────────────────────── + + #[test] + fn srgb_to_xyz_matches_rgb_to_xyz() { + let data = vec![100u8, 150u8, 200u8]; + let src = make_src(data.clone(), 1, 1, 3); + let src2 = make_src(data, 1, 1, 3); + let mut dst1 = make_dst(1, 1, 3); + let mut dst2 = make_dst(1, 1, 3); + srgb_to_xyz(&src, &mut dst1).unwrap(); + rgb_to_xyz(&src2, &mut dst2, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let d1 = dst1.data.borrow(); + let d2 = dst2.data.borrow(); + for i in 0..3 { + assert_approx(d1[i], d2[i], 1e-6, &format!("channel {i}")); + } + } + + #[test] + fn srgba_to_xyz_matches_rgba_to_xyz() { + let data = vec![100u8, 150u8, 200u8, 255u8]; + let src = make_src(data.clone(), 1, 1, 4); + let src2 = make_src(data, 1, 1, 4); + let mut dst1 = make_dst(1, 1, 3); + let mut dst2 = make_dst(1, 1, 3); + srgba_to_xyz(&src, &mut dst1).unwrap(); + rgba_to_xyz(&src2, &mut dst2, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let d1 = dst1.data.borrow(); + let d2 = dst2.data.borrow(); + for i in 0..3 { + assert_approx(d1[i], d2[i], 1e-6, &format!("channel {i}")); + } + } + + // ── Lab ─────────────────────────────────────────────────────────────────── + + #[test] + fn rgb_to_lab_white() { + let src = make_src(vec![255u8; 3], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + rgb_to_lab(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 100.0, 0.5, "L*"); + assert_approx(d[1], 0.0, 0.5, "a*"); + assert_approx(d[2], 0.0, 0.5, "b*"); + } + + #[test] + fn rgb_to_lab_black() { + let src = make_src(vec![0u8; 3], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + rgb_to_lab(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 0.0, 0.1, "L*"); + } + + #[test] + fn bgr_to_lab_matches_rgb_to_lab_for_grey() { + // Grey pixels are BGR-invariant + let data = vec![128u8, 128u8, 128u8]; + let src_rgb = make_src(data.clone(), 1, 1, 3); + let src_bgr = make_src(data, 1, 1, 3); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + rgb_to_lab( + &src_rgb, + &mut dst_rgb, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + bgr_to_lab( + &src_bgr, + &mut dst_bgr, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + let r = dst_rgb.data.borrow(); + let b = dst_bgr.data.borrow(); + for i in 0..3 { + assert_approx(b[i], r[i], 1e-4, &format!("channel {i}")); + } + } + + // ── RGBA → XYZA (alpha preservation) ───────────────────────────────────── + + #[test] + fn rgba_to_xyza_alpha_passthrough() { + // Alpha = 128 → normalised to 128/255 ≈ 0.502 + let src = make_src(vec![255, 255, 255, 128], 1, 1, 4); + let mut dst = make_dst(1, 1, 3); + let mut alpha = make_alpha(1, 1); + rgba_to_xyza( + &src, + &mut dst, + &mut alpha, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + let a = alpha.data.borrow(); + assert_approx(a[0], 128.0 / 255.0, 1e-4, "alpha"); + } + + #[test] + fn srgba_to_xyza_alpha_passthrough() { + let src = make_src(vec![0, 0, 0, 64], 1, 1, 4); + let mut dst = make_dst(1, 1, 3); + let mut alpha = make_alpha(1, 1); + srgba_to_xyza(&src, &mut dst, &mut alpha).unwrap(); + let a = alpha.data.borrow(); + assert_approx(a[0], 64.0 / 255.0, 1e-4, "alpha"); + } + + #[test] + fn bgr_to_luv_matches_rgb_for_grey() { + let data = vec![200u8, 200u8, 200u8]; + let src_rgb = make_src(data.clone(), 1, 1, 3); + let src_bgr = make_src(data, 1, 1, 3); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + rgb_to_luv( + &src_rgb, + &mut dst_rgb, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + bgr_to_luv( + &src_bgr, + &mut dst_bgr, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + let r = dst_rgb.data.borrow(); + let b = dst_bgr.data.borrow(); + for i in 0..3 { + assert_approx(b[i], r[i], 1e-4, &format!("channel {i}")); + } + } + + #[test] + fn bgr_to_lch_matches_rgb_for_grey() { + let data = vec![100u8, 100u8, 100u8]; + let src_rgb = make_src(data.clone(), 1, 1, 3); + let src_bgr = make_src(data, 1, 1, 3); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + rgb_to_lch( + &src_rgb, + &mut dst_rgb, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + bgr_to_lch( + &src_bgr, + &mut dst_bgr, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + let r = dst_rgb.data.borrow(); + let b = dst_bgr.data.borrow(); + for i in 0..3 { + assert_approx(b[i], r[i], 1e-4, &format!("channel {i}")); + } + } + + // ── multi-pixel / stride ────────────────────────────────────────────────── + + #[test] + fn rgb_to_xyz_multi_pixel_consistent() { + // 3 identical pixels — all outputs must be identical + let pixel = [100u8, 149u8, 237u8]; // cornflower blue-ish + let src = make_src(pixel.repeat(3), 3, 1, 3); + let mut dst = make_dst(3, 1, 3); + rgb_to_xyz(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + for px in 1..3 { + for ch in 0..3 { + assert_approx(d[px * 3 + ch], d[ch], 1e-5, &format!("px{px} ch{ch}")); + } + } + } + + #[test] + fn rgb_to_lab_multi_row() { + // 1x2 image, both rows identical white → both Lab rows must match + let src = make_src(vec![255u8; 6], 1, 2, 3); + let mut dst = make_dst(1, 2, 3); + rgb_to_lab(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + for ch in 0..3 { + assert_approx(d[ch], d[3 + ch], 1e-5, &format!("row mismatch ch{ch}")); + } + } + + // ── error paths ─────────────────────────────────────────────────────────── + + #[test] + fn wrong_channel_count_returns_error() { + // 2-channel source is unsupported + let src = make_src(vec![0u8; 2], 1, 1, 2); + let mut dst = make_dst(1, 1, 3); + let result = rgb_to_xyz(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb); + assert!(result.is_err()); + } +} diff --git a/src/image_xyza_laba.rs b/src/image_xyza_laba.rs index 8dec094..96334dc 100644 --- a/src/image_xyza_laba.rs +++ b/src/image_xyza_laba.rs @@ -6,29 +6,22 @@ */ use crate::image::ImageConfiguration; -#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -use crate::neon::neon_channels_to_xyza_or_laba; -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -use crate::sse::sse_channels_to_xyza_laba; use crate::xyz_target::XyzTarget; -use crate::{LCh, Lab, Luv, Rgb, TransferFunction, Xyz}; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -use std::slice; +use crate::{ColorError, ImageBuffer, ImageBufferMut, LCh, Lab, Luv, Rgb, TransferFunction, Xyz}; #[allow(clippy::type_complexity)] fn channels_to_xyz_with_alpha( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { + src.validate()?; + dst.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } let target: XyzTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); if !image_configuration.has_alpha() { @@ -36,127 +29,170 @@ fn channels_to_xyz_with_alpha usize, + unsafe fn(usize, &[f32], &mut [f32], u32, &[[f32; 3]; 3]) -> usize, > = None; #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] { if std::arch::is_x86_feature_detected!("sse4.1") { + use crate::sse::sse_channels_to_xyza_laba; _wide_row_handler = Some(sse_channels_to_xyza_laba::); } } #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] { + use crate::neon::neon_channels_to_xyza_or_laba; _wide_row_handler = Some(neon_channels_to_xyza_or_laba::); } let channels = image_configuration.channel_count(); - let mut lut_table = vec![0f32; 256]; + let mut lut_table = [0.; 256]; for (i, lut) in lut_table.iter_mut().enumerate() { *lut = transfer_function.linearize(i as f32 * (1. / 255.0)); } - let dst_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - dst.as_mut_ptr() as *mut u8, - dst_stride as usize * height as usize, - ) - }; - - let iter; - - #[cfg(feature = "rayon")] - { - iter = dst_slice_safe_align - .par_chunks_exact_mut(dst_stride as usize) - .zip(src.par_chunks_exact(src_stride as usize)); - } - #[cfg(not(feature = "rayon"))] - { - iter = dst_slice_safe_align - .chunks_exact_mut(dst_stride as usize) - .zip(src.chunks_exact(src_stride as usize)); + let mut a_lut_table = [0.; 256]; + for (i, lut) in a_lut_table.iter_mut().enumerate() { + *lut = i as f32 * (1. / 255.0); } - iter.for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; + let mut transient_row = vec![0f32; src.width as usize * channels]; + let width = src.width; - let mut transient_row = vec![0f32; width as usize * channels]; + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) + { + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; - for (dst_chunk, src_chunks) in transient_row - .chunks_exact_mut(channels) - .zip(src.chunks_exact(channels)) + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + for (dst, src) in transient_row + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + dst[0] = lut_table[src[image_configuration.r_index()] as usize]; + dst[1] = lut_table[src[image_configuration.g_index()] as usize]; + dst[2] = lut_table[src[image_configuration.b_index()] as usize]; + } + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba { - dst_chunk[image_configuration.r_index()] = - *lut_table.get_unchecked(src_chunks[image_configuration.r_index()] as usize); - dst_chunk[image_configuration.g_index()] = - *lut_table.get_unchecked(src_chunks[image_configuration.g_index()] as usize); - dst_chunk[image_configuration.b_index()] = - *lut_table.get_unchecked(src_chunks[image_configuration.b_index()] as usize); - dst_chunk[image_configuration.a_index()] = - src_chunks[image_configuration.a_index()] as f32 * (1. / 255.0); + for (dst, src) in transient_row + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + dst[0] = lut_table[src[image_configuration.r_index()] as usize]; + dst[1] = lut_table[src[image_configuration.g_index()] as usize]; + dst[2] = lut_table[src[image_configuration.b_index()] as usize]; + dst[3] = a_lut_table[src[image_configuration.a_index()] as usize]; + } } + let mut cx = 0usize; + if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher( - _cx, - transient_row.as_ptr(), - 0, - width, - dst.as_mut_ptr() as *mut f32, - 0, - matrix, - ); + cx = unsafe { dispatcher(cx, &transient_row, dst, width, matrix) }; } - let dst_ptr = dst.as_mut_ptr() as *mut f32; - - for x in _cx..width as usize { - let px = x * channels; - let src = transient_row.get_unchecked(px..); - - let r = *src.get_unchecked(image_configuration.r_index()); - let g = *src.get_unchecked(image_configuration.g_index()); - let b = *src.get_unchecked(image_configuration.b_index()); - - let rgb = Rgb::::new(r, g, b); - let px = x * channels; - let dst_store = dst_ptr.add(px); - - let xyz = Xyz::from_linear_rgb(rgb, matrix); - - match target { - XyzTarget::Lab => { - let lab = Lab::from_xyz(xyz); - dst_store.write_unaligned(lab.l); - dst_store.add(1).write_unaligned(lab.a); - dst_store.add(2).write_unaligned(lab.b); - } - XyzTarget::Xyz => { - dst_store.write_unaligned(xyz.x); - dst_store.add(1).write_unaligned(xyz.y); - dst_store.add(2).write_unaligned(xyz.z); - } - XyzTarget::Luv => { - let luv = Luv::from_xyz(xyz); - dst_store.write_unaligned(luv.l); - dst_store.add(1).write_unaligned(luv.u); - dst_store.add(2).write_unaligned(luv.v); + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + let src = &transient_row[cx * 3..]; + let dst = &mut dst[cx * 3..]; + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + let rgb = Rgb::::new(src[0], src[1], src[2]); + let xyz = Xyz::from_linear_rgb(rgb, matrix); + match target { + XyzTarget::Lab => { + let lab = Lab::from_xyz(xyz); + dst[0] = lab.l; + dst[1] = lab.a; + dst[2] = lab.b; + } + XyzTarget::Xyz => { + dst[0] = xyz.x; + dst[1] = xyz.y; + dst[2] = xyz.z; + } + XyzTarget::Luv => { + let luv = Luv::from_xyz(xyz); + dst[0] = luv.l; + dst[1] = luv.u; + dst[2] = luv.v; + } + XyzTarget::Lch => { + let luv = Luv::from_xyz(xyz); + let lch = LCh::from_luv(luv); + dst[0] = lch.l; + dst[1] = lch.c; + dst[2] = lch.h; + } } - XyzTarget::Lch => { - let luv = Luv::from_xyz(xyz); - let lch = LCh::from_luv(luv); - dst_store.write_unaligned(lch.l); - dst_store.add(1).write_unaligned(lch.c); - dst_store.add(2).write_unaligned(lch.h); + } + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba + { + let src = &transient_row[cx * 4..]; + let dst = &mut dst[cx * 4..]; + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + let rgb = Rgb::::new(src[0], src[1], src[2]); + let xyz = Xyz::from_linear_rgb(rgb, matrix); + match target { + XyzTarget::Lab => { + let lab = Lab::from_xyz(xyz); + dst[0] = lab.l; + dst[1] = lab.a; + dst[2] = lab.b; + } + XyzTarget::Xyz => { + dst[0] = xyz.x; + dst[1] = xyz.y; + dst[2] = xyz.z; + } + XyzTarget::Luv => { + let luv = Luv::from_xyz(xyz); + dst[0] = luv.l; + dst[1] = luv.u; + dst[2] = luv.v; + } + XyzTarget::Lch => { + let luv = Luv::from_xyz(xyz); + let lch = LCh::from_luv(luv); + dst[0] = lch.l; + dst[1] = lch.c; + dst[2] = lch.h; + } } + dst[3] = src[3]; } - let a = *src.get_unchecked(image_configuration.a_index()); - dst_store.add(3).write_unaligned(a); } - }); + } + + Ok(()) } /// This function converts RGBA to CIE L*ab. @@ -176,25 +212,17 @@ fn channels_to_xyz_with_alpha, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_xyz_with_alpha::<{ ImageConfiguration::Rgba as u8 }, { XyzTarget::Lab as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } /// This function converts BGRA to CIE L*ab. @@ -213,25 +241,17 @@ pub fn rgba_to_lab_with_alpha( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` /// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn bgra_to_lab_with_alpha( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_xyz_with_alpha::<{ ImageConfiguration::Bgra as u8 }, { XyzTarget::Lab as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } /// This function converts RGBA to CIE L*uv. @@ -250,25 +270,17 @@ pub fn bgra_to_lab_with_alpha( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` /// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn rgba_to_luv_with_alpha( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_xyz_with_alpha::<{ ImageConfiguration::Rgba as u8 }, { XyzTarget::Luv as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } /// This function converts BGRA to CIE L*uv. @@ -287,25 +299,17 @@ pub fn rgba_to_luv_with_alpha( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` /// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn bgra_to_luv_with_alpha( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_xyz_with_alpha::<{ ImageConfiguration::Bgra as u8 }, { XyzTarget::Luv as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } /// This function converts RGBA to CIE XYZ against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -320,25 +324,17 @@ pub fn bgra_to_luv_with_alpha( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` /// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn rgba_to_xyz_with_alpha( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_xyz_with_alpha::<{ ImageConfiguration::Rgba as u8 }, { XyzTarget::Xyz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } /// This function converts BGRA to CIE XYZ against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -353,25 +349,17 @@ pub fn rgba_to_xyz_with_alpha( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` /// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn bgra_to_xyz_with_alpha( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_xyz_with_alpha::<{ ImageConfiguration::Bgra as u8 }, { XyzTarget::Xyz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } /// This function converts RGBA to CIE LCH against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -386,25 +374,17 @@ pub fn bgra_to_xyz_with_alpha( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` /// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn rgba_to_lch_with_alpha( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_xyz_with_alpha::<{ ImageConfiguration::Rgba as u8 }, { XyzTarget::Lch as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } /// This function converts BGRA to CIE LCH against D65 white point and preserving and normalizing alpha channels keeping it at last positions. This is much more effective than naive direct transformation @@ -419,23 +399,377 @@ pub fn rgba_to_lch_with_alpha( /// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` /// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn bgra_to_lch_with_alpha( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { channels_to_xyz_with_alpha::<{ ImageConfiguration::Bgra as u8 }, { XyzTarget::Lch as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) +} + +#[cfg(test)] +mod tests_with_alpha { + use super::*; + use crate::{BufferStore, SRGB_TO_XYZ_D65}; + + // ── helpers ─────────────────────────────────────────────────────────────── + + fn make_src(data: Vec, width: u32, height: u32, channels: u32) -> ImageBuffer<'static, u8> { + ImageBuffer::from_vec(data, width, height, width * channels, channels).unwrap() + } + + fn make_dst(width: u32, height: u32, channels: u32) -> ImageBufferMut<'static, f32> { + ImageBufferMut::new( + BufferStore::Owned(vec![0f32; (width * height * channels) as usize]), + width, + height, + width * channels, + channels, + ) + .unwrap() + } + + fn assert_approx(a: f32, b: f32, eps: f32, label: &str) { + assert!((a - b).abs() < eps, "{label}: expected {b:.6}, got {a:.6}"); + } + + // dst layout: [X/L, Y/a/u/C, Z/b/v/h, A] per pixel + fn read_pixel(dst: &ImageBufferMut<'_, f32>, px: usize) -> [f32; 4] { + let d = dst.data.borrow(); + [d[px * 4], d[px * 4 + 1], d[px * 4 + 2], d[px * 4 + 3]] + } + + // ── rgba_to_xyz_with_alpha ──────────────────────────────────────────────── + + #[test] + fn rgba_to_xyz_with_alpha_black_opaque() { + let src = make_src(vec![0, 0, 0, 255], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + rgba_to_xyz_with_alpha(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let p = read_pixel(&dst, 0); + assert_approx(p[0], 0.0, 1e-5, "X"); + assert_approx(p[1], 0.0, 1e-5, "Y"); + assert_approx(p[2], 0.0, 1e-5, "Z"); + assert_approx(p[3], 1.0, 1e-5, "A"); + } + + #[test] + fn rgba_to_xyz_with_alpha_white_opaque() { + let src = make_src(vec![255, 255, 255, 255], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + rgba_to_xyz_with_alpha(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let p = read_pixel(&dst, 0); + assert_approx(p[0], 0.9505, 1e-3, "X"); + assert_approx(p[1], 1.0000, 1e-3, "Y"); + assert_approx(p[2], 1.0890, 1e-3, "Z"); + assert_approx(p[3], 1.0, 1e-5, "A"); + } + + #[test] + fn rgba_to_xyz_with_alpha_transparent() { + let src = make_src(vec![255, 255, 255, 0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + rgba_to_xyz_with_alpha(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let p = read_pixel(&dst, 0); + assert_approx(p[3], 0.0, 1e-5, "A fully transparent"); + } + + #[test] + fn rgba_to_xyz_with_alpha_half_alpha() { + let src = make_src(vec![255, 255, 255, 128], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + rgba_to_xyz_with_alpha(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let p = read_pixel(&dst, 0); + assert_approx(p[3], 128.0 / 255.0, 1e-4, "A half"); + } + + #[test] + fn rgba_to_xyz_with_alpha_xyz_unaffected_by_alpha() { + // Same RGB, different alpha → XYZ channels must be identical + let src_full = make_src(vec![100, 149, 237, 255], 1, 1, 4); + let src_half = make_src(vec![100, 149, 237, 128], 1, 1, 4); + let mut dst_full = make_dst(1, 1, 4); + let mut dst_half = make_dst(1, 1, 4); + rgba_to_xyz_with_alpha( + &src_full, + &mut dst_full, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + rgba_to_xyz_with_alpha( + &src_half, + &mut dst_half, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + let pf = read_pixel(&dst_full, 0); + let ph = read_pixel(&dst_half, 0); + for ch in 0..3 { + assert_approx( + pf[ch], + ph[ch], + 1e-5, + &format!("XYZ ch{ch} alpha-independent"), + ); + } + } + + // ── bgra_to_xyz_with_alpha ──────────────────────────────────────────────── + + #[test] + fn bgra_to_xyz_with_alpha_white() { + let src = make_src(vec![255, 255, 255, 255], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + bgra_to_xyz_with_alpha(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let p = read_pixel(&dst, 0); + assert_approx(p[0], 0.9505, 1e-3, "X"); + assert_approx(p[1], 1.0000, 1e-3, "Y"); + assert_approx(p[2], 1.0890, 1e-3, "Z"); + assert_approx(p[3], 1.0, 1e-5, "A"); + } + + #[test] + fn bgra_to_xyz_with_alpha_grey_matches_rgba() { + // Grey is BGR-invariant + let data = vec![128u8, 128, 128, 200]; + let src_rgba = make_src(data.clone(), 1, 1, 4); + let src_bgra = make_src(data, 1, 1, 4); + let mut dst_rgba = make_dst(1, 1, 4); + let mut dst_bgra = make_dst(1, 1, 4); + rgba_to_xyz_with_alpha( + &src_rgba, + &mut dst_rgba, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + bgra_to_xyz_with_alpha( + &src_bgra, + &mut dst_bgra, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + for ch in 0..4 { + assert_approx( + read_pixel(&dst_rgba, 0)[ch], + read_pixel(&dst_bgra, 0)[ch], + 1e-5, + &format!("ch{ch}"), + ); + } + } + + // ── rgba_to_lab_with_alpha ──────────────────────────────────────────────── + + #[test] + fn rgba_to_lab_with_alpha_white() { + let src = make_src(vec![255, 255, 255, 255], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + rgba_to_lab_with_alpha(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let p = read_pixel(&dst, 0); + assert_approx(p[0], 100.0, 0.5, "L*"); + assert_approx(p[1], 0.0, 0.5, "a*"); + assert_approx(p[2], 0.0, 0.5, "b*"); + assert_approx(p[3], 1.0, 1e-5, "A"); + } + + #[test] + fn rgba_to_lab_with_alpha_black() { + let src = make_src(vec![0, 0, 0, 255], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + rgba_to_lab_with_alpha(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let p = read_pixel(&dst, 0); + assert_approx(p[0], 0.0, 0.1, "L* black"); + assert_approx(p[3], 1.0, 1e-5, "A"); + } + + #[test] + fn rgba_to_lab_with_alpha_alpha_passthrough() { + for alpha in [0u8, 64, 128, 192, 255] { + let src = make_src(vec![200, 100, 50, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + rgba_to_lab_with_alpha(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb) + .unwrap(); + assert_approx( + read_pixel(&dst, 0)[3], + alpha as f32 / 255.0, + 1e-4, + &format!("alpha={alpha}"), + ); + } + } + + // ── bgra_to_lab_with_alpha ──────────────────────────────────────────────── + + #[test] + fn bgra_to_lab_with_alpha_white() { + let src = make_src(vec![255, 255, 255, 255], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + bgra_to_lab_with_alpha(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let p = read_pixel(&dst, 0); + assert_approx(p[0], 100.0, 0.5, "L*"); + assert_approx(p[3], 1.0, 1e-5, "A"); + } + + #[test] + fn bgra_to_lab_with_alpha_grey_matches_rgba() { + let data = vec![150u8, 150, 150, 180]; + let src_rgba = make_src(data.clone(), 1, 1, 4); + let src_bgra = make_src(data, 1, 1, 4); + let mut dst_rgba = make_dst(1, 1, 4); + let mut dst_bgra = make_dst(1, 1, 4); + rgba_to_lab_with_alpha( + &src_rgba, + &mut dst_rgba, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + bgra_to_lab_with_alpha( + &src_bgra, + &mut dst_bgra, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + for ch in 0..4 { + assert_approx( + read_pixel(&dst_rgba, 0)[ch], + read_pixel(&dst_bgra, 0)[ch], + 1e-4, + &format!("ch{ch}"), + ); + } + } + + #[test] + fn rgba_to_luv_with_alpha_alpha_passthrough() { + for alpha in [0u8, 128, 255] { + let src = make_src(vec![100, 100, 100, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + rgba_to_luv_with_alpha(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb) + .unwrap(); + assert_approx( + read_pixel(&dst, 0)[3], + alpha as f32 / 255.0, + 1e-4, + &format!("alpha={alpha}"), + ); + } + } + + #[test] + fn bgra_to_luv_with_alpha_grey_matches_rgba() { + let data = vec![180u8, 180, 180, 100]; + let src_rgba = make_src(data.clone(), 1, 1, 4); + let src_bgra = make_src(data, 1, 1, 4); + let mut dst_rgba = make_dst(1, 1, 4); + let mut dst_bgra = make_dst(1, 1, 4); + rgba_to_luv_with_alpha( + &src_rgba, + &mut dst_rgba, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + bgra_to_luv_with_alpha( + &src_bgra, + &mut dst_bgra, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + for ch in 0..4 { + assert_approx( + read_pixel(&dst_rgba, 0)[ch], + read_pixel(&dst_bgra, 0)[ch], + 1e-4, + &format!("ch{ch}"), + ); + } + } + + #[test] + fn rgba_to_lch_with_alpha_alpha_passthrough() { + for alpha in [0u8, 128, 255] { + let src = make_src(vec![200, 200, 200, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + rgba_to_lch_with_alpha(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb) + .unwrap(); + assert_approx( + read_pixel(&dst, 0)[3], + alpha as f32 / 255.0, + 1e-4, + &format!("alpha={alpha}"), + ); + } + } + + #[test] + fn bgra_to_lch_with_alpha_grey_matches_rgba() { + let data = vec![90u8, 90, 90, 220]; + let src_rgba = make_src(data.clone(), 1, 1, 4); + let src_bgra = make_src(data, 1, 1, 4); + let mut dst_rgba = make_dst(1, 1, 4); + let mut dst_bgra = make_dst(1, 1, 4); + rgba_to_lch_with_alpha( + &src_rgba, + &mut dst_rgba, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + bgra_to_lch_with_alpha( + &src_bgra, + &mut dst_bgra, + &SRGB_TO_XYZ_D65, + TransferFunction::Srgb, + ) + .unwrap(); + for ch in 0..4 { + assert_approx( + read_pixel(&dst_rgba, 0)[ch], + read_pixel(&dst_bgra, 0)[ch], + 1e-4, + &format!("ch{ch}"), + ); + } + } + + // ── multi-pixel consistency ─────────────────────────────────────────────── + + #[test] + fn rgba_to_lab_with_alpha_multi_pixel_consistent() { + let pixel = [100u8, 149, 237, 200]; + let src = make_src(pixel.repeat(4), 4, 1, 4); + let mut dst = make_dst(4, 1, 4); + rgba_to_lab_with_alpha(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let p0 = read_pixel(&dst, 0); + for px in 1..4 { + let p = read_pixel(&dst, px); + for ch in 0..4 { + assert_approx(p[ch], p0[ch], 1e-4, &format!("px{px} ch{ch}")); + } + } + } + + #[test] + fn rgba_to_xyz_with_alpha_multi_row_consistent() { + let src = make_src(vec![128u8, 64, 32, 255].repeat(2), 1, 2, 4); + let mut dst = make_dst(1, 2, 4); + rgba_to_xyz_with_alpha(&src, &mut dst, &SRGB_TO_XYZ_D65, TransferFunction::Srgb).unwrap(); + let p0 = read_pixel(&dst, 0); + let p1 = read_pixel(&dst, 1); + for ch in 0..4 { + assert_approx(p0[ch], p1[ch], 1e-5, &format!("row mismatch ch{ch}")); + } + } } diff --git a/src/jzazbz.rs b/src/jzazbz.rs index 6a34cdf..472261a 100644 --- a/src/jzazbz.rs +++ b/src/jzazbz.rs @@ -6,8 +6,8 @@ */ use crate::utils::mlaf; use crate::{ - EuclideanDistance, Jzczhz, Rgb, TaxicabDistance, TransferFunction, Xyz, SRGB_TO_XYZ_D65, - XYZ_TO_SRGB_D65, + EuclideanDistance, Jzczhz, Rgb, SRGB_TO_XYZ_D65, TaxicabDistance, TransferFunction, + XYZ_TO_SRGB_D65, Xyz, }; use num_traits::Pow; use std::ops::{ @@ -125,6 +125,12 @@ impl Jzazbz { Self::from_xyz_with_display_luminance(xyz, 200.) } + #[inline] + pub fn from_linear_rgb_with_luminance(p0: Rgb, display_luminance: f32) -> Jzazbz { + let q = Xyz::from_linear_rgb(p0, &SRGB_TO_XYZ_D65); + Self::from_xyz_with_display_luminance(q, display_luminance) + } + /// Converts Rgb to Jzazbz /// /// # Arguments diff --git a/src/jzazbz_to_image.rs b/src/jzazbz_to_image.rs index 0dbf318..2867523 100644 --- a/src/jzazbz_to_image.rs +++ b/src/jzazbz_to_image.rs @@ -6,147 +6,158 @@ */ use crate::image::ImageConfiguration; use crate::image_to_jzazbz::JzazbzTarget; -#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -use crate::neon::neon_jzazbz_to_image; -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -use crate::sse::sse_jzazbz_to_image; -use crate::{Jzazbz, Jzczhz, TransferFunction}; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -use std::slice; - -#[allow(clippy::type_complexity)] +use crate::{ColorError, ImageBuffer, ImageBufferMut, Jzazbz, Jzczhz, TransferFunction}; + fn jzazbz_to_image( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, display_luminance: f32, transfer_function: TransferFunction, -) { - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let target: JzazbzTarget = TARGET.into(); +) -> Result<(), ColorError> { + dst.validate()?; + src.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } - let mut _wide_row_handle: Option< - unsafe fn(usize, *const f32, u32, *mut f32, u32, u32, f32) -> usize, - > = None; + let mut _wide_row_handle: Option usize> = + None; #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] if std::arch::is_x86_feature_detected!("sse4.1") { + use crate::sse::sse_jzazbz_to_image; _wide_row_handle = Some(sse_jzazbz_to_image::); } #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] { + use crate::neon::neon_jzazbz_to_image; _wide_row_handle = Some(neon_jzazbz_to_image::); } - let mut lut_table = [0u8; 2049]; - for (i, lut) in lut_table.iter_mut().enumerate() { + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + let target: JzazbzTarget = TARGET.into(); + let channels = image_configuration.channel_count(); + + let mut lut_table = [0u8; 65536]; + for (i, lut) in lut_table[..2049].iter_mut().enumerate() { *lut = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.) .round() .min(255.) as u8; } - let src_slice_safe_align = unsafe { - slice::from_raw_parts( - src.as_ptr() as *const u8, - src_stride as usize * height as usize, - ) - }; + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; + let width = src.width as usize; - let iter; + let mut transient_row = vec![0f32; width * channels]; - #[cfg(feature = "rayon")] + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) { - iter = dst - .par_chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.par_chunks_exact(src_stride as usize)); - } - #[cfg(not(feature = "rayon"))] - { - iter = dst - .chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.chunks_exact(src_stride as usize)); - } - - iter.for_each(|(dst, src)| unsafe { - let channels = image_configuration.channel_count(); - - let mut _cx = 0usize; - - let src_ptr = src.as_ptr() as *mut f32; - - let mut transient_row = vec![0f32; width as usize * channels]; + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; + let mut cx = 0usize; if let Some(dispatcher) = _wide_row_handle { - _cx = dispatcher( - _cx, - src.as_ptr() as *const f32, - 0, - transient_row.as_mut_ptr(), - 0, - width, - display_luminance, - ); + cx = unsafe { dispatcher(0, src, &mut transient_row, width as u32, display_luminance) } } - for x in _cx..width as usize { - let px = x * channels; - let l_x = src_ptr.add(px).read_unaligned(); - let l_y = src_ptr.add(px + 1).read_unaligned(); - let l_z = src_ptr.add(px + 2).read_unaligned(); - let rgb = match target { - JzazbzTarget::Jzazbz => { - let jzazbz = Jzazbz::new_with_luminance(l_x, l_y, l_z, display_luminance); - jzazbz.to_linear_rgb() - } - JzazbzTarget::Jzczhz => { - let jzczhz = Jzczhz::new(l_x, l_y, l_z); - jzczhz.to_linear_rgb_with_luminance(display_luminance) - } - }; - - let dst = transient_row.get_unchecked_mut((x * channels)..); - *dst.get_unchecked_mut(image_configuration.r_index()) = rgb.r; - *dst.get_unchecked_mut(image_configuration.g_index()) = rgb.g; - *dst.get_unchecked_mut(image_configuration.b_index()) = rgb.b; - if image_configuration.has_alpha() { - let l_a = src_ptr.add(px + 3).read_unaligned(); - *dst.get_unchecked_mut(image_configuration.a_index()) = l_a; + if image_configuration.has_alpha() { + let transient_row = &mut transient_row[cx * 4..]; + let src = &src[cx * 4..]; + for (transient, src) in transient_row + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + let rgb = match target { + JzazbzTarget::Jzazbz => { + let jzazbz = + Jzazbz::new_with_luminance(src[0], src[1], src[2], display_luminance); + jzazbz.to_linear_rgb() + } + JzazbzTarget::Jzczhz => { + let jzczhz = Jzczhz::new(src[0], src[1], src[2]); + jzczhz.to_linear_rgb_with_luminance(display_luminance) + } + }; + transient[image_configuration.r_index()] = rgb.r; + transient[image_configuration.g_index()] = rgb.g; + transient[image_configuration.b_index()] = rgb.b; + transient[image_configuration.a_index()] = src[3]; + } + } else { + let transient_row = &mut transient_row[cx * 3..]; + let src = &src[cx * 3..]; + for (transient, src) in transient_row + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + let rgb = match target { + JzazbzTarget::Jzazbz => { + let jzazbz = + Jzazbz::new_with_luminance(src[0], src[1], src[2], display_luminance); + jzazbz.to_linear_rgb() + } + JzazbzTarget::Jzczhz => { + let jzczhz = Jzczhz::new(src[0], src[1], src[2]); + jzczhz.to_linear_rgb_with_luminance(display_luminance) + } + }; + transient[image_configuration.r_index()] = rgb.r; + transient[image_configuration.g_index()] = rgb.g; + transient[image_configuration.b_index()] = rgb.b; } } - for (dst_chunk, src_chunks) in dst - .chunks_exact_mut(channels) - .zip(transient_row.chunks_exact(channels)) + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(transient_row.as_chunks::<3>().0.iter()) + { + let r = (src[image_configuration.r_index()] * 2048.).round() as u16; + let g = (src[image_configuration.g_index()] * 2048.).round() as u16; + let b = (src[image_configuration.b_index()] * 2048.).round() as u16; + dst[image_configuration.r_index()] = lut_table[r.min(2048) as usize]; + dst[image_configuration.g_index()] = lut_table[g.min(2048) as usize]; + dst[image_configuration.b_index()] = lut_table[b.min(2048) as usize]; + } + } else if image_configuration == ImageConfiguration::Rgba + || image_configuration == ImageConfiguration::Bgra { - let r_cast = - (src_chunks[image_configuration.r_index()].min(1.).max(0.) * 2048f32).round(); - let g_cast = - (src_chunks[image_configuration.g_index()].min(1.).max(0.) * 2048f32).round(); - let b_cast = - (src_chunks[image_configuration.b_index()].min(1.).max(0.) * 2048f32).round(); - - dst_chunk[image_configuration.r_index()] = - *lut_table.get_unchecked((r_cast as usize).min(2048)); - dst_chunk[image_configuration.g_index()] = - *lut_table.get_unchecked((g_cast as usize).min(2048)); - dst_chunk[image_configuration.b_index()] = - *lut_table.get_unchecked((b_cast as usize).min(2048)); - - if image_configuration.has_alpha() { - let a_cast = (src_chunks[image_configuration.a_index()] * 255.) - .min(255.) - .max(0.) as u8; - dst_chunk[image_configuration.a_index()] = a_cast; + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(transient_row.as_chunks::<4>().0.iter()) + { + let r = (src[image_configuration.r_index()] * 2048.).round() as u16; + let g = (src[image_configuration.g_index()] * 2048.).round() as u16; + let b = (src[image_configuration.b_index()] * 2048.).round() as u16; + dst[image_configuration.r_index()] = lut_table[r.min(2048) as usize]; + dst[image_configuration.g_index()] = lut_table[g.min(2048) as usize]; + dst[image_configuration.b_index()] = lut_table[b.min(2048) as usize]; + dst[image_configuration.a_index()] = (src[image_configuration.a_index()] * 255.) + .clamp(0., 255.) + .round() as u8; } } - }); + } + + Ok(()) } /// This function converts Jzazbz with interleaved alpha channel to RGBA. This is much more effective than naive direct transformation @@ -160,25 +171,17 @@ fn jzazbz_to_image( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn jzazbz_to_rgba( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { jzazbz_to_image::<{ ImageConfiguration::Rgba as u8 }, { JzazbzTarget::Jzazbz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts Jzazbz to RGB. This is much more effective than naive direct transformation @@ -192,25 +195,17 @@ pub fn jzazbz_to_rgba( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn jzazbz_to_rgb( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { jzazbz_to_image::<{ ImageConfiguration::Rgb as u8 }, { JzazbzTarget::Jzazbz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts Jzazbz to BGR. This is much more effective than naive direct transformation @@ -224,25 +219,17 @@ pub fn jzazbz_to_rgb( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn jzazbz_to_bgr( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { jzazbz_to_image::<{ ImageConfiguration::Bgr as u8 }, { JzazbzTarget::Jzazbz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts Jzazbz with interleaved alpha channel to BGRA. This is much more effective than naive direct transformation @@ -256,25 +243,17 @@ pub fn jzazbz_to_bgr( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn jzazbz_to_bgra( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { jzazbz_to_image::<{ ImageConfiguration::Bgra as u8 }, { JzazbzTarget::Jzazbz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts Jzczhz with interleaved alpha channel to RGBA. This is much more effective than naive direct transformation @@ -288,25 +267,17 @@ pub fn jzazbz_to_bgra( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn jzczhz_to_rgba( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { jzazbz_to_image::<{ ImageConfiguration::Rgba as u8 }, { JzazbzTarget::Jzczhz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts Jzczhz to RGB. This is much more effective than naive direct transformation @@ -320,25 +291,17 @@ pub fn jzczhz_to_rgba( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn jzczhz_to_rgb( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { jzazbz_to_image::<{ ImageConfiguration::Rgb as u8 }, { JzazbzTarget::Jzczhz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts Jzczhz to BGR. This is much more effective than naive direct transformation @@ -352,25 +315,17 @@ pub fn jzczhz_to_rgb( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn jzczhz_to_bgr( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { jzazbz_to_image::<{ ImageConfiguration::Bgr as u8 }, { JzazbzTarget::Jzczhz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) } /// This function converts Jzczhz with interleaved alpha channel to BGRA. This is much more effective than naive direct transformation @@ -384,23 +339,490 @@ pub fn jzczhz_to_bgr( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn jzczhz_to_bgra( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, display_luminance: f32, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { jzazbz_to_image::<{ ImageConfiguration::Bgra as u8 }, { JzazbzTarget::Jzczhz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, display_luminance, transfer_function, - ); + ) +} + +#[cfg(test)] +mod tests_jzazbz_to_image { + use super::*; + use crate::{BufferStore, Rgb}; + + const LUMINANCE: f32 = 203.0; + + fn make_src( + data: Vec, + width: u32, + height: u32, + channels: u32, + ) -> ImageBuffer<'static, f32> { + ImageBuffer::from_vec(data, width, height, width * channels, channels).unwrap() + } + + fn make_dst(width: u32, height: u32, channels: u32) -> ImageBufferMut<'static, u8> { + ImageBufferMut::new( + BufferStore::Owned(vec![0u8; (width * height * channels) as usize]), + width, + height, + width * channels, + channels, + ) + .unwrap() + } + + fn assert_approx(a: u8, b: u8, eps: u8, label: &str) { + assert!( + (a as i16 - b as i16).abs() <= eps as i16, + "{label}: got {a}, expected {b}" + ); + } + + // Convert linear RGB → Jzazbz for use as test input + fn rgb_to_jzazbz(r: f32, g: f32, b: f32) -> (f32, f32, f32) { + let rgb = Rgb::::new(r, g, b); + let jzazbz = Jzazbz::from_linear_rgb_with_luminance(rgb, LUMINANCE); + (jzazbz.jz, jzazbz.az, jzazbz.bz) + } + + fn rgb_to_jzczhz(r: f32, g: f32, b: f32) -> (f32, f32, f32) { + let rgb = Rgb::::new(r, g, b); + let jzazbz = Jzazbz::from_linear_rgb_with_luminance(rgb, LUMINANCE); + let jzczhz = Jzczhz::from_jzazbz(jzazbz); + (jzczhz.jz, jzczhz.cz, jzczhz.hz) + } + + // ── Jzazbz black / white roundtrip ─────────────────────────────────────── + + #[test] + fn jzazbz_rgb_black_roundtrip() { + let (jz, az, bz) = rgb_to_jzazbz(0.0, 0.0, 0.0); + let src = make_src(vec![jz, az, bz], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + jzazbz_to_rgb(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 0, 2, "R black"); + assert_approx(d[1], 0, 2, "G black"); + assert_approx(d[2], 0, 2, "B black"); + } + + #[test] + fn jzazbz_rgb_white_roundtrip() { + let (jz, az, bz) = rgb_to_jzazbz(1.0, 1.0, 1.0); + let src = make_src(vec![jz, az, bz], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + jzazbz_to_rgb(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R white"); + assert_approx(d[1], 255, 2, "G white"); + assert_approx(d[2], 255, 2, "B white"); + } + + #[test] + fn jzazbz_bgr_black_roundtrip() { + let (jz, az, bz) = rgb_to_jzazbz(0.0, 0.0, 0.0); + let src = make_src(vec![jz, az, bz], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + jzazbz_to_bgr(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 0, 2, "B black"); + assert_approx(d[1], 0, 2, "G black"); + assert_approx(d[2], 0, 2, "R black"); + } + + #[test] + fn jzazbz_bgr_white_roundtrip() { + let (jz, az, bz) = rgb_to_jzazbz(1.0, 1.0, 1.0); + let src = make_src(vec![jz, az, bz], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + jzazbz_to_bgr(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "B white"); + assert_approx(d[1], 255, 2, "G white"); + assert_approx(d[2], 255, 2, "R white"); + } + + // ── Jzazbz RGBA / BGRA roundtrip ───────────────────────────────────────── + + #[test] + fn jzazbz_rgba_white_opaque_roundtrip() { + let (jz, az, bz) = rgb_to_jzazbz(1.0, 1.0, 1.0); + let src = make_src(vec![jz, az, bz, 1.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + jzazbz_to_rgba(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R"); + assert_approx(d[1], 255, 2, "G"); + assert_approx(d[2], 255, 2, "B"); + assert_approx(d[3], 255, 1, "A"); + } + + #[test] + fn jzazbz_rgba_black_transparent_roundtrip() { + let (jz, az, bz) = rgb_to_jzazbz(0.0, 0.0, 0.0); + let src = make_src(vec![jz, az, bz, 0.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + jzazbz_to_rgba(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[3], 0, 1, "A transparent"); + } + + #[test] + fn jzazbz_bgra_white_opaque_roundtrip() { + let (jz, az, bz) = rgb_to_jzazbz(1.0, 1.0, 1.0); + let src = make_src(vec![jz, az, bz, 1.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + jzazbz_to_bgra(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "B"); + assert_approx(d[1], 255, 2, "G"); + assert_approx(d[2], 255, 2, "R"); + assert_approx(d[3], 255, 1, "A"); + } + + // ── Jzczhz black / white roundtrip ─────────────────────────────────────── + + #[test] + fn jzczhz_rgb_black_roundtrip() { + let (jz, cz, hz) = rgb_to_jzczhz(0.0, 0.0, 0.0); + let src = make_src(vec![jz, cz, hz], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + jzczhz_to_rgb(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 0, 2, "R black"); + assert_approx(d[1], 0, 2, "G black"); + assert_approx(d[2], 0, 2, "B black"); + } + + #[test] + fn jzczhz_rgb_white_roundtrip() { + let (jz, cz, hz) = rgb_to_jzczhz(1.0, 1.0, 1.0); + let src = make_src(vec![jz, cz, hz], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + jzczhz_to_rgb(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R white"); + assert_approx(d[1], 255, 2, "G white"); + assert_approx(d[2], 255, 2, "B white"); + } + + #[test] + fn jzczhz_bgr_white_roundtrip() { + let (jz, cz, hz) = rgb_to_jzczhz(1.0, 1.0, 1.0); + let src = make_src(vec![jz, cz, hz], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + jzczhz_to_bgr(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "B white"); + assert_approx(d[1], 255, 2, "G white"); + assert_approx(d[2], 255, 2, "R white"); + } + + #[test] + fn jzczhz_rgba_white_opaque_roundtrip() { + let (jz, cz, hz) = rgb_to_jzczhz(1.0, 1.0, 1.0); + let src = make_src(vec![jz, cz, hz, 1.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + jzczhz_to_rgba(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R"); + assert_approx(d[1], 255, 2, "G"); + assert_approx(d[2], 255, 2, "B"); + assert_approx(d[3], 255, 1, "A"); + } + + #[test] + fn jzczhz_bgra_white_opaque_roundtrip() { + let (jz, cz, hz) = rgb_to_jzczhz(1.0, 1.0, 1.0); + let src = make_src(vec![jz, cz, hz, 1.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + jzczhz_to_bgra(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "B"); + assert_approx(d[1], 255, 2, "G"); + assert_approx(d[2], 255, 2, "R"); + assert_approx(d[3], 255, 1, "A"); + } + + // ── alpha denormalization ───────────────────────────────────────────────── + + #[test] + fn jzazbz_rgba_alpha_denormalized_correctly() { + let (jz, az, bz) = rgb_to_jzazbz(0.5, 0.5, 0.5); + for alpha in [0.0f32, 0.25, 0.5, 0.75, 1.0] { + let src = make_src(vec![jz, az, bz, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + jzazbz_to_rgba(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let expected = (alpha * 255.).round() as u8; + assert_approx(d[3], expected, 1, &format!("alpha={alpha}")); + } + } + + #[test] + fn jzazbz_bgra_alpha_denormalized_correctly() { + let (jz, az, bz) = rgb_to_jzazbz(0.5, 0.5, 0.5); + for alpha in [0.0f32, 0.25, 0.5, 0.75, 1.0] { + let src = make_src(vec![jz, az, bz, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + jzazbz_to_bgra(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let expected = (alpha * 255.).round() as u8; + assert_approx(d[3], expected, 1, &format!("alpha={alpha}")); + } + } + + #[test] + fn jzczhz_rgba_alpha_denormalized_correctly() { + let (jz, cz, hz) = rgb_to_jzczhz(0.5, 0.5, 0.5); + for alpha in [0.0f32, 0.25, 0.5, 0.75, 1.0] { + let src = make_src(vec![jz, cz, hz, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + jzczhz_to_rgba(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let expected = (alpha * 255.).round() as u8; + assert_approx(d[3], expected, 1, &format!("alpha={alpha}")); + } + } + + #[test] + fn jzczhz_bgra_alpha_denormalized_correctly() { + let (jz, cz, hz) = rgb_to_jzczhz(0.5, 0.5, 0.5); + for alpha in [0.0f32, 0.25, 0.5, 0.75, 1.0] { + let src = make_src(vec![jz, cz, hz, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + jzczhz_to_bgra(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let expected = (alpha * 255.).round() as u8; + assert_approx(d[3], expected, 1, &format!("alpha={alpha}")); + } + } + + #[test] + fn rgba_rgb_channels_encoded_when_alpha_zero() { + let (jz, az, bz) = rgb_to_jzazbz(1.0, 1.0, 1.0); + let src = make_src(vec![jz, az, bz, 0.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + jzazbz_to_rgba(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R encoded despite alpha=0"); + assert_eq!(d[3], 0, "A must remain 0"); + } + + // ── rgb and bgr agree on grey ───────────────────────────────────────────── + + #[test] + fn jzazbz_rgb_bgr_agree_on_grey() { + let (jz, az, bz) = rgb_to_jzazbz(0.5, 0.5, 0.5); + let src_rgb = make_src(vec![jz, az, bz], 1, 1, 3); + let src_bgr = make_src(vec![jz, az, bz], 1, 1, 3); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + jzazbz_to_rgb(&src_rgb, &mut dst_rgb, LUMINANCE, TransferFunction::Srgb).unwrap(); + jzazbz_to_bgr(&src_bgr, &mut dst_bgr, LUMINANCE, TransferFunction::Srgb).unwrap(); + assert_eq!( + &*dst_rgb.data.borrow(), + &*dst_bgr.data.borrow(), + "grey invariant to channel order" + ); + } + + #[test] + fn jzazbz_rgba_bgra_agree_on_grey() { + let (jz, az, bz) = rgb_to_jzazbz(0.5, 0.5, 0.5); + let src_rgba = make_src(vec![jz, az, bz, 0.7], 1, 1, 4); + let src_bgra = make_src(vec![jz, az, bz, 0.7], 1, 1, 4); + let mut dst_rgba = make_dst(1, 1, 4); + let mut dst_bgra = make_dst(1, 1, 4); + jzazbz_to_rgba(&src_rgba, &mut dst_rgba, LUMINANCE, TransferFunction::Srgb).unwrap(); + jzazbz_to_bgra(&src_bgra, &mut dst_bgra, LUMINANCE, TransferFunction::Srgb).unwrap(); + assert_eq!( + &*dst_rgba.data.borrow(), + &*dst_bgra.data.borrow(), + "grey invariant to channel order" + ); + } + + #[test] + fn jzczhz_rgb_bgr_agree_on_grey() { + let (jz, cz, hz) = rgb_to_jzczhz(0.5, 0.5, 0.5); + let src_rgb = make_src(vec![jz, cz, hz], 1, 1, 3); + let src_bgr = make_src(vec![jz, cz, hz], 1, 1, 3); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + jzczhz_to_rgb(&src_rgb, &mut dst_rgb, LUMINANCE, TransferFunction::Srgb).unwrap(); + jzczhz_to_bgr(&src_bgr, &mut dst_bgr, LUMINANCE, TransferFunction::Srgb).unwrap(); + assert_eq!( + &*dst_rgb.data.borrow(), + &*dst_bgr.data.borrow(), + "grey invariant to channel order" + ); + } + + // ── channel ordering for non-grey pixel ─────────────────────────────────── + + #[test] + fn jzazbz_rgb_channel_order_correct_for_red() { + let (jz, az, bz) = rgb_to_jzazbz(1.0, 0.0, 0.0); + let src = make_src(vec![jz, az, bz], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + jzazbz_to_rgb(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert!(d[0] > 200, "R dominant for red input, got {}", d[0]); + assert!(d[1] < 30, "G low for red input, got {}", d[1]); + assert!(d[2] < 30, "B low for red input, got {}", d[2]); + } + + #[test] + fn jzazbz_bgr_channel_order_correct_for_red() { + let (jz, az, bz) = rgb_to_jzazbz(1.0, 0.0, 0.0); + let src = make_src(vec![jz, az, bz], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + jzazbz_to_bgr(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert!(d[0] < 30, "B low for red input in BGR, got {}", d[0]); + assert!(d[1] < 30, "G low for red input in BGR, got {}", d[1]); + assert!(d[2] > 200, "R dominant for red input in BGR, got {}", d[2]); + } + + #[test] + fn jzczhz_rgb_channel_order_correct_for_red() { + let (jz, cz, hz) = rgb_to_jzczhz(1.0, 0.0, 0.0); + let src = make_src(vec![jz, cz, hz], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + jzczhz_to_rgb(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert!(d[0] > 200, "R dominant for red input, got {}", d[0]); + assert!(d[1] < 30, "G low for red input, got {}", d[1]); + assert!(d[2] < 30, "B low for red input, got {}", d[2]); + } + + // ── display luminance affects output ────────────────────────────────────── + + #[test] + fn different_luminance_produces_different_output() { + let (jz, az, bz) = rgb_to_jzazbz(0.5, 0.3, 0.7); + let src1 = make_src(vec![jz, az, bz], 1, 1, 3); + let src2 = make_src(vec![jz, az, bz], 1, 1, 3); + let mut dst1 = make_dst(1, 1, 3); + let mut dst2 = make_dst(1, 1, 3); + jzazbz_to_rgb(&src1, &mut dst1, 100.0, TransferFunction::Srgb).unwrap(); + jzazbz_to_rgb(&src2, &mut dst2, 400.0, TransferFunction::Srgb).unwrap(); + assert_ne!( + &*dst1.data.borrow(), + &*dst2.data.borrow(), + "different luminance must produce different output" + ); + } + + // ── Jzazbz and Jzczhz differ for chromatic input ───────────────────────── + + #[test] + fn jzazbz_and_jzczhz_differ_for_chromatic_input() { + // They use different coordinate systems so their raw float inputs differ — + // verify the two paths don't accidentally produce identical output for + // chromatic colours (where az/bz ≠ 0, cz/hz representation differs). + let r = 0.8f32; + let g = 0.1f32; + let b = 0.3f32; + let (jz1, az, bz) = rgb_to_jzazbz(r, g, b); + let (jz2, cz, hz) = rgb_to_jzczhz(r, g, b); + // Inputs are genuinely different values + let src_jzazbz = make_src(vec![jz1, az, bz], 1, 1, 3); + let src_jzczhz = make_src(vec![jz2, cz, hz], 1, 1, 3); + let mut dst1 = make_dst(1, 1, 3); + let mut dst2 = make_dst(1, 1, 3); + jzazbz_to_rgb(&src_jzazbz, &mut dst1, LUMINANCE, TransferFunction::Srgb).unwrap(); + jzczhz_to_rgb(&src_jzczhz, &mut dst2, LUMINANCE, TransferFunction::Srgb).unwrap(); + // Both should reconstruct roughly the same sRGB — the paths are different + // but the colour they encode is the same + let d1 = dst1.data.borrow(); + let d2 = dst2.data.borrow(); + for ch in 0..3 { + assert_approx( + d1[ch], + d2[ch], + 4, + &format!("ch{ch} jzazbz vs jzczhz roundtrip"), + ); + } + } + + // ── multi-pixel / multi-row consistency ─────────────────────────────────── + + #[test] + fn jzazbz_rgb_multi_pixel_consistent() { + let (jz, az, bz) = rgb_to_jzazbz(0.4, 0.6, 0.2); + let src = make_src(vec![jz, az, bz].repeat(4), 4, 1, 3); + let mut dst = make_dst(4, 1, 3); + jzazbz_to_rgb(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2]]; + for (i, chunk) in d.chunks(3).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn jzazbz_rgba_multi_pixel_consistent() { + let (jz, az, bz) = rgb_to_jzazbz(0.3, 0.5, 0.7); + let src = make_src(vec![jz, az, bz, 0.6].repeat(4), 4, 1, 4); + let mut dst = make_dst(4, 1, 4); + jzazbz_to_rgba(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2], d[3]]; + for (i, chunk) in d.chunks(4).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn jzazbz_rgb_multi_row_consistent() { + let (jz, az, bz) = rgb_to_jzazbz(0.6, 0.4, 0.8); + let src = make_src(vec![jz, az, bz].repeat(4), 2, 2, 3); + let mut dst = make_dst(2, 2, 3); + jzazbz_to_rgb(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2]]; + for (i, chunk) in d.chunks(3).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn jzczhz_rgb_multi_pixel_consistent() { + let (jz, cz, hz) = rgb_to_jzczhz(0.4, 0.6, 0.2); + let src = make_src(vec![jz, cz, hz].repeat(4), 4, 1, 3); + let mut dst = make_dst(4, 1, 3); + jzczhz_to_rgb(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2]]; + for (i, chunk) in d.chunks(3).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + // ── error paths ─────────────────────────────────────────────────────────── + + #[test] + fn rgb_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0f32; 2], 1, 1, 2, 2).unwrap(); + let mut dst = make_dst(1, 1, 3); + assert!(jzazbz_to_rgb(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).is_err()); + } + + #[test] + fn rgba_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0f32; 2], 1, 1, 2, 2).unwrap(); + let mut dst = make_dst(1, 1, 4); + assert!(jzazbz_to_rgba(&src, &mut dst, LUMINANCE, TransferFunction::Srgb).is_err()); + } } diff --git a/src/jzczhz.rs b/src/jzczhz.rs index 2d8bed5..a38175e 100644 --- a/src/jzczhz.rs +++ b/src/jzczhz.rs @@ -5,7 +5,7 @@ * // license that can be found in the LICENSE file. */ use crate::{EuclideanDistance, Jzazbz, Rgb, TaxicabDistance, TransferFunction, Xyz}; -use erydanos::{eatan2f, ehypot3f, ehypotf, Cosine, Sine}; +use erydanos::{Cosine, Sine, eatan2f, ehypot3f, ehypotf}; use num_traits::Pow; use std::ops::{ Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign, diff --git a/src/lab.rs b/src/lab.rs index 587a32a..c5f4df2 100644 --- a/src/lab.rs +++ b/src/lab.rs @@ -4,10 +4,10 @@ * // Use of this source code is governed by a BSD-style * // license that can be found in the LICENSE file. */ +use crate::EuclideanDistance; use crate::rgb::Rgb; use crate::taxicab::TaxicabDistance; use crate::xyz::Xyz; -use crate::EuclideanDistance; use num_traits::Pow; use std::ops::{ Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign, diff --git a/src/lalphabeta.rs b/src/lalphabeta.rs index 79b8e43..0a13bfd 100644 --- a/src/lalphabeta.rs +++ b/src/lalphabeta.rs @@ -4,7 +4,7 @@ * // Use of this source code is governed by a BSD-style * // license that can be found in the LICENSE file. */ -use crate::{Rgb, TransferFunction, Xyz, SRGB_TO_XYZ_D65, XYZ_TO_SRGB_D65}; +use crate::{Rgb, SRGB_TO_XYZ_D65, TransferFunction, XYZ_TO_SRGB_D65, Xyz}; use std::ops::{Index, IndexMut, Neg}; /// Represents l-alpha-beta (lαβ) colorspace diff --git a/src/lalphabeta_to_image.rs b/src/lalphabeta_to_image.rs index 97ccfa7..24b52db 100644 --- a/src/lalphabeta_to_image.rs +++ b/src/lalphabeta_to_image.rs @@ -5,106 +5,120 @@ * // license that can be found in the LICENSE file. */ use crate::image::ImageConfiguration; -use crate::{LAlphaBeta, Rgb, TransferFunction, XYZ_TO_SRGB_D65}; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -use std::slice; +use crate::{ + ColorError, ImageBuffer, ImageBufferMut, LAlphaBeta, TransferFunction, XYZ_TO_SRGB_D65, +}; fn lalphabeta_to_image( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - - let channels = image_configuration.channel_count(); - - let mut lut_table = vec![0u8; 2049]; - for (i, lut) in lut_table.iter_mut().enumerate() { - *lut = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.).min(255.) as u8; +) -> Result<(), ColorError> { + dst.validate()?; + src.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); } - let src_slice_safe_align = unsafe { - slice::from_raw_parts( - src.as_ptr() as *const u8, - src_stride as usize * height as usize, - ) - }; + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + let channels = image_configuration.channel_count(); - let iter; - #[cfg(feature = "rayon")] - { - iter = dst - .par_chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.par_chunks_exact(src_stride as usize)); - } - #[cfg(not(feature = "rayon"))] - { - iter = dst - .chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.chunks_exact(src_stride as usize)); + let mut lut_table = [0u8; 65536]; + for (i, lut) in lut_table[..2049].iter_mut().enumerate() { + *lut = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.) + .round() + .min(255.) as u8; } - iter.for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; - - let src_ptr = src.as_ptr() as *mut f32; + let mut transient_row = vec![0f32; src.width as usize * channels]; - let mut transient_row = vec![0f32; width as usize * channels]; + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; - for x in _cx..width as usize { - let px = x * channels; - let l_x = src_ptr.add(px).read_unaligned(); - let l_y = src_ptr.add(px + 1).read_unaligned(); - let l_z = src_ptr.add(px + 2).read_unaligned(); - let lalphabeta = LAlphaBeta::new(l_x, l_y, l_z); - let rgb = lalphabeta.to_linear_rgb(&XYZ_TO_SRGB_D65); - - let dst = transient_row.get_unchecked_mut((x * channels)..); - *dst.get_unchecked_mut(image_configuration.r_index()) = rgb.r; - *dst.get_unchecked_mut(image_configuration.g_index()) = rgb.g; - *dst.get_unchecked_mut(image_configuration.b_index()) = rgb.b; - if image_configuration.has_alpha() { - let l_a = src_ptr.add(px + 3).read_unaligned(); - let a_value = (l_a * 255f32).max(0f32).round(); - *dst.get_unchecked_mut(image_configuration.a_index()) = a_value; + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) + { + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; + + if image_configuration.has_alpha() { + for (transient, src) in transient_row + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + let lalphabeta = LAlphaBeta::new(src[0], src[1], src[2]); + let rgb = lalphabeta.to_linear_rgb(&XYZ_TO_SRGB_D65); + transient[image_configuration.r_index()] = rgb.r; + transient[image_configuration.g_index()] = rgb.g; + transient[image_configuration.b_index()] = rgb.b; + let a = src[3]; + transient[image_configuration.a_index()] = (a * 255f32).min(255.).round(); + } + } else { + for (transient, src) in transient_row + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + let lalphabeta = LAlphaBeta::new(src[0], src[1], src[2]); + let rgb = lalphabeta.to_linear_rgb(&XYZ_TO_SRGB_D65); + transient[image_configuration.r_index()] = rgb.r; + transient[image_configuration.g_index()] = rgb.g; + transient[image_configuration.b_index()] = rgb.b; } } - for (dst, src) in dst - .chunks_exact_mut(channels) - .zip(transient_row.chunks_exact(channels)) + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb { - let r = src[image_configuration.r_index()]; - let g = src[image_configuration.g_index()]; - let b = src[image_configuration.b_index()]; - - let rgb = (Rgb::::new( - r.min(1f32).max(0f32), - g.min(1f32).max(0f32), - b.min(1f32).max(0f32), - ) * Rgb::::dup(2048f32)) - .round() - .cast::(); - - *dst.get_unchecked_mut(image_configuration.r_index()) = - *lut_table.get_unchecked(rgb.r.min(2048) as usize); - *dst.get_unchecked_mut(image_configuration.g_index()) = - *lut_table.get_unchecked(rgb.g.min(2048) as usize); - *dst.get_unchecked_mut(image_configuration.b_index()) = - *lut_table.get_unchecked(rgb.b.min(2048) as usize); - if image_configuration.has_alpha() { - *dst.get_unchecked_mut(image_configuration.a_index()) = - *src.get_unchecked(image_configuration.a_index()) as u8; + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(transient_row.as_chunks::<3>().0.iter()) + { + let r = src[image_configuration.r_index()]; + let g = src[image_configuration.g_index()]; + let b = src[image_configuration.b_index()]; + let ri = (r * 2048.).round() as u16; + let gi = (g * 2048.).round() as u16; + let bi = (b * 2048.).round() as u16; + dst[image_configuration.r_index()] = lut_table[ri.min(2048) as usize]; + dst[image_configuration.g_index()] = lut_table[gi.min(2048) as usize]; + dst[image_configuration.b_index()] = lut_table[bi.min(2048) as usize]; + } + } else if image_configuration == ImageConfiguration::Rgba + || image_configuration == ImageConfiguration::Bgra + { + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(transient_row.as_chunks::<4>().0.iter()) + { + let r = src[image_configuration.r_index()]; + let g = src[image_configuration.g_index()]; + let b = src[image_configuration.b_index()]; + let ri = (r * 2048.).round() as u16; + let gi = (g * 2048.).round() as u16; + let bi = (b * 2048.).round() as u16; + dst[image_configuration.r_index()] = lut_table[ri.min(2048) as usize]; + dst[image_configuration.g_index()] = lut_table[gi.min(2048) as usize]; + dst[image_configuration.b_index()] = lut_table[bi.min(2048) as usize]; + dst[image_configuration.a_index()] = src[image_configuration.a_index()] as u8; } } - }); + } + + Ok(()) } /// This function converts *lαβ* with interleaved alpha channel to RGBA. This is much more effective than naive direct transformation @@ -118,23 +132,11 @@ fn lalphabeta_to_image( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn lalphabeta_to_rgba( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { - lalphabeta_to_image::<{ ImageConfiguration::Rgba as u8 }>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + lalphabeta_to_image::<{ ImageConfiguration::Rgba as u8 }>(src, dst, transfer_function) } /// This function converts *lαβ* to RGB. This is much more effective than naive direct transformation @@ -148,23 +150,11 @@ pub fn lalphabeta_to_rgba( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn lalphabeta_to_rgb( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { - lalphabeta_to_image::<{ ImageConfiguration::Rgb as u8 }>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + lalphabeta_to_image::<{ ImageConfiguration::Rgb as u8 }>(src, dst, transfer_function) } /// This function converts *lαβ* to BGR. This is much more effective than naive direct transformation @@ -178,23 +168,11 @@ pub fn lalphabeta_to_rgb( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn lalphabeta_to_bgr( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { - lalphabeta_to_image::<{ ImageConfiguration::Bgr as u8 }>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + lalphabeta_to_image::<{ ImageConfiguration::Bgr as u8 }>(src, dst, transfer_function) } /// This function converts *lαβ* with interleaved alpha channel to BGRA. This is much more effective than naive direct transformation @@ -208,21 +186,9 @@ pub fn lalphabeta_to_bgr( /// * `height` - Image height /// * `transfer_function` - Transfer function from linear colorspace to gamma pub fn lalphabeta_to_bgra( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { - lalphabeta_to_image::<{ ImageConfiguration::Bgra as u8 }>( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + lalphabeta_to_image::<{ ImageConfiguration::Bgra as u8 }>(src, dst, transfer_function) } diff --git a/src/lib.rs b/src/lib.rs index d797ad4..a75e528 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -105,8 +105,8 @@ pub use rgba::Rgb565; pub use rgba::Rgba; pub use rgba::Rgba1010102; pub use rgba::ToRgb565; -pub use rgba::ToRgba1010102; pub use rgba::ToRgba8; +pub use rgba::ToRgba1010102; pub use rgba::ToRgbaF16; pub use rgba::ToRgbaF32; pub use xyb::Xyb; diff --git a/src/linear_to_image.rs b/src/linear_to_image.rs index 9151559..6c5cd49 100644 --- a/src/linear_to_image.rs +++ b/src/linear_to_image.rs @@ -6,102 +6,84 @@ */ use crate::gamma_curves::TransferFunction; use crate::image::ImageConfiguration; -use crate::Rgb; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -use std::slice; - -#[allow(clippy::type_complexity)] +use crate::{ColorError, ImageBuffer, ImageBufferMut}; + fn linear_to_gamma_channels( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { + dst.validate()?; + src.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); if USE_ALPHA && !image_configuration.has_alpha() { panic!("Alpha may be set only on images with alpha"); } - let channels = image_configuration.channel_count(); - - let mut lut_table = vec![0u8; 2049]; + let mut lut_table = [0u8; 2049]; for (i, lut) in lut_table.iter_mut().enumerate() { - *lut = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.).min(255.) as u8; + *lut = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.) + .round() + .min(255.) as u8; } - let src_slice_safe_align = unsafe { - slice::from_raw_parts( - src.as_ptr() as *const u8, - src_stride as usize * height as usize, - ) - }; + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; - let iter; - #[cfg(feature = "rayon")] + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) { - iter = dst - .par_chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.par_chunks_exact(src_stride as usize)); - } - #[cfg(not(feature = "rayon"))] - { - iter = dst - .chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.chunks_exact(src_stride as usize)); - } - - iter.for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; - - let src_ptr = src.as_ptr() as *const f32; - let dst_ptr = dst.as_mut_ptr(); - - for x in _cx..width as usize { - let px = x * channels; - let src_slice = src_ptr.add(px); - let r = src_slice - .add(image_configuration.r_index()) - .read_unaligned(); - let g = src_slice - .add(image_configuration.g_index()) - .read_unaligned(); - let b = src_slice - .add(image_configuration.b_index()) - .read_unaligned(); - - let rgb = (Rgb::::new( - r.min(1f32).max(0f32), - g.min(1f32).max(0f32), - b.min(1f32).max(0f32), - ) * Rgb::::dup(2048f32)) - .round() - .cast::(); - - let dst = dst_ptr.add(px); - - dst.add(image_configuration.r_index()) - .write_unaligned(*lut_table.get_unchecked(rgb.r.min(2048) as usize)); - dst.add(image_configuration.g_index()) - .write_unaligned(*lut_table.get_unchecked(rgb.g.min(2048) as usize)); - dst.add(image_configuration.b_index()) - .write_unaligned(*lut_table.get_unchecked(rgb.b.min(2048) as usize)); - - if USE_ALPHA && image_configuration.has_alpha() { - let a = src_slice - .add(image_configuration.a_index()) - .read_unaligned(); - let a_lin = (a * 255f32).round() as u8; - dst.add(image_configuration.a_index()) - .write_unaligned(a_lin); + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; + + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + let r = (src[image_configuration.r_index()] * 2048.).round() as usize; + let g = (src[image_configuration.g_index()] * 2048.).round() as usize; + let b = (src[image_configuration.b_index()] * 2048.).round() as usize; + dst[image_configuration.r_index()] = lut_table[r.min(2048)]; + dst[image_configuration.g_index()] = lut_table[g.min(2048)]; + dst[image_configuration.b_index()] = lut_table[b.min(2048)]; + } + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba + { + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + let r = (src[image_configuration.r_index()] * 2048.).round() as usize; + let g = (src[image_configuration.g_index()] * 2048.).round() as usize; + let b = (src[image_configuration.b_index()] * 2048.).round() as usize; + dst[image_configuration.r_index()] = lut_table[r.min(2048)]; + dst[image_configuration.g_index()] = lut_table[g.min(2048)]; + dst[image_configuration.b_index()] = lut_table[b.min(2048)]; + dst[image_configuration.a_index()] = (src[image_configuration.a_index()] * 255.) + .round() + .min(255.) as u8; } } - }); + } + + Ok(()) } /// This function converts Linear to RGB. This is much more effective than naive direct transformation @@ -115,23 +97,15 @@ fn linear_to_gamma_channels, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { linear_to_gamma_channels::<{ ImageConfiguration::Rgb as u8 }, false>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts Linear RGBA to RGBA, Alpha channel will be denormalized. This is much more effective than naive direct transformation @@ -145,23 +119,15 @@ pub fn linear_to_rgb( /// * `height` - Image height /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn linear_to_rgba( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { linear_to_gamma_channels::<{ ImageConfiguration::Rgba as u8 }, true>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts Linear BGRA to BGRA, Alpha channel will de dernormalizaed. This is much more effective than naive direct transformation @@ -175,23 +141,15 @@ pub fn linear_to_rgba( /// * `height` - Image height /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn linear_to_bgra( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { linear_to_gamma_channels::<{ ImageConfiguration::Bgra as u8 }, true>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts Linear BGR to Gamma BGR. This is much more effective than naive direct transformation @@ -205,21 +163,348 @@ pub fn linear_to_bgra( /// * `height` - Image height /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn linear_to_bgr( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { linear_to_gamma_channels::<{ ImageConfiguration::Bgr as u8 }, false>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) +} + +#[cfg(test)] +mod tests_linear_f32_to_gamma { + use super::*; + use crate::BufferStore; + + fn make_src( + data: Vec, + width: u32, + height: u32, + channels: u32, + ) -> ImageBuffer<'static, f32> { + ImageBuffer::from_vec(data, width, height, width * channels, channels).unwrap() + } + + fn make_dst(width: u32, height: u32, channels: u32) -> ImageBufferMut<'static, u8> { + ImageBufferMut::new( + BufferStore::Owned(vec![0u8; (width * height * channels) as usize]), + width, + height, + width * channels, + channels, + ) + .unwrap() + } + + fn lut_encode(v: f32) -> u8 { + let idx = (v.clamp(0., 1.) * 2048.).round() as usize; + let idx = idx.min(2048); + (TransferFunction::Srgb.gamma(idx as f32 / 2048.0) * 255.) + .round() + .min(255.) as u8 + } + + fn assert_approx(a: u8, b: u8, label: &str) { + assert!( + (a as i16 - b as i16).abs() <= 1, + "{label}: got {a}, expected {b}" + ); + } + + // ── boundary values ─────────────────────────────────────────────────────── + + #[test] + fn rgb_zero_maps_to_zero() { + let src = make_src(vec![0.0, 0.0, 0.0], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst.data.borrow(), &[0, 0, 0]); + } + + #[test] + fn rgb_one_maps_to_255() { + let src = make_src(vec![1.0, 1.0, 1.0], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst.data.borrow(), &[255, 255, 255]); + } + + #[test] + fn bgr_zero_maps_to_zero() { + let src = make_src(vec![0.0, 0.0, 0.0], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_to_bgr(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst.data.borrow(), &[0, 0, 0]); + } + + #[test] + fn bgr_one_maps_to_255() { + let src = make_src(vec![1.0, 1.0, 1.0], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_to_bgr(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst.data.borrow(), &[255, 255, 255]); + } + + // ── clamping ────────────────────────────────────────────────────────────── + + #[test] + fn rgb_above_one_clamps_to_255() { + for v in [1.001f32, 1.5, 2.0, 100.0] { + let src = make_src(vec![v, v, v], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst.data.borrow(), &[255, 255, 255], "v={v}"); + } + } + + #[test] + fn rgb_below_zero_clamps_to_zero() { + for v in [-0.001f32, -1.0, -100.0] { + let src = make_src(vec![v, v, v], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst.data.borrow(), &[0, 0, 0], "v={v}"); + } + } + + // ── alpha passthrough ───────────────────────────────────────────────────── + + #[test] + fn rgba_alpha_is_denormalized() { + for alpha in [0.0f32, 0.25, 0.5, 0.75, 1.0] { + let src = make_src(vec![0.5, 0.5, 0.5, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + linear_to_rgba(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let expected_a = (alpha * 255.).round() as u8; + assert_approx(d[3], expected_a, &format!("alpha={alpha}")); + } + } + + #[test] + fn bgra_alpha_is_denormalized() { + for alpha in [0.0f32, 0.25, 0.5, 0.75, 1.0] { + let src = make_src(vec![0.5, 0.5, 0.5, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + linear_to_bgra(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let expected_a = (alpha * 255.).round() as u8; + assert_approx(d[3], expected_a, &format!("alpha={alpha}")); + } + } + + #[test] + fn rgba_rgb_channels_encoded_regardless_of_alpha() { + let src = make_src(vec![0.5, 0.3, 0.1, 0.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + linear_to_rgba(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_ne!(d[0], 0, "R must be gamma-encoded even when alpha=0"); + assert_eq!(d[3], 0, "alpha must be 0"); + } + + // ── lut correctness ─────────────────────────────────────────────────────── + + #[test] + fn rgb_lut_matches_expected_encoding() { + for v in [0.0f32, 0.1, 0.2, 0.5, 0.8, 1.0] { + let expected = lut_encode(v); + let src = make_src(vec![v, v, v], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], expected, &format!("R v={v}")); + assert_approx(d[1], expected, &format!("G v={v}")); + assert_approx(d[2], expected, &format!("B v={v}")); + } + } + + #[test] + fn bgr_lut_matches_expected_encoding() { + for v in [0.0f32, 0.25, 0.5, 0.75, 1.0] { + let expected = lut_encode(v); + let src = make_src(vec![v, v, v], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_to_bgr(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], expected, &format!("v={v}")); + } + } + + #[test] + fn rgba_rgb_lut_matches_expected_encoding() { + for v in [0.0f32, 0.25, 0.5, 0.75, 1.0] { + let expected = lut_encode(v); + let src = make_src(vec![v, v, v, 1.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + linear_to_rgba(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], expected, &format!("R v={v}")); + assert_approx(d[1], expected, &format!("G v={v}")); + assert_approx(d[2], expected, &format!("B v={v}")); + } + } + + // ── channel ordering: RGB vs BGR ────────────────────────────────────────── + + #[test] + fn rgb_bgr_agree_on_grey() { + for v in [0.1f32, 0.3, 0.5, 0.7, 0.9] { + let src_rgb = make_src(vec![v, v, v], 1, 1, 3); + let src_bgr = make_src(vec![v, v, v], 1, 1, 3); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + linear_to_rgb(&src_rgb, &mut dst_rgb, TransferFunction::Srgb).unwrap(); + linear_to_bgr(&src_bgr, &mut dst_bgr, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst_rgb.data.borrow(), &*dst_bgr.data.borrow(), "v={v}"); + } + } + + #[test] + fn rgba_bgra_agree_on_grey() { + for v in [0.2f32, 0.5, 0.8] { + let src_rgba = make_src(vec![v, v, v, 0.6], 1, 1, 4); + let src_bgra = make_src(vec![v, v, v, 0.6], 1, 1, 4); + let mut dst_rgba = make_dst(1, 1, 4); + let mut dst_bgra = make_dst(1, 1, 4); + linear_to_rgba(&src_rgba, &mut dst_rgba, TransferFunction::Srgb).unwrap(); + linear_to_bgra(&src_bgra, &mut dst_bgra, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst_rgba.data.borrow(), &*dst_bgra.data.borrow(), "v={v}"); + } + } + + #[test] + fn rgb_distinct_channels_ordered_correctly() { + // R=1.0 G=0.0 B=0.5 — output must respect channel positions + let src = make_src(vec![1.0, 0.0, 0.5], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[0], 255, "R"); + assert_eq!(d[1], 0, "G"); + assert_approx(d[2], lut_encode(0.5), "B"); + } + + #[test] + fn bgr_distinct_channels_ordered_correctly() { + // BGR layout: first byte = B, second = G, third = R + let src = make_src(vec![0.0, 0.5, 1.0], 1, 1, 3); // B=0, G=0.5, R=1.0 + let mut dst = make_dst(1, 1, 3); + linear_to_bgr(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[0], 0, "B"); + assert_approx(d[1], lut_encode(0.5), "G"); + assert_eq!(d[2], 255, "R"); + } + + // ── linear transfer is identity ─────────────────────────────────────────── + + #[test] + fn linear_transfer_rgb_is_identity() { + for v in [0.0f32, 0.25, 0.5, 0.75, 1.0] { + let expected = (v * 255.).round() as u8; + let src = make_src(vec![v, v, v], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_to_rgb(&src, &mut dst, TransferFunction::Linear).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], expected, &format!("v={v}")); + } + } + + #[test] + fn linear_transfer_rgba_rgb_is_identity() { + for v in [0.0f32, 0.5, 1.0] { + let expected = (v * 255.).round() as u8; + let src = make_src(vec![v, v, v, 0.5], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + linear_to_rgba(&src, &mut dst, TransferFunction::Linear).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], expected, &format!("R v={v}")); + assert_approx(d[1], expected, &format!("G v={v}")); + assert_approx(d[2], expected, &format!("B v={v}")); + } + } + + // ── monotonicity ────────────────────────────────────────────────────────── + + #[test] + fn rgb_output_monotonically_non_decreasing() { + let steps = 256usize; + let data: Vec = (0..steps) + .flat_map(|i| { + let v = i as f32 / (steps - 1) as f32; + [v, v, v] + }) + .collect(); + let src = make_src(data, steps as u32, 1, 3); + let mut dst = make_dst(steps as u32, 1, 3); + linear_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let reds: Vec = d.chunks(3).map(|c| c[0]).collect(); + for i in 1..reds.len() { + assert!( + reds[i] >= reds[i - 1], + "non-monotone at {i}: {} < {}", + reds[i], + reds[i - 1] + ); + } + } + + // ── multi-pixel / multi-row consistency ─────────────────────────────────── + + #[test] + fn rgb_multi_pixel_consistent() { + let src = make_src(vec![0.4, 0.6, 0.8f32].repeat(6), 6, 1, 3); + let mut dst = make_dst(6, 1, 3); + linear_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2]]; + for (i, chunk) in d.chunks(3).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn rgba_multi_pixel_consistent() { + let src = make_src(vec![0.3, 0.5, 0.7, 0.9f32].repeat(4), 4, 1, 4); + let mut dst = make_dst(4, 1, 4); + linear_to_rgba(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2], d[3]]; + for (i, chunk) in d.chunks(4).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn rgb_multi_row_consistent() { + let src = make_src(vec![0.2, 0.4, 0.6f32].repeat(4), 2, 2, 3); + let mut dst = make_dst(2, 2, 3); + linear_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2]]; + for (i, chunk) in d.chunks(3).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + // ── error paths ─────────────────────────────────────────────────────────── + + #[test] + fn rgb_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0f32; 4], 1, 1, 4, 4).unwrap(); + let mut dst = make_dst(1, 1, 3); + assert!(linear_to_rgb(&src, &mut dst, TransferFunction::Srgb).is_err()); + } + + #[test] + fn rgba_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0f32; 3], 1, 1, 3, 3).unwrap(); + let mut dst = make_dst(1, 1, 4); + assert!(linear_to_rgba(&src, &mut dst, TransferFunction::Srgb).is_err()); + } } diff --git a/src/linear_to_image_u8.rs b/src/linear_to_image_u8.rs index 06c9645..83dbb57 100644 --- a/src/linear_to_image_u8.rs +++ b/src/linear_to_image_u8.rs @@ -7,100 +7,68 @@ use crate::gamma_curves::TransferFunction; use crate::image::ImageConfiguration; -use crate::Rgb; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; +use crate::{ColorError, ImageBuffer, ImageBufferMut}; -#[allow(clippy::type_complexity)] fn linear_to_gamma_channels( - src: &[u8], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - _height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { + dst.validate()?; + src.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); if USE_ALPHA && !image_configuration.has_alpha() { panic!("Alpha may be set only on images with alpha"); } - let channels = image_configuration.channel_count(); - - let mut lut_table = vec![0u8; 256]; + let mut lut_table = [0u8; 256]; for (i, lut) in lut_table.iter_mut().enumerate() { - *lut = (transfer_function.gamma(i as f32 * (1. / 255.0)) * 255.).min(255.) as u8; + *lut = (transfer_function.gamma(i as f32 * (1. / 255.0)) * 255.) + .round() + .min(255.) as u8; } - #[cfg(feature = "rayon")] - { - dst.par_chunks_exact_mut(dst_stride as usize) - .zip(src.par_chunks_exact(src_stride as usize)) - .for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; - - for x in _cx..width as usize { - let px = x * channels; - let r = *src.get_unchecked(px + image_configuration.r_index()); - let g = *src.get_unchecked(px + image_configuration.g_index()); - let b = *src.get_unchecked(px + image_configuration.b_index()); - - let rgb = Rgb::::new(r, g, b); + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; - let dst = dst.get_unchecked_mut(px..); - - *dst.get_unchecked_mut(image_configuration.r_index()) = - *lut_table.get_unchecked(rgb.r as usize); - *dst.get_unchecked_mut(image_configuration.g_index()) = - *lut_table.get_unchecked(rgb.g as usize); - *dst.get_unchecked_mut(image_configuration.b_index()) = - *lut_table.get_unchecked(rgb.b as usize); - - if USE_ALPHA && image_configuration.has_alpha() { - let a = src.get_unchecked(px + image_configuration.a_index()); - *dst.get_unchecked_mut(image_configuration.a_index()) = *a; - } - } - }); - } - - #[cfg(not(feature = "rayon"))] + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) { - for (dst, src) in dst - .chunks_exact_mut(dst_stride as usize) - .zip(src.chunks_exact(src_stride as usize)) + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; + + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb { - unsafe { - let mut _cx = 0usize; - - for x in _cx..width as usize { - let px = x * channels; - let r = *src.get_unchecked(px + image_configuration.get_r_channel_offset()); - let g = *src.get_unchecked(px + image_configuration.get_g_channel_offset()); - let b = *src.get_unchecked(px + image_configuration.get_b_channel_offset()); - - let rgb = Rgb::::new(r, g, b); - - let dst = dst.get_unchecked_mut(px..); - - *dst.get_unchecked_mut(image_configuration.get_r_channel_offset()) = - *lut_table.get_unchecked(rgb.r as usize); - *dst.get_unchecked_mut(image_configuration.get_g_channel_offset()) = - *lut_table.get_unchecked(rgb.g as usize); - *dst.get_unchecked_mut(image_configuration.get_b_channel_offset()) = - *lut_table.get_unchecked(rgb.b as usize); - - if USE_ALPHA && image_configuration.has_alpha() { - let a = src.get_unchecked(px + image_configuration.get_a_channel_offset()); - *dst.get_unchecked_mut(image_configuration.get_a_channel_offset()) = *a; - } - } + for (dst, &src) in dst.iter_mut().zip(src.iter()) { + *dst = lut_table[src as usize]; + } + } else if image_configuration == ImageConfiguration::Bgra + || image_configuration == ImageConfiguration::Rgba + { + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + dst[0] = lut_table[src[0] as usize]; + dst[1] = lut_table[src[1] as usize]; + dst[2] = lut_table[src[2] as usize]; + dst[3] = src[3]; } } } + + Ok(()) } /// This function converts Linear to RGB. This is much more effective than naive direct transformation @@ -114,23 +82,15 @@ fn linear_to_gamma_channels, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { linear_to_gamma_channels::<{ ImageConfiguration::Rgb as u8 }, false>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts Linear RGBA to RGBA, Alpha channel will be denormalized. This is much more effective than naive direct transformation @@ -144,23 +104,15 @@ pub fn linear_u8_to_rgb( /// * `height` - Image height /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn linear_u8_to_rgba( - src: &[u8], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { linear_to_gamma_channels::<{ ImageConfiguration::Rgba as u8 }, true>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts Linear BGRA to BGRA, Alpha channel will de dernormalizaed. This is much more effective than naive direct transformation @@ -174,23 +126,15 @@ pub fn linear_u8_to_rgba( /// * `height` - Image height /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn linear_u8_to_bgra( - src: &[u8], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { linear_to_gamma_channels::<{ ImageConfiguration::Bgra as u8 }, true>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) } /// This function converts Linear BGR to Gamma BGR. This is much more effective than naive direct transformation @@ -204,21 +148,288 @@ pub fn linear_u8_to_bgra( /// * `height` - Image height /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn linear_u8_to_bgr( - src: &[u8], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { linear_to_gamma_channels::<{ ImageConfiguration::Bgr as u8 }, false>( src, - src_stride, dst, - dst_stride, - width, - height, transfer_function, - ); + ) +} + +#[cfg(test)] +mod tests_linear_u8_to_gamma { + use super::*; + use crate::BufferStore; + + fn make_src(data: Vec, width: u32, height: u32, channels: u32) -> ImageBuffer<'static, u8> { + ImageBuffer::from_vec(data, width, height, width * channels, channels).unwrap() + } + + fn make_dst(width: u32, height: u32, channels: u32) -> ImageBufferMut<'static, u8> { + ImageBufferMut::new( + BufferStore::Owned(vec![0u8; (width * height * channels) as usize]), + width, + height, + width * channels, + channels, + ) + .unwrap() + } + + // ── boundary values ─────────────────────────────────────────────────────── + + #[test] + fn rgb_zero_maps_to_zero() { + let src = make_src(vec![0, 0, 0], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_u8_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst.data.borrow(), &[0, 0, 0]); + } + + #[test] + fn rgb_255_maps_to_255() { + let src = make_src(vec![255, 255, 255], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_u8_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst.data.borrow(), &[255, 255, 255]); + } + + #[test] + fn bgr_zero_maps_to_zero() { + let src = make_src(vec![0, 0, 0], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_u8_to_bgr(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst.data.borrow(), &[0, 0, 0]); + } + + #[test] + fn bgr_255_maps_to_255() { + let src = make_src(vec![255, 255, 255], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_u8_to_bgr(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst.data.borrow(), &[255, 255, 255]); + } + + // ── alpha passthrough (not gamma-encoded) ───────────────────────────────── + + #[test] + fn rgba_alpha_is_not_transformed() { + for alpha in [0u8, 64, 128, 192, 255] { + let src = make_src(vec![128, 128, 128, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + linear_u8_to_rgba(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[3], alpha, "alpha={alpha} was modified"); + } + } + + #[test] + fn bgra_alpha_is_not_transformed() { + for alpha in [0u8, 64, 128, 192, 255] { + let src = make_src(vec![100, 100, 100, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + linear_u8_to_bgra(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[3], alpha, "alpha={alpha} was modified"); + } + } + + #[test] + fn rgba_alpha_zero_rgb_still_transformed() { + let src = make_src(vec![128, 64, 32, 0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + linear_u8_to_rgba(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + // RGB channels must be gamma-encoded regardless of alpha + let expected_r = (TransferFunction::Srgb.gamma(128.0 / 255.0) * 255.0).round() as u8; + assert_eq!(d[0], expected_r, "R channel not transformed when alpha=0"); + assert_eq!(d[3], 0, "alpha must remain 0"); + } + + // ── lut correctness: gamma(x) = expected ───────────────────────────────── + + #[test] + fn rgb_lut_matches_direct_gamma() { + for input in (0u8..=255).step_by(16) { + let expected = (TransferFunction::Srgb.gamma(input as f32 / 255.0) * 255.0) + .round() + .min(255.0) as u8; + let src = make_src(vec![input, input, input], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_u8_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[0], expected, "input={input}"); + assert_eq!(d[1], expected, "input={input}"); + assert_eq!(d[2], expected, "input={input}"); + } + } + + #[test] + fn bgr_lut_matches_direct_gamma() { + for input in (0u8..=255).step_by(16) { + let expected = (TransferFunction::Srgb.gamma(input as f32 / 255.0) * 255.0) + .round() + .min(255.0) as u8; + let src = make_src(vec![input, input, input], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + linear_u8_to_bgr(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[0], expected, "input={input}"); + } + } + + #[test] + fn rgba_rgb_channels_lut_matches_direct_gamma() { + for input in (0u8..=255).step_by(32) { + let expected = (TransferFunction::Srgb.gamma(input as f32 / 255.0) * 255.0) + .round() + .min(255.0) as u8; + let src = make_src(vec![input, input, input, 255], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + linear_u8_to_rgba(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[0], expected, "R input={input}"); + assert_eq!(d[1], expected, "G input={input}"); + assert_eq!(d[2], expected, "B input={input}"); + } + } + + // ── rgb and bgr produce same output for grey pixels ─────────────────────── + + #[test] + fn rgb_bgr_agree_on_grey() { + for v in (0u8..=255).step_by(32) { + let src_rgb = make_src(vec![v, v, v], 1, 1, 3); + let src_bgr = make_src(vec![v, v, v], 1, 1, 3); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + linear_u8_to_rgb(&src_rgb, &mut dst_rgb, TransferFunction::Srgb).unwrap(); + linear_u8_to_bgr(&src_bgr, &mut dst_bgr, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst_rgb.data.borrow(), &*dst_bgr.data.borrow(), "v={v}"); + } + } + + #[test] + fn rgba_bgra_agree_on_grey() { + for v in (0u8..=255).step_by(32) { + let src_rgba = make_src(vec![v, v, v, 200], 1, 1, 4); + let src_bgra = make_src(vec![v, v, v, 200], 1, 1, 4); + let mut dst_rgba = make_dst(1, 1, 4); + let mut dst_bgra = make_dst(1, 1, 4); + linear_u8_to_rgba(&src_rgba, &mut dst_rgba, TransferFunction::Srgb).unwrap(); + linear_u8_to_bgra(&src_bgra, &mut dst_bgra, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst_rgba.data.borrow(), &*dst_bgra.data.borrow(), "v={v}"); + } + } + + // ── linear transfer function is identity ────────────────────────────────── + + #[test] + fn linear_transfer_rgb_is_identity() { + let data: Vec = (0u8..=255).step_by(4).flat_map(|v| [v, v, v]).collect(); + let px = data.len() as u32 / 3; + let src = make_src(data.clone(), px, 1, 3); + let mut dst = make_dst(px, 1, 3); + linear_u8_to_rgb(&src, &mut dst, TransferFunction::Linear).unwrap(); + assert_eq!(&*dst.data.borrow(), data.as_slice()); + } + + #[test] + fn linear_transfer_rgba_rgb_channels_identity() { + let data: Vec = (0u8..=255) + .step_by(16) + .flat_map(|v| [v, v, v, 128u8]) + .collect(); + let px = data.len() as u32 / 4; + let src = make_src(data.clone(), px, 1, 4); + let mut dst = make_dst(px, 1, 4); + linear_u8_to_rgba(&src, &mut dst, TransferFunction::Linear).unwrap(); + let d = dst.data.borrow(); + for (i, chunk) in d.chunks(4).enumerate() { + let orig = data[i * 4]; + assert_eq!(chunk[0], orig, "R px={i}"); + assert_eq!(chunk[1], orig, "G px={i}"); + assert_eq!(chunk[2], orig, "B px={i}"); + assert_eq!(chunk[3], 128, "A px={i}"); + } + } + + // ── monotonicity ────────────────────────────────────────────────────────── + + #[test] + fn rgb_output_monotonically_non_decreasing() { + let data: Vec = (0u8..=255).flat_map(|v| [v, v, v]).collect(); + let src = make_src(data, 256, 1, 3); + let mut dst = make_dst(256, 1, 3); + linear_u8_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + // check only the R channel (all equal anyway) + let reds: Vec = d.chunks(3).map(|c| c[0]).collect(); + for i in 1..reds.len() { + assert!( + reds[i] >= reds[i - 1], + "non-monotone at index {i}: {} < {}", + reds[i], + reds[i - 1] + ); + } + } + + // ── multi-pixel / multi-row consistency ─────────────────────────────────── + + #[test] + fn rgb_multi_pixel_consistent() { + let src = make_src(vec![100, 150, 200].repeat(6), 6, 1, 3); + let mut dst = make_dst(6, 1, 3); + linear_u8_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2]]; + for (i, chunk) in d.chunks(3).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn rgb_multi_row_consistent() { + let src = make_src(vec![80, 160, 240].repeat(4), 2, 2, 3); + let mut dst = make_dst(2, 2, 3); + linear_u8_to_rgb(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2]]; + for (i, chunk) in d.chunks(3).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn rgba_multi_pixel_consistent() { + let src = make_src(vec![100, 150, 200, 128].repeat(4), 4, 1, 4); + let mut dst = make_dst(4, 1, 4); + linear_u8_to_rgba(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2], d[3]]; + for (i, chunk) in d.chunks(4).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + // ── channels mismatch returns error ─────────────────────────────────────── + + #[test] + fn rgb_rejects_wrong_channel_count() { + // 4-channel src passed to rgb (expects 3) + let src = make_src(vec![0u8; 4], 1, 1, 4); + let mut dst = make_dst(1, 1, 3); + assert!(linear_u8_to_rgb(&src, &mut dst, TransferFunction::Srgb).is_err()); + } + + #[test] + fn rgba_rejects_wrong_channel_count() { + let src = make_src(vec![0u8; 3], 1, 1, 3); + let mut dst = make_dst(1, 1, 4); + assert!(linear_u8_to_rgba(&src, &mut dst, TransferFunction::Srgb).is_err()); + } } diff --git a/src/linear_to_planar.rs b/src/linear_to_planar.rs index edca475..6cd7944 100644 --- a/src/linear_to_planar.rs +++ b/src/linear_to_planar.rs @@ -5,85 +5,47 @@ * // license that can be found in the LICENSE file. */ -use crate::TransferFunction; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -use std::slice; +use crate::{ColorError, ImageBuffer, ImageBufferMut, TransferFunction}; #[allow(clippy::type_complexity)] fn linear_to_gamma_channels( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { - let mut lut_table = vec![0u8; 2049]; - for (i, lut) in lut_table.iter_mut().enumerate() { - *lut = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.).min(255.) as u8; +) -> Result<(), ColorError> { + dst.validate()?; + src.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 1 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); } - let src_slice_safe_align = unsafe { - slice::from_raw_parts( - src.as_ptr() as *const u8, - src_stride as usize * height as usize, - ) - }; - - #[cfg(feature = "rayon")] - { - dst.par_chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.par_chunks_exact(src_stride as usize)) - .for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; - - let src_ptr = src.as_ptr() as *const f32; - let dst_ptr = dst.as_mut_ptr(); - - for x in _cx..width as usize { - let px = x; - let src_slice = src_ptr.add(px); - let pixel = - (src_slice.read_unaligned().min(1f32).max(0f32) * 2048f32).round() as usize; - - let dst = dst_ptr.add(px); - let transferred = *lut_table.get_unchecked(pixel.min(2048)); - - dst.write_unaligned(transferred); - } - }); + let mut lut_table = [0u8; 65535]; + for (i, lut) in lut_table[..2049].iter_mut().enumerate() { + *lut = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.) + .round() + .min(255.) as u8; } - #[cfg(not(feature = "rayon"))] + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; + + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) { - for (dst, src) in dst - .chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.chunks_exact(src_stride as usize)) - { - unsafe { - let mut _cx = 0usize; - - let src_ptr = src.as_ptr() as *const f32; - let dst_ptr = dst.as_mut_ptr(); - - for x in _cx..width as usize { - let px = x; - let src_slice = src_ptr.add(px); - let pixel = - (src_slice.read_unaligned().min(1f32).max(0f32) * 2048f32).round() as usize; - - let dst = dst_ptr.add(px); - let transferred = *lut_table.get_unchecked(pixel.min(2048)); - - dst.write_unaligned(transferred); - } - } + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; + for (dst, &src) in dst.iter_mut().zip(src.iter()) { + let px = (src * 2048f32).min(2048.).round() as usize; + *dst = lut_table[px]; } } + + Ok(()) } /// This function converts Linear to Plane. This is much more effective than naive direct transformation @@ -97,21 +59,180 @@ fn linear_to_gamma_channels( /// * `height` - Image height /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn linear_to_plane( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, transfer_function: TransferFunction, -) { - linear_to_gamma_channels( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + linear_to_gamma_channels(src, dst, transfer_function) +} + +#[cfg(test)] +mod tests_linear_to_gamma { + use super::*; + use crate::BufferStore; + + fn make_src(data: Vec, width: u32, height: u32) -> ImageBuffer<'static, f32> { + ImageBuffer::from_vec(data, width, height, width, 1).unwrap() + } + + fn make_dst(width: u32, height: u32) -> ImageBufferMut<'static, u8> { + ImageBufferMut::new( + BufferStore::Owned(vec![0u8; (width * height) as usize]), + width, + height, + width, + 1, + ) + .unwrap() + } + + // ── boundary values ─────────────────────────────────────────────────────── + + #[test] + fn zero_maps_to_zero() { + let src = make_src(vec![0.0], 1, 1); + let mut dst = make_dst(1, 1); + linear_to_plane(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(dst.data.borrow()[0], 0); + } + + #[test] + fn one_maps_to_255() { + let src = make_src(vec![1.0], 1, 1); + let mut dst = make_dst(1, 1); + linear_to_plane(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(dst.data.borrow()[0], 255); + } + + #[test] + fn above_one_clamps_to_255() { + for val in [1.001f32, 2.0, 100.0, f32::MAX] { + let src = make_src(vec![val], 1, 1); + let mut dst = make_dst(1, 1); + linear_to_plane(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(dst.data.borrow()[0], 255, "val={val}"); + } + } + + #[test] + fn negative_clamps_to_zero() { + // negative * 2048 rounds to 0 index → lut[0] = 0 + let src = make_src(vec![-0.5], 1, 1); + let mut dst = make_dst(1, 1); + linear_to_plane(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(dst.data.borrow()[0], 0); + } + + // ── roundtrip: gamma → linear → gamma should recover original ──────────── + + #[test] + fn roundtrip_srgb() { + for original in (0u8..=255).step_by(8) { + let linear = TransferFunction::Srgb.linearize(original as f32 / 255.0); + let src = make_src(vec![linear], 1, 1); + let mut dst = make_dst(1, 1); + linear_to_plane(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let recovered = dst.data.borrow()[0]; + // LUT has 2048 steps → max rounding error is ~1 DN + assert!( + (recovered as i16 - original as i16).abs() <= 2, + "original={original} recovered={recovered}" + ); + } + } + + #[test] + fn roundtrip_linear_transfer() { + for original in (0u8..=255).step_by(16) { + let linear = TransferFunction::Linear.linearize(original as f32 / 255.0); + let src = make_src(vec![linear], 1, 1); + let mut dst = make_dst(1, 1); + linear_to_plane(&src, &mut dst, TransferFunction::Linear).unwrap(); + let recovered = dst.data.borrow()[0]; + assert!( + (recovered as i16 - original as i16).abs() <= 2, + "original={original} recovered={recovered}" + ); + } + } + + // ── monotonicity ────────────────────────────────────────────────────────── + + #[test] + fn output_is_monotonically_non_decreasing() { + let inputs: Vec = (0..=64).map(|i| i as f32 / 64.0).collect(); + let src = make_src(inputs, 65, 1); + let mut dst = make_dst(65, 1); + linear_to_plane(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + for i in 1..d.len() { + assert!( + d[i] >= d[i - 1], + "non-monotone at index {i}: {} < {}", + d[i], + d[i - 1] + ); + } + } + + // ── multi-pixel / multi-row consistency ─────────────────────────────────── + + #[test] + fn multi_pixel_consistent() { + let val = 0.5f32; + let src = make_src(vec![val; 8], 8, 1); + let mut dst = make_dst(8, 1); + linear_to_plane(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert!(d.iter().all(|&v| v == d[0]), "inconsistent pixels: {d:?}"); + } + + #[test] + fn multi_row_consistent() { + let val = 0.3f32; + let src = make_src(vec![val; 4], 2, 2); + let mut dst = make_dst(2, 2); + linear_to_plane(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert!(d.iter().all(|&v| v == d[0]), "row mismatch: {d:?}"); + } + + // ── wrong channel count returns error ───────────────────────────────────── + + #[test] + fn rejects_multi_channel_src() { + // channels=3 is not 1 → must error + let src = ImageBuffer::from_vec(vec![0f32; 3], 1, 1, 3, 3).unwrap(); + let mut dst = make_dst(1, 1); + let result = linear_to_plane(&src, &mut dst, TransferFunction::Srgb); + assert!(result.is_err()); + } + + // ── known values ───────────────────────────────────────────────────────── + + #[test] + fn known_srgb_midtone() { + // sRGB linearised 0.5 ≈ 0.2140, gamma applied back ≈ 128 + let linear = TransferFunction::Srgb.linearize(128.0 / 255.0); + let src = make_src(vec![linear], 1, 1); + let mut dst = make_dst(1, 1); + linear_to_plane(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let v = dst.data.borrow()[0]; + assert!((v as i16 - 128).abs() <= 2, "midtone={v}"); + } + + #[test] + fn linear_transfer_is_identity() { + // For a linear transfer function gamma(x) == x, so output ≈ round(x * 255) + let input = 0.6f32; + let src = make_src(vec![input], 1, 1); + let mut dst = make_dst(1, 1); + linear_to_plane(&src, &mut dst, TransferFunction::Linear).unwrap(); + let expected = (input * 255.0).round() as u8; + let got = dst.data.borrow()[0]; + assert!( + (got as i16 - expected as i16).abs() <= 2, + "expected≈{expected} got={got}" + ); + } } diff --git a/src/neon/cie.rs b/src/neon/cie.rs index 20fb7b4..9617d10 100644 --- a/src/neon/cie.rs +++ b/src/neon/cie.rs @@ -13,8 +13,9 @@ use crate::neon::math::{prefer_vfmaq_f32, vcolorq_matrix_f32, vcubeq_f32}; use erydanos::{vatan2q_f32, vcbrtq_fast_f32, vcosq_f32, vhypotq_fast_f32, vsinq_f32}; use std::arch::aarch64::*; -#[inline(always)] -pub(crate) unsafe fn neon_triple_to_xyz( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_triple_to_xyz( r: float32x4_t, g: float32x4_t, b: float32x4_t, @@ -32,76 +33,86 @@ pub(crate) unsafe fn neon_triple_to_xyz( (x, y, z) } -#[inline(always)] -pub(crate) unsafe fn neon_triple_to_luv( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_triple_to_luv( x: float32x4_t, y: float32x4_t, z: float32x4_t, ) -> (float32x4_t, float32x4_t, float32x4_t) { - let zeros = vdupq_n_f32(0f32); - let den = prefer_vfmaq_f32( - prefer_vfmaq_f32(x, z, vdupq_n_f32(3f32)), - y, - vdupq_n_f32(15f32), - ); - let nan_mask = vceqzq_f32(den); - let l_low_mask = vcltq_f32(y, vdupq_n_f32(LUV_CUTOFF_FORWARD_Y)); - let y_cbrt = vcbrtq_fast_f32(y); - let l = vbslq_f32( - l_low_mask, - vmulq_n_f32(y, LUV_MULTIPLIER_FORWARD_Y), - prefer_vfmaq_f32(vdupq_n_f32(-16f32), y_cbrt, vdupq_n_f32(116f32)), - ); - let u_prime = vdivq_f32(vmulq_n_f32(x, 4f32), den); - let v_prime = vdivq_f32(vmulq_n_f32(y, 9f32), den); - let sub_u_prime = vsubq_f32(u_prime, vdupq_n_f32(LUV_WHITE_U_PRIME)); - let sub_v_prime = vsubq_f32(v_prime, vdupq_n_f32(LUV_WHITE_V_PRIME)); - let l13 = vmulq_n_f32(l, 13f32); - let u = vbslq_f32(nan_mask, zeros, vmulq_f32(l13, sub_u_prime)); - let v = vbslq_f32(nan_mask, zeros, vmulq_f32(l13, sub_v_prime)); - (l, u, v) + unsafe { + let zeros = vdupq_n_f32(0f32); + let den = prefer_vfmaq_f32( + prefer_vfmaq_f32(x, z, vdupq_n_f32(3f32)), + y, + vdupq_n_f32(15f32), + ); + let nan_mask = vceqzq_f32(den); + let l_low_mask = vcltq_f32(y, vdupq_n_f32(LUV_CUTOFF_FORWARD_Y)); + let y_cbrt = vcbrtq_fast_f32(y); + let l = vbslq_f32( + l_low_mask, + vmulq_n_f32(y, LUV_MULTIPLIER_FORWARD_Y), + prefer_vfmaq_f32(vdupq_n_f32(-16f32), y_cbrt, vdupq_n_f32(116f32)), + ); + let u_prime = vdivq_f32(vmulq_n_f32(x, 4f32), den); + let v_prime = vdivq_f32(vmulq_n_f32(y, 9f32), den); + let sub_u_prime = vsubq_f32(u_prime, vdupq_n_f32(LUV_WHITE_U_PRIME)); + let sub_v_prime = vsubq_f32(v_prime, vdupq_n_f32(LUV_WHITE_V_PRIME)); + let l13 = vmulq_n_f32(l, 13f32); + let u = vbslq_f32(nan_mask, zeros, vmulq_f32(l13, sub_u_prime)); + let v = vbslq_f32(nan_mask, zeros, vmulq_f32(l13, sub_v_prime)); + (l, u, v) + } } -#[inline(always)] -pub(crate) unsafe fn neon_triple_to_lab( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_triple_to_lab( x: float32x4_t, y: float32x4_t, z: float32x4_t, ) -> (float32x4_t, float32x4_t, float32x4_t) { - let x = vmulq_n_f32(x, 100f32 / 95.047f32); - let z = vmulq_n_f32(z, 100f32 / 108.883f32); - let cbrt_x = vcbrtq_fast_f32(x); - let cbrt_y = vcbrtq_fast_f32(y); - let cbrt_z = vcbrtq_fast_f32(z); - let s_1 = vdupq_n_f32(16f32 / 116f32); - let s_2 = vdupq_n_f32(7.787f32); - let lower_x = prefer_vfmaq_f32(s_1, s_2, x); - let lower_y = prefer_vfmaq_f32(s_1, s_2, y); - let lower_z = prefer_vfmaq_f32(s_1, s_2, z); - let kappa = vdupq_n_f32(0.008856f32); - let x = vbslq_f32(vcgtq_f32(x, kappa), cbrt_x, lower_x); - let y = vbslq_f32(vcgtq_f32(y, kappa), cbrt_y, lower_y); - let z = vbslq_f32(vcgtq_f32(z, kappa), cbrt_z, lower_z); - let l = prefer_vfmaq_f32(vdupq_n_f32(-16.0f32), y, vdupq_n_f32(116.0f32)); - let a = vmulq_n_f32(vsubq_f32(x, y), 500f32); - let b = vmulq_n_f32(vsubq_f32(y, z), 200f32); - (l, a, b) + unsafe { + let x = vmulq_n_f32(x, 100f32 / 95.047f32); + let z = vmulq_n_f32(z, 100f32 / 108.883f32); + let cbrt_x = vcbrtq_fast_f32(x); + let cbrt_y = vcbrtq_fast_f32(y); + let cbrt_z = vcbrtq_fast_f32(z); + let s_1 = vdupq_n_f32(16f32 / 116f32); + let s_2 = vdupq_n_f32(7.787f32); + let lower_x = prefer_vfmaq_f32(s_1, s_2, x); + let lower_y = prefer_vfmaq_f32(s_1, s_2, y); + let lower_z = prefer_vfmaq_f32(s_1, s_2, z); + let kappa = vdupq_n_f32(0.008856f32); + let x = vbslq_f32(vcgtq_f32(x, kappa), cbrt_x, lower_x); + let y = vbslq_f32(vcgtq_f32(y, kappa), cbrt_y, lower_y); + let z = vbslq_f32(vcgtq_f32(z, kappa), cbrt_z, lower_z); + let l = prefer_vfmaq_f32(vdupq_n_f32(-16.0f32), y, vdupq_n_f32(116.0f32)); + let a = vmulq_n_f32(vsubq_f32(x, y), 500f32); + let b = vmulq_n_f32(vsubq_f32(y, z), 200f32); + (l, a, b) + } } -#[inline(always)] -pub(crate) unsafe fn neon_triple_to_lch( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_triple_to_lch( x: float32x4_t, y: float32x4_t, z: float32x4_t, ) -> (float32x4_t, float32x4_t, float32x4_t) { - let (luv_l, luv_u, luv_v) = neon_triple_to_luv(x, y, z); - let lch_c = vhypotq_fast_f32(luv_u, luv_v); - let lch_h = vatan2q_f32(luv_v, luv_u); - (luv_l, lch_c, lch_h) + unsafe { + let (luv_l, luv_u, luv_v) = neon_triple_to_luv(x, y, z); + let lch_c = vhypotq_fast_f32(luv_u, luv_v); + let lch_h = vatan2q_f32(luv_v, luv_u); + (luv_l, lch_c, lch_h) + } } -#[inline(always)] -pub(crate) unsafe fn neon_luv_to_xyz( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_luv_to_xyz( l: float32x4_t, u: float32x4_t, v: float32x4_t, @@ -140,8 +151,9 @@ pub(crate) unsafe fn neon_luv_to_xyz( (x, y, z) } -#[inline(always)] -pub(crate) unsafe fn neon_lab_to_xyz( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_lab_to_xyz( l: float32x4_t, a: float32x4_t, b: float32x4_t, @@ -166,13 +178,16 @@ pub(crate) unsafe fn neon_lab_to_xyz( (x, y, z) } -#[inline(always)] -pub(crate) unsafe fn neon_lch_to_xyz( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_lch_to_xyz( l: float32x4_t, c: float32x4_t, h: float32x4_t, ) -> (float32x4_t, float32x4_t, float32x4_t) { - let u = vmulq_f32(c, vcosq_f32(h)); - let v = vmulq_f32(c, vsinq_f32(h)); - neon_luv_to_xyz(l, u, v) + unsafe { + let u = vmulq_f32(c, vcosq_f32(h)); + let v = vmulq_f32(c, vsinq_f32(h)); + neon_luv_to_xyz(l, u, v) + } } diff --git a/src/neon/colors.rs b/src/neon/colors.rs index 4f643bc..0525b5f 100644 --- a/src/neon/colors.rs +++ b/src/neon/colors.rs @@ -9,8 +9,9 @@ use crate::neon::math::prefer_vfmaq_f32; use erydanos::vfmodq_f32; use std::arch::aarch64::*; -#[inline(always)] -pub unsafe fn neon_hsl_to_rgb( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_hsl_to_rgb( h: float32x4_t, s: float32x4_t, l: float32x4_t, @@ -23,16 +24,18 @@ pub unsafe fn neon_hsl_to_rgb( vsubq_f32(ones, vabsq_f32(vsubq_f32(vmulq_n_f32(l, 2f32), ones))), s, ); - let x = vmulq_f32( - vsubq_f32( - ones, - vabsq_f32(vsubq_f32( - vfmodq_f32(vmulq_n_f32(h, 1f32 / 60f32), vdupq_n_f32(2f32)), + let x = unsafe { + vmulq_f32( + vsubq_f32( ones, - )), - ), - c, - ); + vabsq_f32(vsubq_f32( + vfmodq_f32(vmulq_n_f32(h, 1f32 / 60f32), vdupq_n_f32(2f32)), + ones, + )), + ), + c, + ) + }; let zeros = vdupq_n_f32(0f32); let m = vsubq_f32(l, vmulq_n_f32(c, 0.5f32)); @@ -87,8 +90,9 @@ pub unsafe fn neon_hsl_to_rgb( (vcvtaq_u32_f32(r), vcvtaq_u32_f32(g), vcvtaq_u32_f32(b)) } -#[inline(always)] -pub unsafe fn neon_hsv_to_rgb( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_hsv_to_rgb( h: float32x4_t, s: float32x4_t, v: float32x4_t, @@ -97,15 +101,17 @@ pub unsafe fn neon_hsv_to_rgb( let s = vmulq_f32(s, scale); let v = vmulq_f32(v, scale); let c = vmulq_f32(s, v); - let h_prime = vfmodq_f32(vmulq_n_f32(h, 1f32 / 60f32), vdupq_n_f32(6f32)); + let h_prime = unsafe { vfmodq_f32(vmulq_n_f32(h, 1f32 / 60f32), vdupq_n_f32(6f32)) }; let ones = vdupq_n_f32(1f32); - let x = vmulq_f32( - vsubq_f32( - ones, - vabsq_f32(vsubq_f32(vfmodq_f32(h_prime, vdupq_n_f32(2f32)), ones)), - ), - c, - ); + let x = unsafe { + vmulq_f32( + vsubq_f32( + ones, + vabsq_f32(vsubq_f32(vfmodq_f32(h_prime, vdupq_n_f32(2f32)), ones)), + ), + c, + ) + }; let zeros = vdupq_n_f32(0f32); let m = vsubq_f32(v, c); let (mut r, mut g, mut b) = (zeros, zeros, zeros); @@ -158,8 +164,9 @@ pub unsafe fn neon_hsv_to_rgb( (vcvtaq_u32_f32(r), vcvtaq_u32_f32(g), vcvtaq_u32_f32(b)) } -#[inline(always)] -pub unsafe fn neon_rgb_to_hsv( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_rgb_to_hsv( r: uint32x4_t, g: uint32x4_t, b: uint32x4_t, @@ -178,14 +185,16 @@ pub unsafe fn neon_rgb_to_hsv( let is_b_max = vceqq_f32(c_max, b); let immediate_zero_flag = vceqzq_f32(delta); let mut h = vdupq_n_f32(0f32); - h = vbslq_f32( - is_r_max, - vmulq_n_f32( - vfmodq_f32(vmulq_f32(vsubq_f32(g, b), rcp_delta), vdupq_n_f32(6f32)), - 60f32, - ), - h, - ); + h = unsafe { + vbslq_f32( + is_r_max, + vmulq_n_f32( + vfmodq_f32(vmulq_f32(vsubq_f32(g, b), rcp_delta), vdupq_n_f32(6f32)), + 60f32, + ), + h, + ) + }; let adding_2 = vdupq_n_f32(2f32); h = vbslq_f32( is_g_max, @@ -216,8 +225,9 @@ pub unsafe fn neon_rgb_to_hsv( (h, vmulq_f32(s, scale), vmulq_f32(v, scale)) } -#[inline(always)] -pub unsafe fn neon_rgb_to_hsl( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_rgb_to_hsl( r: uint32x4_t, g: uint32x4_t, b: uint32x4_t, @@ -236,14 +246,16 @@ pub unsafe fn neon_rgb_to_hsl( let is_b_max = vceqq_f32(c_max, b); let immediate_zero_flag = vceqzq_f32(delta); let mut h = vdupq_n_f32(0f32); - h = vbslq_f32( - is_r_max, - vmulq_n_f32( - vfmodq_f32(vmulq_f32(vsubq_f32(g, b), rcp_delta), vdupq_n_f32(6f32)), - 60f32, - ), - h, - ); + h = unsafe { + vbslq_f32( + is_r_max, + vmulq_n_f32( + vfmodq_f32(vmulq_f32(vsubq_f32(g, b), rcp_delta), vdupq_n_f32(6f32)), + 60f32, + ), + h, + ) + }; let adding_2 = vdupq_n_f32(2f32); h = vbslq_f32( is_g_max, diff --git a/src/neon/from_sigmoidal.rs b/src/neon/from_sigmoidal.rs index 560a973..4f9059e 100644 --- a/src/neon/from_sigmoidal.rs +++ b/src/neon/from_sigmoidal.rs @@ -9,147 +9,150 @@ use crate::image::ImageConfiguration; use crate::neon::sigmoidal::neon_sigmoidal_to_rgb; use std::arch::aarch64::*; -#[inline(always)] -unsafe fn neon_sigmoidal_vld( +#[inline] +#[target_feature(enable = "neon")] +fn neon_sigmoidal_vld( src: *const f32, ) -> (uint32x4_t, uint32x4_t, uint32x4_t, uint32x4_t) { let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); if image_configuration.has_alpha() { - let sigmoidal_pixel = vld4q_f32(src); + let sigmoidal_pixel = unsafe { vld4q_f32(src) }; let (r0, g0, b0) = neon_sigmoidal_to_rgb(sigmoidal_pixel.0, sigmoidal_pixel.1, sigmoidal_pixel.2); let a0 = vmulq_n_f32(sigmoidal_pixel.3, 255f32); return (r0, g0, b0, vcvtaq_u32_f32(a0)); } - let sigmoidal_pixel = vld3q_f32(src); + let sigmoidal_pixel = unsafe { vld3q_f32(src) }; let (r0, g0, b0) = neon_sigmoidal_to_rgb(sigmoidal_pixel.0, sigmoidal_pixel.1, sigmoidal_pixel.2); (r0, g0, b0, vdupq_n_u32(0u32)) } -#[inline(always)] -pub unsafe fn neon_from_sigmoidal_row( +#[target_feature(enable = "neon")] +pub(crate) fn neon_from_sigmoidal_row( start_cx: usize, - src: *const f32, - dst: *mut u8, + src: &[f32], + dst: &mut [u8], width: u32, ) -> usize { - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + unsafe { + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.channel_count(); + let channels = image_configuration.channel_count(); - let mut cx = start_cx; + let mut cx = start_cx; - while cx + 16 < width as usize { - let offset_src_ptr = src.add(cx * channels); + while cx + 16 <= width as usize { + let offset_src_ptr = src.get_unchecked(cx * channels..).as_ptr(); - let src_ptr_0 = offset_src_ptr; + let src_ptr_0 = offset_src_ptr; - let (r_row0_, g_row0_, b_row0_, a_row0_) = - neon_sigmoidal_vld::(src_ptr_0); + let (r_row0_, g_row0_, b_row0_, a_row0_) = + neon_sigmoidal_vld::(src_ptr_0); - let src_ptr_1 = offset_src_ptr.add(4 * channels); + let src_ptr_1 = offset_src_ptr.add(4 * channels); - let (r_row1_, g_row1_, b_row1_, a_row1_) = - neon_sigmoidal_vld::(src_ptr_1); + let (r_row1_, g_row1_, b_row1_, a_row1_) = + neon_sigmoidal_vld::(src_ptr_1); - let src_ptr_2 = offset_src_ptr.add(4 * 2 * channels); + let src_ptr_2 = offset_src_ptr.add(4 * 2 * channels); - let (r_row2_, g_row2_, b_row2_, a_row2_) = - neon_sigmoidal_vld::(src_ptr_2); + let (r_row2_, g_row2_, b_row2_, a_row2_) = + neon_sigmoidal_vld::(src_ptr_2); - let src_ptr_3 = offset_src_ptr.add(4 * 3 * channels); + let src_ptr_3 = offset_src_ptr.add(4 * 3 * channels); - let (r_row3_, g_row3_, b_row3_, a_row3_) = - neon_sigmoidal_vld::(src_ptr_3); + let (r_row3_, g_row3_, b_row3_, a_row3_) = + neon_sigmoidal_vld::(src_ptr_3); - let r_row01 = vcombine_u16(vqmovn_u32(r_row0_), vqmovn_u32(r_row1_)); - let g_row01 = vcombine_u16(vqmovn_u32(g_row0_), vqmovn_u32(g_row1_)); - let b_row01 = vcombine_u16(vqmovn_u32(b_row0_), vqmovn_u32(b_row1_)); + let r_row01 = vcombine_u16(vqmovn_u32(r_row0_), vqmovn_u32(r_row1_)); + let g_row01 = vcombine_u16(vqmovn_u32(g_row0_), vqmovn_u32(g_row1_)); + let b_row01 = vcombine_u16(vqmovn_u32(b_row0_), vqmovn_u32(b_row1_)); - let r_row23 = vcombine_u16(vqmovn_u32(r_row2_), vqmovn_u32(r_row3_)); - let g_row23 = vcombine_u16(vqmovn_u32(g_row2_), vqmovn_u32(g_row3_)); - let b_row23 = vcombine_u16(vqmovn_u32(b_row2_), vqmovn_u32(b_row3_)); + let r_row23 = vcombine_u16(vqmovn_u32(r_row2_), vqmovn_u32(r_row3_)); + let g_row23 = vcombine_u16(vqmovn_u32(g_row2_), vqmovn_u32(g_row3_)); + let b_row23 = vcombine_u16(vqmovn_u32(b_row2_), vqmovn_u32(b_row3_)); - let r_row = vcombine_u8(vqmovn_u16(r_row01), vqmovn_u16(r_row23)); - let g_row = vcombine_u8(vqmovn_u16(g_row01), vqmovn_u16(g_row23)); - let b_row = vcombine_u8(vqmovn_u16(b_row01), vqmovn_u16(b_row23)); + let r_row = vcombine_u8(vqmovn_u16(r_row01), vqmovn_u16(r_row23)); + let g_row = vcombine_u8(vqmovn_u16(g_row01), vqmovn_u16(g_row23)); + let b_row = vcombine_u8(vqmovn_u16(b_row01), vqmovn_u16(b_row23)); - let dst_ptr = dst.add(cx * channels); + let dst_ptr = dst.get_unchecked_mut(cx * channels); - match image_configuration { - ImageConfiguration::Rgb => { - let rgb = uint8x16x3_t(r_row, g_row, b_row); - vst3q_u8(dst_ptr, rgb); - } - ImageConfiguration::Rgba | ImageConfiguration::Bgra => { - let a_row01 = vcombine_u16(vqmovn_u32(a_row0_), vqmovn_u32(a_row1_)); - let a_row23 = vcombine_u16(vqmovn_u32(a_row2_), vqmovn_u32(a_row3_)); - let a_row = vcombine_u8(vqmovn_u16(a_row01), vqmovn_u16(a_row23)); - if image_configuration == ImageConfiguration::Rgba { - let rgba = uint8x16x4_t(r_row, g_row, b_row, a_row); - vst4q_u8(dst_ptr, rgba); - } else { - let bgra = uint8x16x4_t(b_row, g_row, r_row, a_row); - vst4q_u8(dst_ptr, bgra); + match image_configuration { + ImageConfiguration::Rgb => { + let rgb = uint8x16x3_t(r_row, g_row, b_row); + vst3q_u8(dst_ptr, rgb); + } + ImageConfiguration::Rgba | ImageConfiguration::Bgra => { + let a_row01 = vcombine_u16(vqmovn_u32(a_row0_), vqmovn_u32(a_row1_)); + let a_row23 = vcombine_u16(vqmovn_u32(a_row2_), vqmovn_u32(a_row3_)); + let a_row = vcombine_u8(vqmovn_u16(a_row01), vqmovn_u16(a_row23)); + if image_configuration == ImageConfiguration::Rgba { + let rgba = uint8x16x4_t(r_row, g_row, b_row, a_row); + vst4q_u8(dst_ptr, rgba); + } else { + let bgra = uint8x16x4_t(b_row, g_row, r_row, a_row); + vst4q_u8(dst_ptr, bgra); + } + } + ImageConfiguration::Bgr => { + let bgr = uint8x16x3_t(b_row, g_row, r_row); + vst3q_u8(dst_ptr, bgr); } } - ImageConfiguration::Bgr => { - let bgr = uint8x16x3_t(b_row, g_row, r_row); - vst3q_u8(dst_ptr, bgr); - } - } - cx += 16; - } + cx += 16; + } - while cx + 8 < width as usize { - let offset_src_ptr = src.add(cx * channels); + while cx + 8 <= width as usize { + let offset_src_ptr = src.get_unchecked(cx * channels..).as_ptr(); - let src_ptr_0 = offset_src_ptr; + let src_ptr_0 = offset_src_ptr; - let (r_row0_, g_row0_, b_row0_, a_row0_) = - neon_sigmoidal_vld::(src_ptr_0); + let (r_row0_, g_row0_, b_row0_, a_row0_) = + neon_sigmoidal_vld::(src_ptr_0); - let src_ptr_1 = offset_src_ptr.add(4 * channels); + let src_ptr_1 = offset_src_ptr.add(4 * channels); - let (r_row1_, g_row1_, b_row1_, a_row1_) = - neon_sigmoidal_vld::(src_ptr_1); + let (r_row1_, g_row1_, b_row1_, a_row1_) = + neon_sigmoidal_vld::(src_ptr_1); - let r_row01 = vcombine_u16(vqmovn_u32(r_row0_), vqmovn_u32(r_row1_)); - let g_row01 = vcombine_u16(vqmovn_u32(g_row0_), vqmovn_u32(g_row1_)); - let b_row01 = vcombine_u16(vqmovn_u32(b_row0_), vqmovn_u32(b_row1_)); + let r_row01 = vcombine_u16(vqmovn_u32(r_row0_), vqmovn_u32(r_row1_)); + let g_row01 = vcombine_u16(vqmovn_u32(g_row0_), vqmovn_u32(g_row1_)); + let b_row01 = vcombine_u16(vqmovn_u32(b_row0_), vqmovn_u32(b_row1_)); - let r_row = vqmovn_u16(r_row01); - let g_row = vqmovn_u16(g_row01); - let b_row = vqmovn_u16(b_row01); + let r_row = vqmovn_u16(r_row01); + let g_row = vqmovn_u16(g_row01); + let b_row = vqmovn_u16(b_row01); - let dst_ptr = dst.add(cx * channels); + let dst_ptr = dst.get_unchecked_mut(cx * channels); - match image_configuration { - ImageConfiguration::Rgb => { - let rgb = uint8x8x3_t(r_row, g_row, b_row); - vst3_u8(dst_ptr, rgb); - } - ImageConfiguration::Rgba | ImageConfiguration::Bgra => { - let a_row01 = vcombine_u16(vqmovn_u32(a_row0_), vqmovn_u32(a_row1_)); - let a_row = vqmovn_u16(a_row01); - if image_configuration == ImageConfiguration::Rgba { - let rgba = uint8x8x4_t(r_row, g_row, b_row, a_row); - vst4_u8(dst_ptr, rgba); - } else { - let bgra = uint8x8x4_t(b_row, g_row, r_row, a_row); - vst4_u8(dst_ptr, bgra); + match image_configuration { + ImageConfiguration::Rgb => { + let rgb = uint8x8x3_t(r_row, g_row, b_row); + vst3_u8(dst_ptr, rgb); + } + ImageConfiguration::Rgba | ImageConfiguration::Bgra => { + let a_row01 = vcombine_u16(vqmovn_u32(a_row0_), vqmovn_u32(a_row1_)); + let a_row = vqmovn_u16(a_row01); + if image_configuration == ImageConfiguration::Rgba { + let rgba = uint8x8x4_t(r_row, g_row, b_row, a_row); + vst4_u8(dst_ptr, rgba); + } else { + let bgra = uint8x8x4_t(b_row, g_row, r_row, a_row); + vst4_u8(dst_ptr, bgra); + } + } + ImageConfiguration::Bgr => { + let bgr = uint8x8x3_t(b_row, g_row, r_row); + vst3_u8(dst_ptr, bgr); } } - ImageConfiguration::Bgr => { - let bgr = uint8x8x3_t(b_row, g_row, r_row); - vst3_u8(dst_ptr, bgr); - } + + cx += 8; } - cx += 8; + cx } - - cx } diff --git a/src/neon/gamma_curves.rs b/src/neon/gamma_curves.rs deleted file mode 100644 index c530a23..0000000 --- a/src/neon/gamma_curves.rs +++ /dev/null @@ -1,132 +0,0 @@ -/* - * // Copyright 2024 (c) the Radzivon Bartoshyk. All rights reserved. - * // - * // Use of this source code is governed by a BSD-style - * // license that can be found in the LICENSE file. - */ -#![allow(dead_code)] -use crate::neon::math::vpowq_n_f32; -use std::arch::aarch64::*; - -#[inline(always)] -pub unsafe fn neon_srgb_from_linear(linear: float32x4_t) -> float32x4_t { - let linear = vmaxq_f32(linear, vdupq_n_f32(0f32)); - let linear = vminq_f32(linear, vdupq_n_f32(1f32)); - let low_cut_off = vdupq_n_f32(0.0030412825601275209f32); - let mask = vcgeq_f32(linear, low_cut_off); - - let mut low = linear; - let mut high = linear; - low = vmulq_n_f32(low, 12.92f32); - - high = vsubq_f32( - vmulq_n_f32(vpowq_n_f32(high, 1.0f32 / 2.4f32), 1.0550107189475866f32), - vdupq_n_f32(0.0550107189475866f32), - ); - vbslq_f32(mask, high, low) -} - -#[inline(always)] -pub unsafe fn neon_srgb_to_linear(gamma: float32x4_t) -> float32x4_t { - let gamma = vmaxq_f32(gamma, vdupq_n_f32(0f32)); - let gamma = vminq_f32(gamma, vdupq_n_f32(1f32)); - let low_cut_off = vdupq_n_f32(12.92f32 * 0.0030412825601275209f32); - let mask = vcgeq_f32(gamma, low_cut_off); - - let mut low = gamma; - let high = vpowq_n_f32( - vmulq_n_f32( - vaddq_f32(gamma, vdupq_n_f32(0.0550107189475866f32)), - 1f32 / 1.0550107189475866f32, - ), - 2.4f32, - ); - low = vmulq_n_f32(low, 1f32 / 12.92f32); - vbslq_f32(mask, high, low) -} - -#[inline(always)] -pub unsafe fn neon_rec709_from_linear(linear: float32x4_t) -> float32x4_t { - let linear = vmaxq_f32(linear, vdupq_n_f32(0f32)); - let linear = vminq_f32(linear, vdupq_n_f32(1f32)); - let low_cut_off = vdupq_n_f32(0.018053968510807f32); - let mask = vcgeq_f32(linear, low_cut_off); - - let mut low = linear; - let mut high = linear; - low = vmulq_n_f32(low, 4.5f32); - - high = vsubq_f32( - vmulq_n_f32(vpowq_n_f32(high, 0.45f32), 1.09929682680944f32), - vdupq_n_f32(0.09929682680944f32), - ); - vbslq_f32(mask, high, low) -} - -#[inline(always)] -pub unsafe fn neon_rec709_to_linear(gamma: float32x4_t) -> float32x4_t { - let gamma = vmaxq_f32(gamma, vdupq_n_f32(0f32)); - let gamma = vminq_f32(gamma, vdupq_n_f32(1f32)); - let low_cut_off = vdupq_n_f32(4.5f32 * 0.018053968510807f32); - let mask = vcgeq_f32(gamma, low_cut_off); - - let mut low = gamma; - let high = vpowq_n_f32( - vmulq_n_f32( - vaddq_f32(gamma, vdupq_n_f32(0.09929682680944f32)), - 1f32 / 1.09929682680944f32, - ), - 1.0f32 / 0.45f32, - ); - low = vmulq_n_f32(low, 1f32 / 4.5f32); - vbslq_f32(mask, high, low) -} - -#[inline(always)] -pub unsafe fn neon_pure_gamma_function(gamma: float32x4_t, gamma_constant: f32) -> float32x4_t { - let zero_mask = vclezq_f32(gamma); - let ones = vdupq_n_f32(1f32); - let zeros = vdupq_n_f32(0f32); - let ones_mask = vcgeq_f32(gamma, ones); - let mut rs = vpowq_n_f32(gamma, gamma_constant); - rs = vbslq_f32(zero_mask, zeros, rs); - vbslq_f32(ones_mask, ones, rs) -} - -#[inline(always)] -pub unsafe fn neon_smpte428_to_linear(gamma: float32x4_t) -> float32x4_t { - const SCALE: f32 = 1. / 0.91655527974030934f32; - vmulq_n_f32( - vpowq_n_f32(vmaxq_f32(gamma, vdupq_n_f32(0.)), 2.6f32), - SCALE, - ) -} - -#[inline(always)] -pub unsafe fn neon_smpte428_from_linear(linear: float32x4_t) -> float32x4_t { - const POWER_VALUE: f32 = 1.0f32 / 2.6f32; - vpowq_n_f32( - vmulq_n_f32(vmaxq_f32(linear, vdupq_n_f32(0.)), 0.91655527974030934f32), - POWER_VALUE, - ) -} - -#[inline(always)] -pub unsafe fn neon_gamma2p2_to_linear(gamma: float32x4_t) -> float32x4_t { - neon_pure_gamma_function(gamma, 2.2f32) -} - -#[inline(always)] -pub unsafe fn neon_gamma2p8_to_linear(gamma: float32x4_t) -> float32x4_t { - neon_pure_gamma_function(gamma, 2.8f32) -} - -#[inline(always)] -pub unsafe fn neon_gamma2p2_from_linear(linear: float32x4_t) -> float32x4_t { - neon_pure_gamma_function(linear, 1f32 / 2.2f32) -} - -#[inline(always)] -pub unsafe fn neon_gamma2p8_from_linear(linear: float32x4_t) -> float32x4_t { - neon_pure_gamma_function(linear, 1f32 / 2.8f32) -} diff --git a/src/neon/image_to_jzazbz.rs b/src/neon/image_to_jzazbz.rs index 311c3f0..64ce5c8 100644 --- a/src/neon/image_to_jzazbz.rs +++ b/src/neon/image_to_jzazbz.rs @@ -7,7 +7,7 @@ use crate::image::ImageConfiguration; use crate::image_to_jzazbz::JzazbzTarget; use crate::neon::math::{vcolorq_matrix_f32, vpowq_n_f32}; -use crate::{load_f32_and_deinterleave, SRGB_TO_XYZ_D65}; +use crate::{SRGB_TO_XYZ_D65, load_f32_and_deinterleave}; use erydanos::{vatan2q_f32, vhypotq_fast_f32, visnanq_f32, vmlafq_f32, vpowq_f32}; use std::arch::aarch64::*; @@ -97,8 +97,8 @@ macro_rules! triple_to_jzazbz { }}; } -#[inline(always)] -pub(crate) unsafe fn neon_image_to_jzazbz( +#[target_feature(enable = "neon")] +pub(crate) fn neon_image_to_jzazbz( start_cx: usize, src: &[f32], dst: &mut [f32], @@ -111,23 +111,25 @@ pub(crate) unsafe fn neon_image_to_jzazbz( +#[target_feature(enable = "neon")] +pub(crate) fn neon_image_to_oklab( start_cx: usize, width: u32, dst: &mut [f32], ) -> usize { - let target: OklabTarget = TARGET.into(); - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.channel_count(); - let mut cx = start_cx; + unsafe { + let target: OklabTarget = TARGET.into(); + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + let channels = image_configuration.channel_count(); + let mut cx = start_cx; - let (c0, c1, c2, c3, c4, c5, c6, c7, c8) = ( - vdupq_n_f32(0.4122214708f32), - vdupq_n_f32(0.5363325363f32), - vdupq_n_f32(0.0514459929f32), - vdupq_n_f32(0.2119034982f32), - vdupq_n_f32(0.6806995451f32), - vdupq_n_f32(0.1073969566f32), - vdupq_n_f32(0.0883024619f32), - vdupq_n_f32(0.2817188376f32), - vdupq_n_f32(0.6299787005f32), - ); + let (c0, c1, c2, c3, c4, c5, c6, c7, c8) = ( + vdupq_n_f32(0.4122214708f32), + vdupq_n_f32(0.5363325363f32), + vdupq_n_f32(0.0514459929f32), + vdupq_n_f32(0.2119034982f32), + vdupq_n_f32(0.6806995451f32), + vdupq_n_f32(0.1073969566f32), + vdupq_n_f32(0.0883024619f32), + vdupq_n_f32(0.2817188376f32), + vdupq_n_f32(0.6299787005f32), + ); - let (m0, m1, m2, m3, m4, m5, m6, m7, m8) = ( - vdupq_n_f32(0.2104542553f32), - vdupq_n_f32(0.7936177850f32), - vdupq_n_f32(-0.0040720468f32), - vdupq_n_f32(1.9779984951f32), - vdupq_n_f32(-2.4285922050f32), - vdupq_n_f32(0.4505937099f32), - vdupq_n_f32(0.0259040371f32), - vdupq_n_f32(0.7827717662f32), - vdupq_n_f32(-0.8086757660f32), - ); + let (m0, m1, m2, m3, m4, m5, m6, m7, m8) = ( + vdupq_n_f32(0.2104542553f32), + vdupq_n_f32(0.7936177850f32), + vdupq_n_f32(-0.0040720468f32), + vdupq_n_f32(1.9779984951f32), + vdupq_n_f32(-2.4285922050f32), + vdupq_n_f32(0.4505937099f32), + vdupq_n_f32(0.0259040371f32), + vdupq_n_f32(0.7827717662f32), + vdupq_n_f32(-0.8086757660f32), + ); - while cx + 4 <= width as usize { - let in_place_ptr = dst.get_unchecked_mut(cx * channels..); - let (r_chan, g_chan, b_chan, a_chan) = - load_f32_and_deinterleave!(in_place_ptr.as_ptr(), image_configuration); + while cx + 4 <= width as usize { + let in_place_ptr = dst.get_unchecked_mut(cx * channels..); + let (r_chan, g_chan, b_chan, a_chan) = + load_f32_and_deinterleave!(in_place_ptr.as_ptr(), image_configuration); - let (x_low_low, y_low_low, z_low_low) = triple_to_oklab!( - r_chan, - g_chan, - b_chan, - transfer_function, - target, - c0, - c1, - c2, - c3, - c4, - c5, - c6, - c7, - c8, - m0, - m1, - m2, - m3, - m4, - m5, - m6, - m7, - m8 - ); + let (x_low_low, y_low_low, z_low_low) = triple_to_oklab!( + r_chan, + g_chan, + b_chan, + transfer_function, + target, + c0, + c1, + c2, + c3, + c4, + c5, + c6, + c7, + c8, + m0, + m1, + m2, + m3, + m4, + m5, + m6, + m7, + m8 + ); - if image_configuration.has_alpha() { - let xyz_low_low = float32x4x4_t(x_low_low, y_low_low, z_low_low, a_chan); - vst4q_f32(in_place_ptr.as_mut_ptr(), xyz_low_low); - } else { - let xyz_low_low = float32x4x3_t(x_low_low, y_low_low, z_low_low); - vst3q_f32(in_place_ptr.as_mut_ptr(), xyz_low_low); + if image_configuration.has_alpha() { + let xyz_low_low = float32x4x4_t(x_low_low, y_low_low, z_low_low, a_chan); + vst4q_f32(in_place_ptr.as_mut_ptr(), xyz_low_low); + } else { + let xyz_low_low = float32x4x3_t(x_low_low, y_low_low, z_low_low); + vst3q_f32(in_place_ptr.as_mut_ptr(), xyz_low_low); + } + + cx += 4; } - cx += 4; + cx } - - cx } diff --git a/src/neon/jzazbz_to_image.rs b/src/neon/jzazbz_to_image.rs index 9233b5d..18e5f17 100644 --- a/src/neon/jzazbz_to_image.rs +++ b/src/neon/jzazbz_to_image.rs @@ -12,7 +12,7 @@ use erydanos::{vcosq_f32, visnanq_f32, vmlafq_f32, vpowq_f32, vsinq_f32}; use crate::image::ImageConfiguration; use crate::image_to_jzazbz::JzazbzTarget; use crate::neon::math::{vcolorq_matrix_f32, vpowq_n_f32}; -use crate::{load_f32_and_deinterleave_direct, XYZ_TO_SRGB_D65}; +use crate::{XYZ_TO_SRGB_D65, load_f32_and_deinterleave_direct}; macro_rules! perceptual_quantizer_inverse { ($color: expr) => {{ @@ -33,89 +33,92 @@ macro_rules! perceptual_quantizer_inverse { }}; } -#[inline(always)] -unsafe fn neon_jzazbz_gamma_vld( +#[inline] +#[target_feature(enable = "neon")] +fn neon_jzazbz_gamma_vld( src: *const f32, target: JzazbzTarget, luminance: f32, ) -> (float32x4_t, float32x4_t, float32x4_t, float32x4_t) { - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let (jz, mut az, mut bz, a_f32) = load_f32_and_deinterleave_direct!(src, image_configuration); + unsafe { + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + let (jz, mut az, mut bz, a_f32) = + load_f32_and_deinterleave_direct!(src, image_configuration); + + if target == JzazbzTarget::Jzczhz { + let cz = az; + let hz = bz; + az = vmulq_f32(cz, vcosq_f32(hz)); + bz = vmulq_f32(cz, vsinq_f32(hz)); + } - if target == JzazbzTarget::Jzczhz { - let cz = az; - let hz = bz; - az = vmulq_f32(cz, vcosq_f32(hz)); - bz = vmulq_f32(cz, vsinq_f32(hz)); - } + let jz = vaddq_f32(jz, vdupq_n_f32(1.6295499532821566e-11)); + let iz = vdivq_f32( + jz, + vmlafq_f32(jz, vdupq_n_f32(0.56f32), vdupq_n_f32(0.44f32)), + ); - let jz = vaddq_f32(jz, vdupq_n_f32(1.6295499532821566e-11)); - let iz = vdivq_f32( - jz, - vmlafq_f32(jz, vdupq_n_f32(0.56f32), vdupq_n_f32(0.44f32)), - ); - - let (m0, m1, m2, m3, m4, m5, m6, m7, m8) = ( - vdupq_n_f32(1f32), - vdupq_n_f32(1.386050432715393e-1), - vdupq_n_f32(5.804731615611869e-2), - vdupq_n_f32(1f32), - vdupq_n_f32(-1.386050432715393e-1), - vdupq_n_f32(-5.804731615611891e-2), - vdupq_n_f32(1f32), - vdupq_n_f32(-9.601924202631895e-2), - vdupq_n_f32(-8.118918960560390e-1), - ); - - let (mut l_l, mut l_m, mut l_s) = - vcolorq_matrix_f32(iz, az, bz, m0, m1, m2, m3, m4, m5, m6, m7, m8); - - l_l = perceptual_quantizer_inverse!(l_l); - l_m = perceptual_quantizer_inverse!(l_m); - l_s = perceptual_quantizer_inverse!(l_s); - - let (c0, c1, c2, c3, c4, c5, c6, c7, c8) = ( - vdupq_n_f32(1.661373055774069e+00), - vdupq_n_f32(-9.145230923250668e-01), - vdupq_n_f32(2.313620767186147e-01), - vdupq_n_f32(-3.250758740427037e-01), - vdupq_n_f32(1.571847038366936e+00), - vdupq_n_f32(-2.182538318672940e-01), - vdupq_n_f32(-9.098281098284756e-02), - vdupq_n_f32(-3.127282905230740e-01), - vdupq_n_f32(1.522766561305260e+00), - ); - - let (mut x, mut y, mut z) = - vcolorq_matrix_f32(l_l, l_m, l_s, c0, c1, c2, c3, c4, c5, c6, c7, c8); - - x = vmulq_n_f32(x, luminance); - y = vmulq_n_f32(y, luminance); - z = vmulq_n_f32(z, luminance); - - let (x0, x1, x2, x3, x4, x5, x6, x7, x8) = ( - vdupq_n_f32(*XYZ_TO_SRGB_D65.get_unchecked(0).get_unchecked(0)), - vdupq_n_f32(*XYZ_TO_SRGB_D65.get_unchecked(0).get_unchecked(1)), - vdupq_n_f32(*XYZ_TO_SRGB_D65.get_unchecked(0).get_unchecked(2)), - vdupq_n_f32(*XYZ_TO_SRGB_D65.get_unchecked(1).get_unchecked(0)), - vdupq_n_f32(*XYZ_TO_SRGB_D65.get_unchecked(1).get_unchecked(1)), - vdupq_n_f32(*XYZ_TO_SRGB_D65.get_unchecked(1).get_unchecked(2)), - vdupq_n_f32(*XYZ_TO_SRGB_D65.get_unchecked(2).get_unchecked(0)), - vdupq_n_f32(*XYZ_TO_SRGB_D65.get_unchecked(2).get_unchecked(1)), - vdupq_n_f32(*XYZ_TO_SRGB_D65.get_unchecked(2).get_unchecked(2)), - ); - - let (r_l, g_l, b_l) = vcolorq_matrix_f32(x, y, z, x0, x1, x2, x3, x4, x5, x6, x7, x8); - - (r_l, g_l, b_l, a_f32) + let (m0, m1, m2, m3, m4, m5, m6, m7, m8) = ( + vdupq_n_f32(1f32), + vdupq_n_f32(1.386050432715393e-1), + vdupq_n_f32(5.804731615611869e-2), + vdupq_n_f32(1f32), + vdupq_n_f32(-1.386050432715393e-1), + vdupq_n_f32(-5.804731615611891e-2), + vdupq_n_f32(1f32), + vdupq_n_f32(-9.601924202631895e-2), + vdupq_n_f32(-8.118918960560390e-1), + ); + + let (mut l_l, mut l_m, mut l_s) = + vcolorq_matrix_f32(iz, az, bz, m0, m1, m2, m3, m4, m5, m6, m7, m8); + + l_l = perceptual_quantizer_inverse!(l_l); + l_m = perceptual_quantizer_inverse!(l_m); + l_s = perceptual_quantizer_inverse!(l_s); + + let (c0, c1, c2, c3, c4, c5, c6, c7, c8) = ( + vdupq_n_f32(1.661373055774069e+00), + vdupq_n_f32(-9.145230923250668e-01), + vdupq_n_f32(2.313620767186147e-01), + vdupq_n_f32(-3.250758740427037e-01), + vdupq_n_f32(1.571847038366936e+00), + vdupq_n_f32(-2.182538318672940e-01), + vdupq_n_f32(-9.098281098284756e-02), + vdupq_n_f32(-3.127282905230740e-01), + vdupq_n_f32(1.522766561305260e+00), + ); + + let (mut x, mut y, mut z) = + vcolorq_matrix_f32(l_l, l_m, l_s, c0, c1, c2, c3, c4, c5, c6, c7, c8); + + x = vmulq_n_f32(x, luminance); + y = vmulq_n_f32(y, luminance); + z = vmulq_n_f32(z, luminance); + + let (x0, x1, x2, x3, x4, x5, x6, x7, x8) = ( + vdupq_n_f32(XYZ_TO_SRGB_D65[0][0]), + vdupq_n_f32(XYZ_TO_SRGB_D65[0][1]), + vdupq_n_f32(XYZ_TO_SRGB_D65[0][2]), + vdupq_n_f32(XYZ_TO_SRGB_D65[1][0]), + vdupq_n_f32(XYZ_TO_SRGB_D65[1][1]), + vdupq_n_f32(XYZ_TO_SRGB_D65[1][2]), + vdupq_n_f32(XYZ_TO_SRGB_D65[2][0]), + vdupq_n_f32(XYZ_TO_SRGB_D65[2][1]), + vdupq_n_f32(XYZ_TO_SRGB_D65[2][2]), + ); + + let (r_l, g_l, b_l) = vcolorq_matrix_f32(x, y, z, x0, x1, x2, x3, x4, x5, x6, x7, x8); + + (r_l, g_l, b_l, a_f32) + } } -pub unsafe fn neon_jzazbz_to_image( +#[target_feature(enable = "neon")] +pub(crate) fn neon_jzazbz_to_image( start_cx: usize, - src: *const f32, - src_offset: u32, - dst: *mut f32, - dst_offset: u32, + src: &[f32], + dst: &mut [f32], width: u32, display_luminance: f32, ) -> usize { @@ -126,16 +129,15 @@ pub unsafe fn neon_jzazbz_to_image(src_ptr_0, target, luminance_scale); - let dst_ptr = ((dst as *mut u8).add(dst_offset as usize) as *mut f32).add(cx * channels); + let dst_ptr = unsafe { dst.get_unchecked_mut(cx * channels) }; if image_configuration.has_alpha() { let store_rows = match image_configuration { @@ -146,7 +148,9 @@ pub unsafe fn neon_jzazbz_to_image { @@ -156,7 +160,9 @@ pub unsafe fn neon_jzazbz_to_image float32x4_t { +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn prefer_vfmaq_f32(a: float32x4_t, b: float32x4_t, c: float32x4_t) -> float32x4_t { #[cfg(target_arch = "aarch64")] { vfmaq_f32(a, b, c) @@ -24,18 +21,21 @@ pub(crate) unsafe fn prefer_vfmaq_f32( } } -#[inline(always)] -pub unsafe fn vpowjq_f32(val: float32x4_t, n: float32x4_t) -> float32x4_t { - vpowq_fast_f32(val, n) +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn vpowjq_f32(val: float32x4_t, n: float32x4_t) -> float32x4_t { + unsafe { vpowq_fast_f32(val, n) } } -#[inline(always)] -pub unsafe fn vpowq_n_f32(t: float32x4_t, power: f32) -> float32x4_t { +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn vpowq_n_f32(t: float32x4_t, power: f32) -> float32x4_t { vpowjq_f32(t, vdupq_n_f32(power)) } -#[inline(always)] -pub unsafe fn vcolorq_matrix_f32( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn vcolorq_matrix_f32( r: float32x4_t, g: float32x4_t, b: float32x4_t, @@ -54,7 +54,8 @@ pub unsafe fn vcolorq_matrix_f32( let new_b = prefer_vfmaq_f32(prefer_vfmaq_f32(vmulq_f32(g, c8), b, c9), r, c7); (new_r, new_g, new_b) } -#[inline(always)] -pub(crate) unsafe fn vcubeq_f32(x: float32x4_t) -> float32x4_t { +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn vcubeq_f32(x: float32x4_t) -> float32x4_t { vmulq_f32(vmulq_f32(x, x), x) } diff --git a/src/neon/mod.rs b/src/neon/mod.rs index 424b552..5922083 100644 --- a/src/neon/mod.rs +++ b/src/neon/mod.rs @@ -8,7 +8,6 @@ mod cie; mod colors; mod from_sigmoidal; -mod gamma_curves; mod hsv_to_image; mod image_to_hsv; mod image_to_jzazbz; @@ -24,16 +23,16 @@ mod to_xyza_laba; mod xyz_lab_to_image; mod xyza_laba_to_image; -pub use colors::*; -pub use from_sigmoidal::neon_from_sigmoidal_row; +pub(crate) use colors::*; +pub(crate) use from_sigmoidal::neon_from_sigmoidal_row; pub use hsv_to_image::*; pub(crate) use image_to_hsv::neon_channels_to_hsv_u16; pub(crate) use image_to_jzazbz::neon_image_to_jzazbz; -pub use image_to_oklab::neon_image_to_oklab; -pub use jzazbz_to_image::neon_jzazbz_to_image; +pub(crate) use image_to_oklab::neon_image_to_oklab; +pub(crate) use jzazbz_to_image::neon_jzazbz_to_image; pub(crate) use oklab_to_image::neon_oklab_to_image; pub(crate) use to_sigmoidal::neon_image_to_sigmoidal; pub(crate) use to_xyz_lab::neon_channels_to_xyz_or_lab; -pub use to_xyza_laba::*; -pub use xyz_lab_to_image::*; -pub use xyza_laba_to_image::*; +pub(crate) use to_xyza_laba::neon_channels_to_xyza_or_laba; +pub(crate) use xyz_lab_to_image::neon_xyz_to_channels; +pub(crate) use xyza_laba_to_image::neon_xyza_to_image; diff --git a/src/neon/oklab_to_image.rs b/src/neon/oklab_to_image.rs index 88359d5..a9b0c48 100644 --- a/src/neon/oklab_to_image.rs +++ b/src/neon/oklab_to_image.rs @@ -13,8 +13,9 @@ use crate::image_to_oklab::OklabTarget; use crate::load_f32_and_deinterleave_direct; use crate::neon::math::vcolorq_matrix_f32; -#[inline(always)] -unsafe fn neon_oklab_gamma_vld( +#[inline] +#[target_feature(enable = "neon")] +fn neon_oklab_gamma_vld( src: *const f32, m0: float32x4_t, m1: float32x4_t, @@ -35,29 +36,31 @@ unsafe fn neon_oklab_gamma_vld (float32x4_t, float32x4_t, float32x4_t, float32x4_t) { - let target: OklabTarget = TARGET.into(); - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let (l, mut a, mut b, a_f32) = load_f32_and_deinterleave_direct!(src, image_configuration); + unsafe { + let target: OklabTarget = TARGET.into(); + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + let (l, mut a, mut b, a_f32) = load_f32_and_deinterleave_direct!(src, image_configuration); - if target == OklabTarget::Oklch { - let a0 = vmulq_f32(a, vcosq_f32(b)); - let b0 = vmulq_f32(a, vsinq_f32(b)); - a = a0; - b = b0; - } + if target == OklabTarget::Oklch { + let a0 = vmulq_f32(a, vcosq_f32(b)); + let b0 = vmulq_f32(a, vsinq_f32(b)); + a = a0; + b = b0; + } - let (mut l_l, mut l_m, mut l_s) = - vcolorq_matrix_f32(l, a, b, m0, m1, m2, m3, m4, m5, m6, m7, m8); + let (mut l_l, mut l_m, mut l_s) = + vcolorq_matrix_f32(l, a, b, m0, m1, m2, m3, m4, m5, m6, m7, m8); - l_l = vmulq_f32(vmulq_f32(l_l, l_l), l_l); - l_m = vmulq_f32(vmulq_f32(l_m, l_m), l_m); - l_s = vmulq_f32(vmulq_f32(l_s, l_s), l_s); + l_l = vmulq_f32(vmulq_f32(l_l, l_l), l_l); + l_m = vmulq_f32(vmulq_f32(l_m, l_m), l_m); + l_s = vmulq_f32(vmulq_f32(l_s, l_s), l_s); - let (r_l, g_l, b_l) = vcolorq_matrix_f32(l_l, l_m, l_s, c0, c1, c2, c3, c4, c5, c6, c7, c8); - (r_l, g_l, b_l, a_f32) + let (r_l, g_l, b_l) = vcolorq_matrix_f32(l_l, l_m, l_s, c0, c1, c2, c3, c4, c5, c6, c7, c8); + (r_l, g_l, b_l, a_f32) + } } -#[inline(always)] +#[target_feature(enable = "neon")] pub(crate) unsafe fn neon_oklab_to_image( start_cx: usize, src: &[f32], @@ -93,34 +96,36 @@ pub(crate) unsafe fn neon_oklab_to_image( - src_ptr, m0, m1, m2, m3, m4, m5, m6, m7, m8, c0, c1, c2, c3, c4, c5, c6, c7, c8, - ); + let (r_row0_, g_row0_, b_row0_, a_row0_) = + neon_oklab_gamma_vld::( + src_ptr, m0, m1, m2, m3, m4, m5, m6, m7, m8, c0, c1, c2, c3, c4, c5, c6, c7, c8, + ); - if image_configuration.has_alpha() { - let store_rows = match image_configuration { - ImageConfiguration::Rgb | ImageConfiguration::Rgba => { - float32x4x4_t(r_row0_, g_row0_, b_row0_, a_row0_) - } - ImageConfiguration::Bgra | ImageConfiguration::Bgr => { - float32x4x4_t(b_row0_, g_row0_, r_row0_, a_row0_) - } - }; - vst4q_f32(dst_ptr, store_rows); - } else { - let store_rows = match image_configuration { - ImageConfiguration::Rgb | ImageConfiguration::Rgba => { - float32x4x3_t(r_row0_, g_row0_, b_row0_) - } - ImageConfiguration::Bgra | ImageConfiguration::Bgr => { - float32x4x3_t(b_row0_, g_row0_, r_row0_) - } - }; - vst3q_f32(dst_ptr, store_rows); + if image_configuration.has_alpha() { + let store_rows = match image_configuration { + ImageConfiguration::Rgb | ImageConfiguration::Rgba => { + float32x4x4_t(r_row0_, g_row0_, b_row0_, a_row0_) + } + ImageConfiguration::Bgra | ImageConfiguration::Bgr => { + float32x4x4_t(b_row0_, g_row0_, r_row0_, a_row0_) + } + }; + vst4q_f32(dst_ptr, store_rows); + } else { + let store_rows = match image_configuration { + ImageConfiguration::Rgb | ImageConfiguration::Rgba => { + float32x4x3_t(r_row0_, g_row0_, b_row0_) + } + ImageConfiguration::Bgra | ImageConfiguration::Bgr => { + float32x4x3_t(b_row0_, g_row0_, r_row0_) + } + }; + vst3q_f32(dst_ptr, store_rows); + } } cx += 4; diff --git a/src/neon/to_xyz_lab.rs b/src/neon/to_xyz_lab.rs index 6b70091..31a6d3c 100644 --- a/src/neon/to_xyz_lab.rs +++ b/src/neon/to_xyz_lab.rs @@ -13,7 +13,7 @@ use crate::neon::cie::{ use crate::xyz_target::XyzTarget; use std::arch::aarch64::*; -#[inline(always)] +#[target_feature(enable = "neon")] pub(crate) unsafe fn neon_channels_to_xyz_or_lab< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, @@ -34,52 +34,54 @@ pub(crate) unsafe fn neon_channels_to_xyz_or_lab< let channels = image_configuration.channel_count(); let mut cx = start_cx; - let cq1 = vdupq_n_f32(*matrix.get_unchecked(0).get_unchecked(0)); - let cq2 = vdupq_n_f32(*matrix.get_unchecked(0).get_unchecked(1)); - let cq3 = vdupq_n_f32(*matrix.get_unchecked(0).get_unchecked(2)); - let cq4 = vdupq_n_f32(*matrix.get_unchecked(1).get_unchecked(0)); - let cq5 = vdupq_n_f32(*matrix.get_unchecked(1).get_unchecked(1)); - let cq6 = vdupq_n_f32(*matrix.get_unchecked(1).get_unchecked(2)); - let cq7 = vdupq_n_f32(*matrix.get_unchecked(2).get_unchecked(0)); - let cq8 = vdupq_n_f32(*matrix.get_unchecked(2).get_unchecked(1)); - let cq9 = vdupq_n_f32(*matrix.get_unchecked(2).get_unchecked(2)); + let cq1 = vdupq_n_f32(matrix[0][0]); + let cq2 = vdupq_n_f32(matrix[0][1]); + let cq3 = vdupq_n_f32(matrix[0][2]); + let cq4 = vdupq_n_f32(matrix[1][0]); + let cq5 = vdupq_n_f32(matrix[1][1]); + let cq6 = vdupq_n_f32(matrix[1][2]); + let cq7 = vdupq_n_f32(matrix[2][0]); + let cq8 = vdupq_n_f32(matrix[2][1]); + let cq9 = vdupq_n_f32(matrix[2][2]); while cx + 4 <= width as usize { - let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); - let (r_chan, g_chan, b_chan, a_chan) = - load_f32_and_deinterleave!(src_ptr, image_configuration); + unsafe { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); + let (r_chan, g_chan, b_chan, a_chan) = + load_f32_and_deinterleave!(src_ptr, image_configuration); - let (mut x_low_low, mut y_low_low, mut z_low_low) = neon_triple_to_xyz( - r_chan, g_chan, b_chan, cq1, cq2, cq3, cq4, cq5, cq6, cq7, cq8, cq9, - ); + let (mut x_low_low, mut y_low_low, mut z_low_low) = neon_triple_to_xyz( + r_chan, g_chan, b_chan, cq1, cq2, cq3, cq4, cq5, cq6, cq7, cq8, cq9, + ); - match target { - XyzTarget::Lab => { - let (l, a, b) = neon_triple_to_lab(x_low_low, y_low_low, z_low_low); - x_low_low = l; - y_low_low = a; - z_low_low = b; + match target { + XyzTarget::Lab => { + let (l, a, b) = neon_triple_to_lab(x_low_low, y_low_low, z_low_low); + x_low_low = l; + y_low_low = a; + z_low_low = b; + } + XyzTarget::Xyz => {} + XyzTarget::Luv => { + let (l, u, v) = neon_triple_to_luv(x_low_low, y_low_low, z_low_low); + x_low_low = l; + y_low_low = u; + z_low_low = v; + } + XyzTarget::Lch => { + let (l, c, h) = neon_triple_to_lch(x_low_low, y_low_low, z_low_low); + x_low_low = l; + y_low_low = c; + z_low_low = h; + } } - XyzTarget::Xyz => {} - XyzTarget::Luv => { - let (l, u, v) = neon_triple_to_luv(x_low_low, y_low_low, z_low_low); - x_low_low = l; - y_low_low = u; - z_low_low = v; - } - XyzTarget::Lch => { - let (l, c, h) = neon_triple_to_lch(x_low_low, y_low_low, z_low_low); - x_low_low = l; - y_low_low = c; - z_low_low = h; - } - } - let xyz_low_low = float32x4x3_t(x_low_low, y_low_low, z_low_low); - vst3q_f32(dst.get_unchecked_mut(cx * 3..).as_mut_ptr(), xyz_low_low); + let xyz_low_low = float32x4x3_t(x_low_low, y_low_low, z_low_low); + vst3q_f32(dst.get_unchecked_mut(cx * 3..).as_mut_ptr(), xyz_low_low); - if USE_ALPHA { - vst1q_f32(a_linearized.get_unchecked_mut(cx..).as_mut_ptr(), a_chan); + if USE_ALPHA { + vst1q_f32(a_linearized.get_unchecked_mut(cx..).as_mut_ptr(), a_chan); + } } cx += 4; diff --git a/src/neon/to_xyza_laba.rs b/src/neon/to_xyza_laba.rs index 6815483..5f85c91 100644 --- a/src/neon/to_xyza_laba.rs +++ b/src/neon/to_xyza_laba.rs @@ -13,14 +13,12 @@ use crate::neon::cie::{ use crate::xyz_target::XyzTarget; use std::arch::aarch64::*; -#[inline(always)] -pub unsafe fn neon_channels_to_xyza_or_laba( +#[target_feature(enable = "neon")] +pub(crate) fn neon_channels_to_xyza_or_laba( start_cx: usize, - src: *const f32, - src_offset: usize, + src: &[f32], + dst: &mut [f32], width: u32, - dst: *mut f32, - dst_offset: usize, matrix: &[[f32; 3]; 3], ) -> usize { let target: XyzTarget = TARGET.into(); @@ -28,22 +26,20 @@ pub unsafe fn neon_channels_to_xyza_or_laba (float32x4_t, float32x4_t, float32x4_t) { - let target: XyzTarget = TARGET.into(); - let lab_pixel = vld3q_f32(src); - let (mut r_f32, mut g_f32, mut b_f32) = (lab_pixel.0, lab_pixel.1, lab_pixel.2); - - match target { - XyzTarget::Lab => { - let (x, y, z) = neon_lab_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; + unsafe { + let target: XyzTarget = TARGET.into(); + let lab_pixel = vld3q_f32(src); + let (mut r_f32, mut g_f32, mut b_f32) = (lab_pixel.0, lab_pixel.1, lab_pixel.2); + + match target { + XyzTarget::Lab => { + let (x, y, z) = neon_lab_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + XyzTarget::Luv => { + let (x, y, z) = neon_luv_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + XyzTarget::Lch => { + let (x, y, z) = neon_lch_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + _ => {} } - XyzTarget::Luv => { - let (x, y, z) = neon_luv_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; - } - XyzTarget::Lch => { - let (x, y, z) = neon_lch_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; - } - _ => {} - } - let (linear_r, linear_g, linear_b) = - vcolorq_matrix_f32(r_f32, g_f32, b_f32, c1, c2, c3, c4, c5, c6, c7, c8, c9); + let (linear_r, linear_g, linear_b) = + vcolorq_matrix_f32(r_f32, g_f32, b_f32, c1, c2, c3, c4, c5, c6, c7, c8, c9); - (linear_r, linear_g, linear_b) + (linear_r, linear_g, linear_b) + } } #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -#[inline(always)] -pub unsafe fn neon_xyz_to_channels< +#[target_feature(enable = "neon")] +pub(crate) fn neon_xyz_to_channels< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, const TARGET: u8, >( start_cx: usize, - src: *const f32, - src_offset: usize, - a_channel: *const f32, - a_offset: usize, - dst: *mut f32, - dst_offset: usize, + src: &[f32], + a_channel: &[f32], + dst: &mut [f32], width: u32, matrix: &[[f32; 3]; 3], ) -> usize { @@ -86,21 +86,20 @@ pub unsafe fn neon_xyz_to_channels< let mut cx = start_cx; - let c1 = vdupq_n_f32(*matrix.get_unchecked(0).get_unchecked(0)); - let c2 = vdupq_n_f32(*matrix.get_unchecked(0).get_unchecked(1)); - let c3 = vdupq_n_f32(*matrix.get_unchecked(0).get_unchecked(2)); - let c4 = vdupq_n_f32(*matrix.get_unchecked(1).get_unchecked(0)); - let c5 = vdupq_n_f32(*matrix.get_unchecked(1).get_unchecked(1)); - let c6 = vdupq_n_f32(*matrix.get_unchecked(1).get_unchecked(2)); - let c7 = vdupq_n_f32(*matrix.get_unchecked(2).get_unchecked(0)); - let c8 = vdupq_n_f32(*matrix.get_unchecked(2).get_unchecked(1)); - let c9 = vdupq_n_f32(*matrix.get_unchecked(2).get_unchecked(2)); + let c1 = vdupq_n_f32(matrix[0][0]); + let c2 = vdupq_n_f32(matrix[0][1]); + let c3 = vdupq_n_f32(matrix[0][2]); + let c4 = vdupq_n_f32(matrix[1][0]); + let c5 = vdupq_n_f32(matrix[1][1]); + let c6 = vdupq_n_f32(matrix[1][2]); + let c7 = vdupq_n_f32(matrix[2][0]); + let c8 = vdupq_n_f32(matrix[2][1]); + let c9 = vdupq_n_f32(matrix[2][2]); let src_channels = 3usize; - while cx + 4 < width as usize { - let offset_src_ptr = - ((src as *const u8).add(src_offset) as *const f32).add(cx * src_channels); + while cx + 4 <= width as usize { + let offset_src_ptr = unsafe { src.get_unchecked(cx * src_channels..).as_ptr() }; let src_ptr_0 = offset_src_ptr; @@ -109,11 +108,11 @@ pub unsafe fn neon_xyz_to_channels< src_ptr_0, c1, c2, c3, c4, c5, c6, c7, c8, c9, ); - let dst_ptr = ((dst as *mut u8).add(dst_offset) as *mut f32).add(cx * channels); + let dst_ptr = unsafe { dst.get_unchecked_mut(cx * channels..).as_mut_ptr() }; if USE_ALPHA { - let offset_a_src_ptr = ((a_channel as *const u8).add(a_offset) as *const f32).add(cx); - let a_row = vld1q_f32(offset_a_src_ptr); + let offset_a_src_ptr = unsafe { a_channel.get_unchecked(cx..).as_ptr() }; + let a_row = unsafe { vld1q_f32(offset_a_src_ptr) }; let store_rows = match image_configuration { ImageConfiguration::Rgb | ImageConfiguration::Rgba => { float32x4x4_t(r_row0_, g_row0_, b_row0_, a_row) @@ -122,7 +121,9 @@ pub unsafe fn neon_xyz_to_channels< float32x4x4_t(b_row0_, g_row0_, r_row0_, a_row) } }; - vst4q_f32(dst_ptr, store_rows); + unsafe { + vst4q_f32(dst_ptr, store_rows); + } } else { let store_rows = match image_configuration { ImageConfiguration::Rgb | ImageConfiguration::Rgba => { @@ -132,7 +133,9 @@ pub unsafe fn neon_xyz_to_channels< float32x4x3_t(b_row0_, g_row0_, r_row0_) } }; - vst3q_f32(dst_ptr, store_rows); + unsafe { + vst3q_f32(dst_ptr, store_rows); + } } cx += 4; diff --git a/src/neon/xyza_laba_to_image.rs b/src/neon/xyza_laba_to_image.rs index 1c74b98..e9b3775 100644 --- a/src/neon/xyza_laba_to_image.rs +++ b/src/neon/xyza_laba_to_image.rs @@ -11,8 +11,9 @@ use crate::neon::math::vcolorq_matrix_f32; use crate::xyz_target::XyzTarget; use std::arch::aarch64::*; -#[inline(always)] -pub(crate) unsafe fn neon_xyza_lab_vld( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_xyza_lab_vld( src: *const f32, c1: float32x4_t, c2: float32x4_t, @@ -25,7 +26,7 @@ pub(crate) unsafe fn neon_xyza_lab_vld (float32x4_t, float32x4_t, float32x4_t, float32x4_t) { let target: XyzTarget = TARGET.into(); - let lab_pixel = vld4q_f32(src); + let lab_pixel = unsafe { vld4q_f32(src) }; let (mut r_f32, mut g_f32, mut b_f32) = (lab_pixel.0, lab_pixel.1, lab_pixel.2); match target { @@ -56,13 +57,12 @@ pub(crate) unsafe fn neon_xyza_lab_vld( +#[inline] +#[target_feature(enable = "neon")] +pub(crate) fn neon_xyza_to_image( start_cx: usize, - src: *const f32, - src_offset: usize, - dst: *mut f32, - dst_offset: usize, + src: &[f32], + dst: &mut [f32], width: u32, matrix: &[[f32; 3]; 3], ) -> usize { @@ -75,20 +75,20 @@ pub unsafe fn neon_xyza_to_image { @@ -107,7 +107,9 @@ pub unsafe fn neon_xyza_to_image( if image_configuration == ImageConfiguration::Bgr || image_configuration == ImageConfiguration::Rgb { - let transient_row = &mut transient_row[cx * 3..]; - let src = &src[cx * 3usize..]; - for (dst, src) in transient_row - .as_chunks_mut::<3>() - .0 - .iter_mut() - .zip(src.as_chunks::<3>().0.iter()) { - let rgb = match target { - OklabTarget::Oklab => { - let oklab = Oklab::new(src[0], src[1], src[2]); - oklab.to_linear_rgb() - } - OklabTarget::Oklch => { - let oklch = Oklch::new(src[0], src[1], src[2]); - oklch.to_linear_rgb() - } - }; - dst[0] = rgb.r; - dst[1] = rgb.g; - dst[2] = rgb.b; + let transient_row = &mut transient_row[cx * 3..]; + let src = &src[cx * 3usize..]; + for (dst, src) in transient_row + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + let rgb = match target { + OklabTarget::Oklab => { + let oklab = Oklab::new(src[0], src[1], src[2]); + oklab.to_linear_rgb() + } + OklabTarget::Oklch => { + let oklch = Oklch::new(src[0], src[1], src[2]); + oklch.to_linear_rgb() + } + }; + dst[0] = rgb.r; + dst[1] = rgb.g; + dst[2] = rgb.b; + } } for (dst, src) in dst @@ -122,28 +124,30 @@ fn oklab_to_image( } else if image_configuration == ImageConfiguration::Bgra || image_configuration == ImageConfiguration::Rgba { - let transient_row = &mut transient_row[cx * 4..]; - let src = &src[cx * 4usize..]; - for (dst, src) in transient_row - .as_chunks_mut::<4>() - .0 - .iter_mut() - .zip(src.as_chunks::<4>().0.iter()) { - let rgb = match target { - OklabTarget::Oklab => { - let oklab = Oklab::new(src[0], src[1], src[2]); - oklab.to_linear_rgb() - } - OklabTarget::Oklch => { - let oklch = Oklch::new(src[0], src[1], src[2]); - oklch.to_linear_rgb() - } - }; - dst[0] = rgb.r; - dst[1] = rgb.g; - dst[2] = rgb.b; - dst[3] = src[3]; + let transient_row = &mut transient_row[cx * 4..]; + let src = &src[cx * 4usize..]; + for (dst, src) in transient_row + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + let rgb = match target { + OklabTarget::Oklab => { + let oklab = Oklab::new(src[0], src[1], src[2]); + oklab.to_linear_rgb() + } + OklabTarget::Oklch => { + let oklch = Oklch::new(src[0], src[1], src[2]); + oklch.to_linear_rgb() + } + }; + dst[0] = rgb.r; + dst[1] = rgb.g; + dst[2] = rgb.b; + dst[3] = src[3]; + } } for (dst, src) in dst diff --git a/src/oklch.rs b/src/oklch.rs index 758bb06..ae98d77 100644 --- a/src/oklch.rs +++ b/src/oklch.rs @@ -5,7 +5,7 @@ * // license that can be found in the LICENSE file. */ use crate::{EuclideanDistance, Oklab, Rgb, TaxicabDistance, TransferFunction}; -use erydanos::{eatan2f, ehypotf, Cosine, Sine}; +use erydanos::{Cosine, Sine, eatan2f, ehypotf}; use num_traits::Pow; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; diff --git a/src/planar_to_linear.rs b/src/planar_to_linear.rs index 33e1db0..d5b96c7 100644 --- a/src/planar_to_linear.rs +++ b/src/planar_to_linear.rs @@ -5,65 +5,44 @@ * // license that can be found in the LICENSE file. */ -use crate::TransferFunction; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -use std::slice; - -#[inline(always)] -#[allow(clippy::type_complexity)] +use crate::{ColorError, ImageBuffer, ImageBufferMut, TransferFunction}; + fn channels_to_linear( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { - let dst_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - dst.as_mut_ptr() as *mut u8, - dst_stride as usize * height as usize, - ) - }; +) -> Result<(), ColorError> { + dst.validate()?; + src.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 1 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } - let mut lut_table = vec![0f32; 256]; + let mut lut_table = [0f32; 256]; for (i, lut) in lut_table.iter_mut().enumerate() { *lut = transfer_function.linearize(i as f32 * (1. / 255.0)); } - let iter; - #[cfg(feature = "rayon")] - { - iter = dst_slice_safe_align - .par_chunks_exact_mut(dst_stride as usize) - .zip(src.par_chunks_exact(src_stride as usize)); - } - #[cfg(not(feature = "rayon"))] - { - iter = dst_slice_safe_align - .chunks_exact_mut(dst_stride as usize) - .zip(src.chunks_exact(src_stride as usize)); - } - - iter.for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; - let src_ptr = src.as_ptr(); - let dst_ptr = dst.as_mut_ptr() as *mut f32; - - for x in _cx..width as usize { - let px = x; - let dst = dst_ptr.add(px); - let src = src_ptr.add(px); - let transferred = *lut_table.get_unchecked(src.read_unaligned() as usize); + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) + { + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; - dst.write_unaligned(transferred); + for (dst, &src) in dst.iter_mut().zip(src.iter()) { + *dst = lut_table[src as usize]; } - }); + } + + Ok(()) } /// This function converts Plane to Linear. This is much more effective than naive direct transformation @@ -77,21 +56,186 @@ fn channels_to_linear( /// * `dst_stride` - Bytes per row for dst data /// * `transfer_function` - Transfer function from gamma to linear space. If you don't have specific pick `Srgb` pub fn plane_to_linear( - src: &[u8], - src_stride: u32, - dst: &mut [f32], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u8>, + dst: &mut ImageBufferMut<'_, f32>, transfer_function: TransferFunction, -) { - channels_to_linear( - src, - src_stride, - dst, - dst_stride, - width, - height, - transfer_function, - ); +) -> Result<(), ColorError> { + channels_to_linear(src, dst, transfer_function) +} + +#[cfg(test)] +mod tests_plane_to_linear { + use super::*; + use crate::BufferStore; + + fn make_src(data: Vec, width: u32, height: u32) -> ImageBuffer<'static, u8> { + ImageBuffer::from_vec(data, width, height, width, 1).unwrap() + } + + fn make_dst(width: u32, height: u32) -> ImageBufferMut<'static, f32> { + ImageBufferMut::new( + BufferStore::Owned(vec![0f32; (width * height) as usize]), + width, + height, + width, + 1, + ) + .unwrap() + } + + fn assert_approx(a: f32, b: f32, eps: f32, label: &str) { + assert!((a - b).abs() < eps, "{label}: got {a}, expected {b}"); + } + + // ── boundary values ─────────────────────────────────────────────────────── + + #[test] + fn zero_maps_to_zero() { + let src = make_src(vec![0], 1, 1); + let mut dst = make_dst(1, 1); + plane_to_linear(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_eq!(dst.data.borrow()[0], 0.0); + } + + #[test] + fn max_maps_to_one() { + let src = make_src(vec![255], 1, 1); + let mut dst = make_dst(1, 1); + plane_to_linear(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_approx(dst.data.borrow()[0], 1.0, 1e-5, "255 → 1.0"); + } + + // ── lut correctness ─────────────────────────────────────────────────────── + + #[test] + fn lut_matches_direct_linearize() { + for input in 0u8..=255 { + let expected = TransferFunction::Srgb.linearize(input as f32 / 255.0); + let src = make_src(vec![input], 1, 1); + let mut dst = make_dst(1, 1); + plane_to_linear(&src, &mut dst, TransferFunction::Srgb).unwrap(); + assert_approx( + dst.data.borrow()[0], + expected, + 1e-6, + &format!("input={input}"), + ); + } + } + + #[test] + fn lut_matches_direct_linearize_linear_transfer() { + for input in (0u8..=255).step_by(16) { + let expected = TransferFunction::Linear.linearize(input as f32 / 255.0); + let src = make_src(vec![input], 1, 1); + let mut dst = make_dst(1, 1); + plane_to_linear(&src, &mut dst, TransferFunction::Linear).unwrap(); + assert_approx( + dst.data.borrow()[0], + expected, + 1e-6, + &format!("input={input}"), + ); + } + } + + // ── linear transfer is identity ─────────────────────────────────────────── + + #[test] + fn linear_transfer_is_identity() { + for input in (0u8..=255).step_by(16) { + let src = make_src(vec![input], 1, 1); + let mut dst = make_dst(1, 1); + plane_to_linear(&src, &mut dst, TransferFunction::Linear).unwrap(); + let expected = input as f32 / 255.0; + assert_approx( + dst.data.borrow()[0], + expected, + 1e-6, + &format!("input={input}"), + ); + } + } + + // ── output range ────────────────────────────────────────────────────────── + + #[test] + fn output_always_in_0_1_range() { + let data: Vec = (0u8..=255).collect(); + let src = make_src(data, 256, 1); + let mut dst = make_dst(256, 1); + plane_to_linear(&src, &mut dst, TransferFunction::Srgb).unwrap(); + for (i, &v) in dst.data.borrow().iter().enumerate() { + assert!( + (0.0..=1.0).contains(&v), + "output out of range at index {i}: {v}" + ); + } + } + + // ── monotonicity ────────────────────────────────────────────────────────── + + #[test] + fn output_monotonically_non_decreasing() { + let data: Vec = (0u8..=255).collect(); + let src = make_src(data, 256, 1); + let mut dst = make_dst(256, 1); + plane_to_linear(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + for i in 1..d.len() { + assert!( + d[i] >= d[i - 1], + "non-monotone at index {i}: {} < {}", + d[i], + d[i - 1] + ); + } + } + + // ── roundtrip ───────────────────────────────────────────────────────────── + + #[test] + fn roundtrip_srgb() { + for original in 0u8..=255 { + let src = make_src(vec![original], 1, 1); + let mut dst = make_dst(1, 1); + plane_to_linear(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let linear = dst.data.borrow()[0]; + // gamma(linear) * 255 should recover original within 1 DN + let recovered = (TransferFunction::Srgb.gamma(linear) * 255.).round() as u8; + assert!( + (recovered as i16 - original as i16).abs() <= 1, + "original={original} recovered={recovered}" + ); + } + } + + // ── multi-pixel / multi-row consistency ─────────────────────────────────── + + #[test] + fn multi_pixel_consistent() { + let src = make_src(vec![128u8; 8], 8, 1); + let mut dst = make_dst(8, 1); + plane_to_linear(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert!(d.iter().all(|&v| v == d[0]), "inconsistent pixels: {d:?}"); + } + + #[test] + fn multi_row_consistent() { + let src = make_src(vec![200u8; 4], 2, 2); + let mut dst = make_dst(2, 2); + plane_to_linear(&src, &mut dst, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert!(d.iter().all(|&v| v == d[0]), "row mismatch: {d:?}"); + } + + // ── error paths ─────────────────────────────────────────────────────────── + + #[test] + fn rejects_multi_channel_src() { + let src = ImageBuffer::from_vec(vec![0u8; 3], 1, 1, 3, 3).unwrap(); + let mut dst = make_dst(1, 1); + assert!(plane_to_linear(&src, &mut dst, TransferFunction::Srgb).is_err()); + } } diff --git a/src/rgb.rs b/src/rgb.rs index f234bf6..b6ddeb8 100644 --- a/src/rgb.rs +++ b/src/rgb.rs @@ -10,14 +10,14 @@ use crate::lab::Lab; use crate::luv::Luv; use crate::oklch::Oklch; use crate::{ + Hsl, Jzazbz, LAlphaBeta, LCh, Oklab, Rgba, Sigmoidal, TaxicabDistance, TransferFunction, Xyz, adjust_saturation, clip_color, color_add, color_burn, color_darken, color_difference, color_dodge, color_exclusion, color_hard_light, color_hard_mix, color_lighten, color_linear_burn, color_linear_light, color_pin_light, color_reflect, color_screen, - color_soft_light, color_soft_light_weight, color_vivid_light, pdf_lum, Hsl, Jzazbz, LAlphaBeta, - LCh, Oklab, Rgba, Sigmoidal, TaxicabDistance, TransferFunction, Xyz, + color_soft_light, color_soft_light_weight, color_vivid_light, pdf_lum, }; use num_traits::{AsPrimitive, Bounded, Float, Num, Pow}; -use std::cmp::{max, min, Ordering}; +use std::cmp::{Ordering, max, min}; use std::ops::{Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub}; #[repr(C)] @@ -288,11 +288,11 @@ impl From> for Rgb { impl Rgb { #[inline] - pub fn apply(&self, gen: fn(f32) -> f32) -> Self { + pub fn apply(&self, fun: fn(f32) -> f32) -> Self { Self { - r: gen(self.r), - g: gen(self.g), - b: gen(self.b), + r: fun(self.r), + g: fun(self.g), + b: fun(self.b), } } diff --git a/src/rgb_expand.rs b/src/rgb_expand.rs index af81e56..2788612 100644 --- a/src/rgb_expand.rs +++ b/src/rgb_expand.rs @@ -5,162 +5,60 @@ * // license that can be found in the LICENSE file. */ -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -use crate::avx::*; -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -use crate::sse::*; -#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -use std::arch::aarch64::*; -#[cfg(target_arch = "x86")] -#[allow(unused_imports)] -use std::arch::x86::*; -#[cfg(target_arch = "x86_64")] -#[allow(unused_imports)] -use std::arch::x86_64::*; +use crate::{ColorError, ImageBuffer, ImageBufferMut}; /// Expands RGB to RGBA. pub fn rgb_to_rgba( - src: &[u8], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer, + dst: &mut ImageBufferMut, default_alpha: u8, -) { - let mut dst_offset = 0usize; - let mut src_offset = 0usize; - - #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] - let mut _use_sse = std::arch::is_x86_feature_detected!("sse4.1"); - - #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] - let mut _use_avx = std::arch::is_x86_feature_detected!("avx2"); - - for _ in 0..height as usize { - #[allow(unused_mut)] - let mut cx = 0usize; - - #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] - unsafe { - let src_ptr = src.as_ptr().add(src_offset); - let dst_ptr = dst.as_mut_ptr().add(dst_offset); - if _use_avx { - rgb_expand_avx(width, default_alpha, cx, src_ptr, dst_ptr); - } - } - - #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] - unsafe { - let src_ptr = src.as_ptr().add(src_offset); - let dst_ptr = dst.as_mut_ptr().add(dst_offset); - if _use_sse { - rgb_expand_sse(width, default_alpha, cx, src_ptr, dst_ptr); - } - } - - #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] - unsafe { - let v_alpha = vdupq_n_u8(default_alpha); - let src_ptr = src.as_ptr().add(src_offset); - let dst_ptr = dst.as_mut_ptr().add(dst_offset); - while cx + 16 < width as usize { - let xyz_pixel = vld3q_u8(src_ptr.add(cx * 3usize)); - let dst_pixel = uint8x16x4_t(xyz_pixel.0, xyz_pixel.1, xyz_pixel.2, v_alpha); - vst4q_u8(dst_ptr.add(cx * 4), dst_pixel); - cx += 16; - } - } - - for x in cx..width as usize { - let px = dst_offset + x * 4; - let s_x = src_offset + x * 3; - unsafe { - let source = src.get_unchecked(s_x..); - let dest = dst.get_unchecked_mut(px..); - *dest.get_unchecked_mut(0) = *source.get_unchecked(0); - *dest.get_unchecked_mut(1) = *source.get_unchecked(1); - *dest.get_unchecked_mut(2) = *source.get_unchecked(2); - *dest.get_unchecked_mut(3) = default_alpha; - } - } - - dst_offset += dst_stride as usize; - src_offset += src_stride as usize; +) -> Result<(), ColorError> { + src.validate()?; + dst.validate()?; + if src.channels != 3 { + return Err(ColorError::Generic( + "We can add alpha only to 3 channels image".to_string(), + )); + } + if dst.channels != 4 { + return Err(ColorError::Generic( + "Dst image should be 4 channels".to_string(), + )); } -} - -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[target_feature(enable = "avx2")] -unsafe fn rgb_expand_avx( - width: u32, - default_alpha: u8, - mut cx: usize, - src_ptr: *const u8, - dst_ptr: *mut u8, -) { - let v_alpha = _mm256_set1_epi8(default_alpha as i8); - while cx + 32 < width as usize { - let xyz_chan_ptr = src_ptr.add(cx * 3usize); - let xyz0 = _mm256_loadu_si256(xyz_chan_ptr as *const __m256i); - let xyz1 = _mm256_loadu_si256(xyz_chan_ptr.add(32) as *const __m256i); - let xyz2 = _mm256_loadu_si256(xyz_chan_ptr.add(64) as *const __m256i); - let (x_p, y_p, z_p) = avx2_deinterleave_rgb_epi8(xyz0, xyz1, xyz2); - let xyza_chan_ptr = dst_ptr.add(cx * 4usize); + dst.try_match_immutable(src)?; - let (xyza0, xyza1, xyza2, xyza3) = avx2_interleave_rgba_epi8(x_p, y_p, z_p, v_alpha); - _mm256_storeu_si256(xyza_chan_ptr as *mut __m256i, xyza0); - _mm256_storeu_si256(xyza_chan_ptr.add(32) as *mut __m256i, xyza1); - _mm256_storeu_si256(xyza_chan_ptr.add(64) as *mut __m256i, xyza2); - _mm256_storeu_si256(xyza_chan_ptr.add(96) as *mut __m256i, xyza3); - cx += 32; + let src_working_width = src.width * src.channels; + let dst_working_width = dst.width * dst.channels; + let dst_stride = dst.stride(); + for (src, dst) in src + .data + .chunks(src.stride()) + .zip(dst.data.borrow_mut().chunks_mut(dst_stride)) + { + let src = &src[..src_working_width as usize]; + let dst = &mut dst[..dst_working_width as usize]; + for (src, dst) in src + .as_chunks::<3>() + .0 + .iter() + .zip(dst.as_chunks_mut::<4>().0.iter_mut()) + { + dst[0] = src[0]; + dst[1] = src[1]; + dst[2] = src[2]; + dst[3] = default_alpha; + } } -} -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -#[target_feature(enable = "sse4.1")] -unsafe fn rgb_expand_sse( - width: u32, - default_alpha: u8, - mut cx: usize, - src_ptr: *const u8, - dst_ptr: *mut u8, -) { - let v_alpha = _mm_set1_epi8(default_alpha as i8); - while cx + 16 < width as usize { - let xyz_chan_ptr = src_ptr.add(cx * 3usize); - let xyz0 = _mm_loadu_si128(xyz_chan_ptr as *const __m128i); - let xyz1 = _mm_loadu_si128(xyz_chan_ptr.add(16) as *const __m128i); - let xyz2 = _mm_loadu_si128(xyz_chan_ptr.add(32) as *const __m128i); - let (x_p, y_p, z_p) = sse_deinterleave_rgb(xyz0, xyz1, xyz2); - let (xyza0, xyza1, xyza2, xyza3) = sse_interleave_rgba(x_p, y_p, z_p, v_alpha); - let xyza_chan_ptr = dst_ptr.add(cx * 4usize); - _mm_storeu_si128(xyza_chan_ptr as *mut __m128i, xyza0); - _mm_storeu_si128(xyza_chan_ptr.add(16) as *mut __m128i, xyza1); - _mm_storeu_si128(xyza_chan_ptr.add(32) as *mut __m128i, xyza2); - _mm_storeu_si128(xyza_chan_ptr.add(48) as *mut __m128i, xyza3); - cx += 16; - } + Ok(()) } /// Expands BGR to BGRA. pub fn bgr_to_bgra( - src: &[u8], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer, + dst: &mut ImageBufferMut, default_alpha: u8, -) { - rgb_to_rgba( - src, - src_stride, - dst, - dst_stride, - width, - height, - default_alpha, - ); +) -> Result<(), ColorError> { + rgb_to_rgba(src, dst, default_alpha) } diff --git a/src/rgba.rs b/src/rgba.rs index 2de41c7..a065264 100644 --- a/src/rgba.rs +++ b/src/rgba.rs @@ -11,12 +11,12 @@ use crate::routines::{ op_screen, op_soft_light, op_vivid_light, }; use crate::{ - adjust_saturation, clip_color, color_add, pdf_lum, EuclideanDistance, TaxicabDistance, - TransferFunction, + EuclideanDistance, TaxicabDistance, TransferFunction, adjust_saturation, clip_color, color_add, + pdf_lum, }; use half::f16; -use num_traits::{clamp, AsPrimitive, Bounded, Float, Num, Pow}; -use std::cmp::{max, min, Ordering}; +use num_traits::{AsPrimitive, Bounded, Float, Num, Pow, clamp}; +use std::cmp::{Ordering, max, min}; use std::ops::{Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, Neg, Sub}; #[repr(C)] diff --git a/src/routines.rs b/src/routines.rs index 8a6161f..9b739d4 100644 --- a/src/routines.rs +++ b/src/routines.rs @@ -18,9 +18,7 @@ macro_rules! color_dodge { #[macro_export] macro_rules! color_linear_burn { - ($base: expr, $other: expr) => {{ - ($base + $other - 1.).max(0.) - }}; + ($base: expr, $other: expr) => {{ ($base + $other - 1.).max(0.) }}; } #[macro_export] @@ -36,30 +34,22 @@ macro_rules! color_burn { #[macro_export] macro_rules! color_darken { - ($base: expr, $other: expr) => {{ - $base.min($other) - }}; + ($base: expr, $other: expr) => {{ $base.min($other) }}; } #[macro_export] macro_rules! color_lighten { - ($base: expr, $other: expr) => {{ - $base.max($other) - }}; + ($base: expr, $other: expr) => {{ $base.max($other) }}; } #[macro_export] macro_rules! color_screen { - ($base: expr, $other: expr) => {{ - $base + $other - $base * $other - }}; + ($base: expr, $other: expr) => {{ $base + $other - $base * $other }}; } #[macro_export] macro_rules! color_add { - ($base: expr, $other: expr) => {{ - ($base + $other).min(1.) - }}; + ($base: expr, $other: expr) => {{ ($base + $other).min(1.) }}; } #[macro_export] @@ -119,16 +109,12 @@ macro_rules! color_reflect { #[macro_export] macro_rules! color_difference { - ($base: expr, $other: expr) => {{ - ($base - $other).abs() - }}; + ($base: expr, $other: expr) => {{ ($base - $other).abs() }}; } #[macro_export] macro_rules! pdf_lum { - ($base: expr) => {{ - 0.3 * $base.r + 0.59 * $base.g + 0.11 * $base.g - }}; + ($base: expr) => {{ 0.3 * $base.r + 0.59 * $base.g + 0.11 * $base.g }}; } #[macro_export] @@ -189,9 +175,7 @@ macro_rules! color_soft_light { #[macro_export] macro_rules! color_exclusion { - ($base: expr, $other: expr) => {{ - $base + $other - 2. * $base * $other - }}; + ($base: expr, $other: expr) => {{ $base + $other - 2. * $base * $other }}; } #[macro_export] @@ -217,11 +201,7 @@ macro_rules! adjust_saturation { #[inline] pub(crate) fn op_color_dodge(a: f32, b: f32) -> f32 { - if b == 1.0 { - b - } else { - (a / (1.0 - b)).min(1.) - } + if b == 1.0 { b } else { (a / (1.0 - b)).min(1.) } } #[inline] diff --git a/src/sigmoidal_to_image.rs b/src/sigmoidal_to_image.rs index dcee138..5285876 100644 --- a/src/sigmoidal_to_image.rs +++ b/src/sigmoidal_to_image.rs @@ -5,116 +5,105 @@ * // license that can be found in the LICENSE file. */ -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -use crate::avx::avx_from_sigmoidal_row; use crate::image::ImageConfiguration; -#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -use crate::neon::neon_from_sigmoidal_row; -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -use crate::sse::sse_from_sigmoidal_row; -use crate::{Rgb, Sigmoidal}; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -use std::slice; - -#[allow(clippy::type_complexity)] +use crate::{ColorError, ImageBuffer, ImageBufferMut, Rgb, Sigmoidal}; + fn sigmoidal_to_image( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, -) { + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, +) -> Result<(), ColorError> { + dst.validate()?; + src.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); if USE_ALPHA && !image_configuration.has_alpha() { panic!("Alpha may be set only on images with alpha"); } - let channels = image_configuration.channel_count(); - - let mut _wide_row_handler: Option usize> = None; + let mut _wide_row_handler: Option usize> = None; #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] if std::arch::is_x86_feature_detected!("sse4.1") { + use crate::sse::sse_from_sigmoidal_row; _wide_row_handler = Some(sse_from_sigmoidal_row::); } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] if std::arch::is_x86_feature_detected!("avx2") { + use crate::avx::avx_from_sigmoidal_row; _wide_row_handler = Some(avx_from_sigmoidal_row::); } #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] { + use crate::neon::neon_from_sigmoidal_row; _wide_row_handler = Some(neon_from_sigmoidal_row::); } - let src_slice_safe_align = unsafe { - slice::from_raw_parts( - src.as_ptr() as *const u8, - src_stride as usize * height as usize, - ) - }; - - let iter; + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; + let width = src.width; - #[cfg(feature = "rayon")] - { - iter = dst - .par_chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.par_chunks_exact(src_stride as usize)); - } - #[cfg(not(feature = "rayon"))] + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) { - iter = dst - .chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.chunks_exact(src_stride as usize)); - } - - iter.for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; - let src_ptr = src.as_ptr() as *const f32; - let dst_ptr = dst.as_mut_ptr(); + let mut cx = 0usize; if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher(_cx, src_ptr, dst_ptr, width); + cx = unsafe { dispatcher(cx, src, dst, width) }; } - for x in _cx..width as usize { - let px = x * channels; - let reading_ptr = src_ptr.add(px); - let sr = reading_ptr.read_unaligned(); - let sg = reading_ptr.add(1).read_unaligned(); - let sb = reading_ptr.add(2).read_unaligned(); - - let sigmoidal = Sigmoidal::new(sr, sg, sb); - let rgb: Rgb = sigmoidal.into(); - - let hx = x * channels; - - let dst = dst_ptr.add(hx); - - dst.add(image_configuration.r_index()) - .write_unaligned(rgb.r); - dst.add(image_configuration.g_index()) - .write_unaligned(rgb.g); - dst.add(image_configuration.b_index()) - .write_unaligned(rgb.b); - - if image_configuration.has_alpha() { - let a = (reading_ptr.add(3).read_unaligned() * 255f32) - .max(0f32) - .round() - .min(255f32); - dst.add(image_configuration.a_index()) - .write_unaligned(a as u8); + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + let src = &src[cx * 3..]; + let dst = &mut dst[cx * 3..]; + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + let sigmoidal = Sigmoidal::new(src[0], src[1], src[2]); + let rgb: Rgb = sigmoidal.into(); + dst[image_configuration.r_index()] = rgb.r; + dst[image_configuration.g_index()] = rgb.g; + dst[image_configuration.b_index()] = rgb.b; + } + } else if image_configuration == ImageConfiguration::Rgba + || image_configuration == ImageConfiguration::Bgra + { + let src = &src[cx * 4..]; + let dst = &mut dst[cx * 4..]; + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + let sigmoidal = Sigmoidal::new(src[0], src[1], src[2]); + let rgb: Rgb = sigmoidal.into(); + dst[image_configuration.r_index()] = rgb.r; + dst[image_configuration.g_index()] = rgb.g; + dst[image_configuration.b_index()] = rgb.b; + dst[image_configuration.a_index()] = + (src[3] * 255f32).clamp(0f32, 255f32).round() as u8; } } - }); + } + + Ok(()) } /// This function converts Sigmoid to RGB. This is much more effective than naive direct transformation @@ -127,16 +116,10 @@ fn sigmoidal_to_image( /// * `dst` - A mutable slice to receive RGB data /// * `dst_stride` - Bytes per row for dst data pub fn sigmoidal_to_rgb( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, -) { - sigmoidal_to_image::<{ ImageConfiguration::Rgb as u8 }, false>( - src, src_stride, dst, dst_stride, width, height, - ); + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, +) -> Result<(), ColorError> { + sigmoidal_to_image::<{ ImageConfiguration::Rgb as u8 }, false>(src, dst) } /// This function converts Sigmoid to BGRA. Alpha channel expected to be normalized and will be denormalized during transformation. This is much more effective than naive direct transformation @@ -149,16 +132,10 @@ pub fn sigmoidal_to_rgb( /// * `dst` - A mutable slice to receive BGRA data /// * `dst_stride` - Bytes per row for dst data pub fn sigmoidal_to_bgra( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, -) { - sigmoidal_to_image::<{ ImageConfiguration::Bgra as u8 }, true>( - src, src_stride, dst, dst_stride, width, height, - ); + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, +) -> Result<(), ColorError> { + sigmoidal_to_image::<{ ImageConfiguration::Bgra as u8 }, true>(src, dst) } /// This function converts Sigmoid to RGBA. Alpha channel expected to be normalized and will be denormalized during transformation. This is much more effective than naive direct transformation @@ -171,14 +148,319 @@ pub fn sigmoidal_to_bgra( /// * `dst` - A mutable slice to receive RGBA data /// * `dst_stride` - Bytes per row for dst data pub fn sigmoidal_to_rgba( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, -) { - sigmoidal_to_image::<{ ImageConfiguration::Rgba as u8 }, true>( - src, src_stride, dst, dst_stride, width, height, - ); + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, +) -> Result<(), ColorError> { + sigmoidal_to_image::<{ ImageConfiguration::Rgba as u8 }, true>(src, dst) +} + +#[cfg(test)] +mod tests_sigmoidal_to_image { + use super::*; + use crate::BufferStore; + + fn make_src( + data: Vec, + width: u32, + height: u32, + channels: u32, + ) -> ImageBuffer<'static, f32> { + ImageBuffer::from_vec(data, width, height, width * channels, channels).unwrap() + } + + fn make_dst(width: u32, height: u32, channels: u32) -> ImageBufferMut<'static, u8> { + ImageBufferMut::new( + BufferStore::Owned(vec![0u8; (width * height * channels) as usize]), + width, + height, + width * channels, + channels, + ) + .unwrap() + } + + fn assert_approx(a: u8, b: u8, eps: u8, label: &str) { + assert!( + (a as i16 - b as i16).abs() <= eps as i16, + "{label}: got {a}, expected {b}" + ); + } + + // Convert known RGB u8 → Sigmoidal f32 so tests can work with the inverse + fn rgb_to_sigmoidal(r: u8, g: u8, b: u8) -> (f32, f32, f32) { + let rgb = Rgb::::new(r, g, b); + let sig: Sigmoidal = rgb.into(); + (sig.sr, sig.sg, sig.sb) + } + + // ── black / white roundtrip ─────────────────────────────────────────────── + + #[test] + fn rgb_black_roundtrip() { + let (sr, sg, sb) = rgb_to_sigmoidal(0, 0, 0); + let src = make_src(vec![sr, sg, sb], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + sigmoidal_to_rgb(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 0, 1, "R black"); + assert_approx(d[1], 0, 1, "G black"); + assert_approx(d[2], 0, 1, "B black"); + } + + #[test] + fn rgb_white_roundtrip() { + let (sr, sg, sb) = rgb_to_sigmoidal(255, 255, 255); + let src = make_src(vec![sr, sg, sb], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + sigmoidal_to_rgb(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 1, "R white"); + assert_approx(d[1], 255, 1, "G white"); + assert_approx(d[2], 255, 1, "B white"); + } + + #[test] + fn rgba_black_opaque_roundtrip() { + let (sr, sg, sb) = rgb_to_sigmoidal(0, 0, 0); + let src = make_src(vec![sr, sg, sb, 1.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + sigmoidal_to_rgba(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 0, 1, "R"); + assert_approx(d[1], 0, 1, "G"); + assert_approx(d[2], 0, 1, "B"); + assert_approx(d[3], 255, 1, "A"); + } + + #[test] + fn rgba_white_opaque_roundtrip() { + let (sr, sg, sb) = rgb_to_sigmoidal(255, 255, 255); + let src = make_src(vec![sr, sg, sb, 1.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + sigmoidal_to_rgba(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 1, "R"); + assert_approx(d[1], 255, 1, "G"); + assert_approx(d[2], 255, 1, "B"); + assert_approx(d[3], 255, 1, "A"); + } + + #[test] + fn bgra_white_opaque_roundtrip() { + let (sr, sg, sb) = rgb_to_sigmoidal(255, 255, 255); + let src = make_src(vec![sr, sg, sb, 1.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + sigmoidal_to_bgra(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + // BGRA layout: d[0]=B d[1]=G d[2]=R d[3]=A + assert_approx(d[0], 255, 1, "B"); + assert_approx(d[1], 255, 1, "G"); + assert_approx(d[2], 255, 1, "R"); + assert_approx(d[3], 255, 1, "A"); + } + + // ── full roundtrip for a range of values ────────────────────────────────── + + #[test] + fn rgb_roundtrip_range() { + for v in (0u8..=255).step_by(16) { + let (sr, sg, sb) = rgb_to_sigmoidal(v, v, v); + let src = make_src(vec![sr, sg, sb], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + sigmoidal_to_rgb(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], v, 1, &format!("R v={v}")); + assert_approx(d[1], v, 1, &format!("G v={v}")); + assert_approx(d[2], v, 1, &format!("B v={v}")); + } + } + + #[test] + fn rgba_roundtrip_range() { + for v in (0u8..=255).step_by(32) { + let (sr, sg, sb) = rgb_to_sigmoidal(v, v, v); + let src = make_src(vec![sr, sg, sb, 0.6], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + sigmoidal_to_rgba(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], v, 1, &format!("R v={v}")); + assert_approx(d[1], v, 1, &format!("G v={v}")); + assert_approx(d[2], v, 1, &format!("B v={v}")); + } + } + + // ── alpha denormalization ───────────────────────────────────────────────── + + #[test] + fn rgba_alpha_denormalized_correctly() { + let (sr, sg, sb) = rgb_to_sigmoidal(128, 128, 128); + for alpha in [0.0f32, 0.25, 0.5, 0.75, 1.0] { + let src = make_src(vec![sr, sg, sb, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + sigmoidal_to_rgba(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + let expected = (alpha * 255.).round() as u8; + assert_approx(d[3], expected, 1, &format!("alpha={alpha}")); + } + } + + #[test] + fn bgra_alpha_denormalized_correctly() { + let (sr, sg, sb) = rgb_to_sigmoidal(128, 128, 128); + for alpha in [0.0f32, 0.25, 0.5, 0.75, 1.0] { + let src = make_src(vec![sr, sg, sb, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + sigmoidal_to_bgra(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + let expected = (alpha * 255.).round() as u8; + assert_approx(d[3], expected, 1, &format!("alpha={alpha}")); + } + } + + #[test] + fn rgba_rgb_channels_encoded_when_alpha_zero() { + let (sr, sg, sb) = rgb_to_sigmoidal(200, 100, 50); + let src = make_src(vec![sr, sg, sb, 0.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + sigmoidal_to_rgba(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 200, 1, "R encoded despite alpha=0"); + assert_eq!(d[3], 0, "A must remain 0"); + } + + // ── channel ordering ────────────────────────────────────────────────────── + + #[test] + fn rgb_channel_order_correct_for_red() { + let (sr, sg, sb) = rgb_to_sigmoidal(255, 0, 0); + let src = make_src(vec![sr, sg, sb], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + sigmoidal_to_rgb(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + assert!(d[0] > 200, "R dominant for red input, got {}", d[0]); + assert!(d[1] < 10, "G low for red input, got {}", d[1]); + assert!(d[2] < 10, "B low for red input, got {}", d[2]); + } + + #[test] + fn bgra_channel_order_correct_for_red() { + // BGRA layout: d[0]=B d[1]=G d[2]=R d[3]=A + let (sr, sg, sb) = rgb_to_sigmoidal(255, 0, 0); + let src = make_src(vec![sr, sg, sb, 1.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + sigmoidal_to_bgra(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + assert!(d[0] < 10, "B low for red input in BGRA, got {}", d[0]); + assert!(d[1] < 10, "G low for red input in BGRA, got {}", d[1]); + assert!(d[2] > 200, "R dominant for red input in BGRA, got {}", d[2]); + assert_approx(d[3], 255, 1, "A"); + } + + #[test] + fn rgba_channel_order_correct_for_blue() { + let (sr, sg, sb) = rgb_to_sigmoidal(0, 0, 255); + let src = make_src(vec![sr, sg, sb, 1.0], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + sigmoidal_to_rgba(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + assert!(d[0] < 10, "R low for blue input, got {}", d[0]); + assert!(d[1] < 10, "G low for blue input, got {}", d[1]); + assert!(d[2] > 200, "B dominant for blue input, got {}", d[2]); + } + + // ── rgb and bgra agree on grey ──────────────────────────────────────────── + + #[test] + fn rgb_rgba_agree_on_grey_channels() { + let (sr, sg, sb) = rgb_to_sigmoidal(128, 128, 128); + let src_rgb = make_src(vec![sr, sg, sb], 1, 1, 3); + let src_rgba = make_src(vec![sr, sg, sb, 1.0], 1, 1, 4); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_rgba = make_dst(1, 1, 4); + sigmoidal_to_rgb(&src_rgb, &mut dst_rgb).unwrap(); + sigmoidal_to_rgba(&src_rgba, &mut dst_rgba).unwrap(); + let r = dst_rgb.data.borrow(); + let a = dst_rgba.data.borrow(); + assert_approx(r[0], a[0], 1, "R"); + assert_approx(r[1], a[1], 1, "G"); + assert_approx(r[2], a[2], 1, "B"); + } + + #[test] + fn rgba_bgra_agree_on_grey() { + let (sr, sg, sb) = rgb_to_sigmoidal(180, 180, 180); + let src_rgba = make_src(vec![sr, sg, sb, 0.5], 1, 1, 4); + let src_bgra = make_src(vec![sr, sg, sb, 0.5], 1, 1, 4); + let mut dst_rgba = make_dst(1, 1, 4); + let mut dst_bgra = make_dst(1, 1, 4); + sigmoidal_to_rgba(&src_rgba, &mut dst_rgba).unwrap(); + sigmoidal_to_bgra(&src_bgra, &mut dst_bgra).unwrap(); + assert_eq!( + &*dst_rgba.data.borrow(), + &*dst_bgra.data.borrow(), + "grey must be invariant to channel order" + ); + } + + // ── multi-pixel / multi-row consistency ─────────────────────────────────── + + #[test] + fn rgb_multi_pixel_consistent() { + let (sr, sg, sb) = rgb_to_sigmoidal(100, 150, 200); + let src = make_src(vec![sr, sg, sb].repeat(4), 4, 1, 3); + let mut dst = make_dst(4, 1, 3); + sigmoidal_to_rgb(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2]]; + for (i, chunk) in d.chunks(3).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn rgba_multi_pixel_consistent() { + let (sr, sg, sb) = rgb_to_sigmoidal(80, 120, 200); + let src = make_src(vec![sr, sg, sb, 0.7].repeat(4), 4, 1, 4); + let mut dst = make_dst(4, 1, 4); + sigmoidal_to_rgba(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2], d[3]]; + for (i, chunk) in d.chunks(4).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn rgb_multi_row_consistent() { + let (sr, sg, sb) = rgb_to_sigmoidal(60, 120, 180); + let src = make_src(vec![sr, sg, sb].repeat(4), 2, 2, 3); + let mut dst = make_dst(2, 2, 3); + sigmoidal_to_rgb(&src, &mut dst).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2]]; + for (i, chunk) in d.chunks(3).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn rgb_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0f32; 2], 1, 1, 2, 2).unwrap(); + let mut dst = make_dst(1, 1, 3); + assert!(sigmoidal_to_rgb(&src, &mut dst).is_err()); + } + + #[test] + fn rgba_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0f32; 2], 1, 1, 2, 2).unwrap(); + let mut dst = make_dst(1, 1, 4); + assert!(sigmoidal_to_rgba(&src, &mut dst).is_err()); + } + + #[test] + fn bgra_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0f32; 2], 1, 1, 2, 2).unwrap(); + let mut dst = make_dst(1, 1, 4); + assert!(sigmoidal_to_bgra(&src, &mut dst).is_err()); + } } diff --git a/src/sse/cie.rs b/src/sse/cie.rs index 916214d..6cda738 100644 --- a/src/sse/cie.rs +++ b/src/sse/cie.rs @@ -104,78 +104,76 @@ pub(crate) fn sse_triple_to_lch(x: __m128, y: __m128, z: __m128) -> (__m128, __m #[inline] #[target_feature(enable = "sse4.1")] pub(crate) fn sse_lab_to_xyz(l: __m128, a: __m128, b: __m128) -> (__m128, __m128, __m128) { - unsafe { - let y = _mm_mul_ps( - _mm_add_ps(l, _mm_set1_ps(16f32)), - _mm_set1_ps(1f32 / 116f32), - ); - let x = _mm_add_ps(_mm_mul_ps(a, _mm_set1_ps(1f32 / 500f32)), y); - let z = _mm_sub_ps(y, _mm_mul_ps(b, _mm_set1_ps(1f32 / 200f32))); - let x3 = _mm_cube_ps(x); - let y3 = _mm_cube_ps(y); - let z3 = _mm_cube_ps(z); - let kappa = _mm_set1_ps(0.008856f32); - let k_sub = _mm_set1_ps(16f32 / 116f32); - let mult_1 = _mm_set1_ps(1f32 / 7.787f32); - let low_x = _mm_mul_ps(_mm_sub_ps(x, k_sub), mult_1); - let low_y = _mm_mul_ps(_mm_sub_ps(y, k_sub), mult_1); - let low_z = _mm_mul_ps(_mm_sub_ps(z, k_sub), mult_1); + let y = _mm_mul_ps( + _mm_add_ps(l, _mm_set1_ps(16f32)), + _mm_set1_ps(1f32 / 116f32), + ); + let x = _mm_add_ps(_mm_mul_ps(a, _mm_set1_ps(1f32 / 500f32)), y); + let z = _mm_sub_ps(y, _mm_mul_ps(b, _mm_set1_ps(1f32 / 200f32))); + let x3 = _mm_cube_ps(x); + let y3 = _mm_cube_ps(y); + let z3 = _mm_cube_ps(z); + let kappa = _mm_set1_ps(0.008856f32); + let k_sub = _mm_set1_ps(16f32 / 116f32); + let mult_1 = _mm_set1_ps(1f32 / 7.787f32); + let low_x = _mm_mul_ps(_mm_sub_ps(x, k_sub), mult_1); + let low_y = _mm_mul_ps(_mm_sub_ps(y, k_sub), mult_1); + let low_z = _mm_mul_ps(_mm_sub_ps(z, k_sub), mult_1); - let x = _mm_select_ps(_mm_cmpgt_ps(x3, kappa), x3, low_x); - let y = _mm_select_ps(_mm_cmpgt_ps(y3, kappa), y3, low_y); - let z = _mm_select_ps(_mm_cmpgt_ps(z3, kappa), z3, low_z); - let x = _mm_mul_ps(x, _mm_set1_ps(95.047f32 / 100f32)); - let z = _mm_mul_ps(z, _mm_set1_ps(108.883f32 / 100f32)); - (x, y, z) - } + let x = _mm_select_ps(_mm_cmpgt_ps(x3, kappa), x3, low_x); + let y = _mm_select_ps(_mm_cmpgt_ps(y3, kappa), y3, low_y); + let z = _mm_select_ps(_mm_cmpgt_ps(z3, kappa), z3, low_z); + let x = _mm_mul_ps(x, _mm_set1_ps(95.047f32 / 100f32)); + let z = _mm_mul_ps(z, _mm_set1_ps(108.883f32 / 100f32)); + (x, y, z) } #[inline] #[target_feature(enable = "sse4.1")] pub(crate) fn sse_luv_to_xyz(l: __m128, u: __m128, v: __m128) -> (__m128, __m128, __m128) { - unsafe { - let zeros = _mm_setzero_ps(); - let zero_mask = _mm_cmpeq_ps(l, zeros); - let l13 = _mm_rcp_ps(_mm_mul_ps(l, _mm_set1_ps(13f32))); - let u = _mm_prefer_fma_ps(_mm_set1_ps(LUV_WHITE_U_PRIME), l13, u); - let v = _mm_prefer_fma_ps(_mm_set1_ps(LUV_WHITE_V_PRIME), l13, v); - let l_h = _mm_mul_ps( - _mm_add_ps(l, _mm_set1_ps(16f32)), - _mm_set1_ps(1f32 / 116f32), - ); - let y_high = _mm_mul_ps(_mm_mul_ps(l_h, l_h), l_h); - let y_low = _mm_mul_ps(l, _mm_set1_ps(LUV_MULTIPLIER_INVERSE_Y)); - let y = _mm_select_ps( - zero_mask, - zeros, - _mm_select_ps(_mm_cmpgt_ps(l, _mm_set1_ps(8f32)), y_high, y_low), - ); - let zero_mask_2 = _mm_cmpeq_ps(v, zeros); - let den = _mm_rcp_ps(_mm_mul_ps(v, _mm_set1_ps(4f32))); - let mut x = _mm_mul_ps(_mm_mul_ps(_mm_mul_ps(y, u), den), _mm_set1_ps(9f32)); - x = _mm_select_ps(zero_mask, zeros, x); - x = _mm_select_ps(zero_mask_2, zeros, x); - let mut z = _mm_mul_ps( - _mm_mul_ps( - _mm_prefer_fma_ps( - _mm_prefer_fma_ps(_mm_set1_ps(12f32), _mm_set1_ps(-3f32), u), - v, - _mm_set1_ps(-20f32), - ), - y, + let zeros = _mm_setzero_ps(); + let zero_mask = _mm_cmpeq_ps(l, zeros); + let l13 = _mm_rcp_ps(_mm_mul_ps(l, _mm_set1_ps(13f32))); + let u = _mm_prefer_fma_ps(_mm_set1_ps(LUV_WHITE_U_PRIME), l13, u); + let v = _mm_prefer_fma_ps(_mm_set1_ps(LUV_WHITE_V_PRIME), l13, v); + let l_h = _mm_mul_ps( + _mm_add_ps(l, _mm_set1_ps(16f32)), + _mm_set1_ps(1f32 / 116f32), + ); + let y_high = _mm_mul_ps(_mm_mul_ps(l_h, l_h), l_h); + let y_low = _mm_mul_ps(l, _mm_set1_ps(LUV_MULTIPLIER_INVERSE_Y)); + let y = _mm_select_ps( + zero_mask, + zeros, + _mm_select_ps(_mm_cmpgt_ps(l, _mm_set1_ps(8f32)), y_high, y_low), + ); + let zero_mask_2 = _mm_cmpeq_ps(v, zeros); + let den = _mm_rcp_ps(_mm_mul_ps(v, _mm_set1_ps(4f32))); + let mut x = _mm_mul_ps(_mm_mul_ps(_mm_mul_ps(y, u), den), _mm_set1_ps(9f32)); + x = _mm_select_ps(zero_mask, zeros, x); + x = _mm_select_ps(zero_mask_2, zeros, x); + let mut z = _mm_mul_ps( + _mm_mul_ps( + _mm_prefer_fma_ps( + _mm_prefer_fma_ps(_mm_set1_ps(12f32), _mm_set1_ps(-3f32), u), + v, + _mm_set1_ps(-20f32), ), - den, - ); - z = _mm_select_ps(zero_mask, zeros, z); - z = _mm_select_ps(zero_mask_2, zeros, z); - (x, y, z) - } + y, + ), + den, + ); + z = _mm_select_ps(zero_mask, zeros, z); + z = _mm_select_ps(zero_mask_2, zeros, z); + (x, y, z) } #[inline] #[target_feature(enable = "sse4.1")] -pub(crate) unsafe fn sse_lch_to_xyz(l: __m128, c: __m128, h: __m128) -> (__m128, __m128, __m128) { - let u = _mm_mul_ps(c, _mm_cos_ps(h)); - let v = _mm_mul_ps(c, _mm_sin_ps(h)); - sse_luv_to_xyz(l, u, v) +pub(crate) fn sse_lch_to_xyz(l: __m128, c: __m128, h: __m128) -> (__m128, __m128, __m128) { + unsafe { + let u = _mm_mul_ps(c, _mm_cos_ps(h)); + let v = _mm_mul_ps(c, _mm_sin_ps(h)); + sse_luv_to_xyz(l, u, v) + } } diff --git a/src/sse/color.rs b/src/sse/color.rs index 2bd84db..41beab8 100644 --- a/src/sse/color.rs +++ b/src/sse/color.rs @@ -12,291 +12,303 @@ use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; -#[inline(always)] -pub unsafe fn sse_hsl_to_rgb( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_hsl_to_rgb( h: __m128, s: __m128, l: __m128, scale: __m128, ) -> (__m128i, __m128i, __m128i) { - let s = _mm_mul_ps(s, scale); - let l = _mm_mul_ps(l, scale); - let ones = _mm_set1_ps(1f32); - let twos = _mm_set1_ps(2f32); - let c = _mm_mul_ps( - _mm_sub_ps(ones, _mm_abs_ps(_mm_sub_ps(_mm_mul_ps(l, twos), ones))), - s, - ); - let x = _mm_mul_ps( - _mm_sub_ps( - ones, - _mm_abs_ps(_mm_sub_ps( - _mm_fmod_ps(_mm_mul_ps(h, _mm_set1_ps(1f32 / 60f32)), twos), + unsafe { + let s = _mm_mul_ps(s, scale); + let l = _mm_mul_ps(l, scale); + let ones = _mm_set1_ps(1f32); + let twos = _mm_set1_ps(2f32); + let c = _mm_mul_ps( + _mm_sub_ps(ones, _mm_abs_ps(_mm_sub_ps(_mm_mul_ps(l, twos), ones))), + s, + ); + let x = _mm_mul_ps( + _mm_sub_ps( ones, - )), - ), - c, - ); + _mm_abs_ps(_mm_sub_ps( + _mm_fmod_ps(_mm_mul_ps(h, _mm_set1_ps(1f32 / 60f32)), twos), + ones, + )), + ), + c, + ); - let zeros = _mm_setzero_ps(); - let m = _mm_sub_ps(l, _mm_mul_ps(c, _mm_set1_ps(0.5f32))); - let h_prime = h; - let (mut r, mut g, mut b) = (zeros, zeros, zeros); + let zeros = _mm_setzero_ps(); + let m = _mm_sub_ps(l, _mm_mul_ps(c, _mm_set1_ps(0.5f32))); + let h_prime = h; + let (mut r, mut g, mut b) = (zeros, zeros, zeros); - let between_zero_and_one_mask = _mm_and_ps( - _mm_cmpge_ps(h, zeros), - _mm_cmplt_ps(h_prime, _mm_set1_ps(60f32)), - ); - let between_one_and_two_mask = _mm_and_ps( - _mm_cmpge_ps(h_prime, _mm_set1_ps(60f32)), - _mm_cmplt_ps(h_prime, _mm_set1_ps(120f32)), - ); - let between_two_and_three_mask = _mm_and_ps( - _mm_cmpge_ps(h_prime, _mm_set1_ps(120f32)), - _mm_cmplt_ps(h_prime, _mm_set1_ps(180f32)), - ); - let between_three_and_four_mask = _mm_and_ps( - _mm_cmpge_ps(h_prime, _mm_set1_ps(180f32)), - _mm_cmplt_ps(h_prime, _mm_set1_ps(240f32)), - ); - let between_four_and_five_mask = _mm_and_ps( - _mm_cmpge_ps(h_prime, _mm_set1_ps(240f32)), - _mm_cmplt_ps(h_prime, _mm_set1_ps(300f32)), - ); - let between_five_and_six_mask = _mm_and_ps( - _mm_cmpge_ps(h_prime, _mm_set1_ps(300f32)), - _mm_cmplt_ps(h_prime, _mm_set1_ps(360f32)), - ); - // if h_prime >= 0f32 && h_prime < 1f32 { - r = _mm_select_ps(between_zero_and_one_mask, c, r); - g = _mm_select_ps(between_zero_and_one_mask, x, g); - // if h_prime >= 1f32 && h_prime < 2f32 { - r = _mm_select_ps(between_one_and_two_mask, x, r); - g = _mm_select_ps(between_one_and_two_mask, c, g); - // if h_prime >= 2f32 && h_prime < 3f32 - g = _mm_select_ps(between_two_and_three_mask, c, g); - b = _mm_select_ps(between_two_and_three_mask, x, b); - // if h_prime >= 3f32 && h_prime < 4f32 { - g = _mm_select_ps(between_three_and_four_mask, x, g); - b = _mm_select_ps(between_three_and_four_mask, c, b); - // if h_prime >= 4f32 && h_prime < 5f32 { - r = _mm_select_ps(between_four_and_five_mask, x, r); - b = _mm_select_ps(between_four_and_five_mask, c, b); - // if h_prime >= 5f32 && h_prime < 6f32 { - r = _mm_select_ps(between_five_and_six_mask, c, r); - b = _mm_select_ps(between_five_and_six_mask, x, b); - r = _mm_add_ps(r, m); - g = _mm_add_ps(g, m); - b = _mm_add_ps(b, m); - let rgb_scale = _mm_set1_ps(255f32); - r = _mm_mul_ps(r, rgb_scale); - g = _mm_mul_ps(g, rgb_scale); - b = _mm_mul_ps(b, rgb_scale); - const ROUNDING_FLAGS: i32 = _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC; - ( - _mm_cvtps_epi32(_mm_round_ps::(r)), - _mm_cvtps_epi32(_mm_round_ps::(g)), - _mm_cvtps_epi32(_mm_round_ps::(b)), - ) + let between_zero_and_one_mask = _mm_and_ps( + _mm_cmpge_ps(h, zeros), + _mm_cmplt_ps(h_prime, _mm_set1_ps(60f32)), + ); + let between_one_and_two_mask = _mm_and_ps( + _mm_cmpge_ps(h_prime, _mm_set1_ps(60f32)), + _mm_cmplt_ps(h_prime, _mm_set1_ps(120f32)), + ); + let between_two_and_three_mask = _mm_and_ps( + _mm_cmpge_ps(h_prime, _mm_set1_ps(120f32)), + _mm_cmplt_ps(h_prime, _mm_set1_ps(180f32)), + ); + let between_three_and_four_mask = _mm_and_ps( + _mm_cmpge_ps(h_prime, _mm_set1_ps(180f32)), + _mm_cmplt_ps(h_prime, _mm_set1_ps(240f32)), + ); + let between_four_and_five_mask = _mm_and_ps( + _mm_cmpge_ps(h_prime, _mm_set1_ps(240f32)), + _mm_cmplt_ps(h_prime, _mm_set1_ps(300f32)), + ); + let between_five_and_six_mask = _mm_and_ps( + _mm_cmpge_ps(h_prime, _mm_set1_ps(300f32)), + _mm_cmplt_ps(h_prime, _mm_set1_ps(360f32)), + ); + // if h_prime >= 0f32 && h_prime < 1f32 { + r = _mm_select_ps(between_zero_and_one_mask, c, r); + g = _mm_select_ps(between_zero_and_one_mask, x, g); + // if h_prime >= 1f32 && h_prime < 2f32 { + r = _mm_select_ps(between_one_and_two_mask, x, r); + g = _mm_select_ps(between_one_and_two_mask, c, g); + // if h_prime >= 2f32 && h_prime < 3f32 + g = _mm_select_ps(between_two_and_three_mask, c, g); + b = _mm_select_ps(between_two_and_three_mask, x, b); + // if h_prime >= 3f32 && h_prime < 4f32 { + g = _mm_select_ps(between_three_and_four_mask, x, g); + b = _mm_select_ps(between_three_and_four_mask, c, b); + // if h_prime >= 4f32 && h_prime < 5f32 { + r = _mm_select_ps(between_four_and_five_mask, x, r); + b = _mm_select_ps(between_four_and_five_mask, c, b); + // if h_prime >= 5f32 && h_prime < 6f32 { + r = _mm_select_ps(between_five_and_six_mask, c, r); + b = _mm_select_ps(between_five_and_six_mask, x, b); + r = _mm_add_ps(r, m); + g = _mm_add_ps(g, m); + b = _mm_add_ps(b, m); + let rgb_scale = _mm_set1_ps(255f32); + r = _mm_mul_ps(r, rgb_scale); + g = _mm_mul_ps(g, rgb_scale); + b = _mm_mul_ps(b, rgb_scale); + const ROUNDING_FLAGS: i32 = _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC; + ( + _mm_cvtps_epi32(_mm_round_ps::(r)), + _mm_cvtps_epi32(_mm_round_ps::(g)), + _mm_cvtps_epi32(_mm_round_ps::(b)), + ) + } } -#[inline(always)] -pub unsafe fn sse_hsv_to_rgb( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_hsv_to_rgb( h: __m128, s: __m128, v: __m128, scale: __m128, ) -> (__m128i, __m128i, __m128i) { - let s = _mm_mul_ps(s, scale); - let v = _mm_mul_ps(v, scale); - let c = _mm_mul_ps(s, v); - let h_der = _mm_mul_ps(h, _mm_set1_ps(1f32 / 60f32)); - let six = _mm_set1_ps(6f32); - let h_prime = _mm_fmod_ps(h_der, six); - let ones = _mm_set1_ps(1f32); - let x = _mm_mul_ps( - _mm_sub_ps( - ones, - _mm_abs_ps(_mm_sub_ps(_mm_fmod_ps(h_prime, _mm_set1_ps(2f32)), ones)), - ), - c, - ); - let zeros = _mm_setzero_ps(); - let m = _mm_sub_ps(v, c); - let (mut r, mut g, mut b) = (zeros, zeros, zeros); - let between_zero_and_one_mask = - _mm_and_ps(_mm_cmpge_ps(h_prime, zeros), _mm_cmplt_ps(h_prime, ones)); - let twos = _mm_set1_ps(2f32); - let between_one_and_two_mask = - _mm_and_ps(_mm_cmpge_ps(h_prime, ones), _mm_cmplt_ps(h_prime, twos)); - let threes = _mm_set1_ps(3f32); - let between_two_and_three_mask = - _mm_and_ps(_mm_cmpge_ps(h_prime, twos), _mm_cmplt_ps(h_prime, threes)); - let fours = _mm_set1_ps(4f32); - let between_three_and_four_mask = - _mm_and_ps(_mm_cmpge_ps(h_prime, threes), _mm_cmplt_ps(h_prime, fours)); - let fives = _mm_set1_ps(5f32); - let between_four_and_five_mask = - _mm_and_ps(_mm_cmpge_ps(h_prime, fours), _mm_cmplt_ps(h_prime, fives)); - let between_five_and_six_mask = - _mm_and_ps(_mm_cmpge_ps(h_prime, fives), _mm_cmplt_ps(h_prime, six)); - // if h_prime >= 0f32 && h_prime < 1f32 { - r = _mm_select_ps(between_zero_and_one_mask, c, r); - g = _mm_select_ps(between_zero_and_one_mask, x, g); - // if h_prime >= 1f32 && h_prime < 2f32 { - r = _mm_select_ps(between_one_and_two_mask, x, r); - g = _mm_select_ps(between_one_and_two_mask, c, g); - // if h_prime >= 2f32 && h_prime < 3f32 - g = _mm_select_ps(between_two_and_three_mask, c, g); - b = _mm_select_ps(between_two_and_three_mask, x, b); - // if h_prime >= 3f32 && h_prime < 4f32 { - g = _mm_select_ps(between_three_and_four_mask, x, g); - b = _mm_select_ps(between_three_and_four_mask, c, b); - // if h_prime >= 4f32 && h_prime < 5f32 { - r = _mm_select_ps(between_four_and_five_mask, x, r); - b = _mm_select_ps(between_four_and_five_mask, c, b); - // if h_prime >= 5f32 && h_prime < 6f32 { - r = _mm_select_ps(between_five_and_six_mask, c, r); - b = _mm_select_ps(between_five_and_six_mask, x, b); - r = _mm_add_ps(r, m); - g = _mm_add_ps(g, m); - b = _mm_add_ps(b, m); - let rgb_scale = _mm_set1_ps(255f32); - r = _mm_mul_ps(r, rgb_scale); - g = _mm_mul_ps(g, rgb_scale); - b = _mm_mul_ps(b, rgb_scale); - const ROUNDING_FLAGS: i32 = _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC; - ( - _mm_cvtps_epi32(_mm_round_ps::(r)), - _mm_cvtps_epi32(_mm_round_ps::(g)), - _mm_cvtps_epi32(_mm_round_ps::(b)), - ) + unsafe { + let s = _mm_mul_ps(s, scale); + let v = _mm_mul_ps(v, scale); + let c = _mm_mul_ps(s, v); + let h_der = _mm_mul_ps(h, _mm_set1_ps(1f32 / 60f32)); + let six = _mm_set1_ps(6f32); + let h_prime = _mm_fmod_ps(h_der, six); + let ones = _mm_set1_ps(1f32); + let x = _mm_mul_ps( + _mm_sub_ps( + ones, + _mm_abs_ps(_mm_sub_ps(_mm_fmod_ps(h_prime, _mm_set1_ps(2f32)), ones)), + ), + c, + ); + let zeros = _mm_setzero_ps(); + let m = _mm_sub_ps(v, c); + let (mut r, mut g, mut b) = (zeros, zeros, zeros); + let between_zero_and_one_mask = + _mm_and_ps(_mm_cmpge_ps(h_prime, zeros), _mm_cmplt_ps(h_prime, ones)); + let twos = _mm_set1_ps(2f32); + let between_one_and_two_mask = + _mm_and_ps(_mm_cmpge_ps(h_prime, ones), _mm_cmplt_ps(h_prime, twos)); + let threes = _mm_set1_ps(3f32); + let between_two_and_three_mask = + _mm_and_ps(_mm_cmpge_ps(h_prime, twos), _mm_cmplt_ps(h_prime, threes)); + let fours = _mm_set1_ps(4f32); + let between_three_and_four_mask = + _mm_and_ps(_mm_cmpge_ps(h_prime, threes), _mm_cmplt_ps(h_prime, fours)); + let fives = _mm_set1_ps(5f32); + let between_four_and_five_mask = + _mm_and_ps(_mm_cmpge_ps(h_prime, fours), _mm_cmplt_ps(h_prime, fives)); + let between_five_and_six_mask = + _mm_and_ps(_mm_cmpge_ps(h_prime, fives), _mm_cmplt_ps(h_prime, six)); + // if h_prime >= 0f32 && h_prime < 1f32 { + r = _mm_select_ps(between_zero_and_one_mask, c, r); + g = _mm_select_ps(between_zero_and_one_mask, x, g); + // if h_prime >= 1f32 && h_prime < 2f32 { + r = _mm_select_ps(between_one_and_two_mask, x, r); + g = _mm_select_ps(between_one_and_two_mask, c, g); + // if h_prime >= 2f32 && h_prime < 3f32 + g = _mm_select_ps(between_two_and_three_mask, c, g); + b = _mm_select_ps(between_two_and_three_mask, x, b); + // if h_prime >= 3f32 && h_prime < 4f32 { + g = _mm_select_ps(between_three_and_four_mask, x, g); + b = _mm_select_ps(between_three_and_four_mask, c, b); + // if h_prime >= 4f32 && h_prime < 5f32 { + r = _mm_select_ps(between_four_and_five_mask, x, r); + b = _mm_select_ps(between_four_and_five_mask, c, b); + // if h_prime >= 5f32 && h_prime < 6f32 { + r = _mm_select_ps(between_five_and_six_mask, c, r); + b = _mm_select_ps(between_five_and_six_mask, x, b); + r = _mm_add_ps(r, m); + g = _mm_add_ps(g, m); + b = _mm_add_ps(b, m); + let rgb_scale = _mm_set1_ps(255f32); + r = _mm_mul_ps(r, rgb_scale); + g = _mm_mul_ps(g, rgb_scale); + b = _mm_mul_ps(b, rgb_scale); + const ROUNDING_FLAGS: i32 = _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC; + ( + _mm_cvtps_epi32(_mm_round_ps::(r)), + _mm_cvtps_epi32(_mm_round_ps::(g)), + _mm_cvtps_epi32(_mm_round_ps::(b)), + ) + } } -#[inline(always)] -pub unsafe fn sse_rgb_to_hsv( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_rgb_to_hsv( r: __m128i, g: __m128i, b: __m128i, scale: __m128, ) -> (__m128, __m128, __m128) { - let rgb_scale = _mm_set1_ps(1f32 / 255f32); - let r = _mm_mul_ps(_mm_cvtepi32_ps(r), rgb_scale); - let g = _mm_mul_ps(_mm_cvtepi32_ps(g), rgb_scale); - let b = _mm_mul_ps(_mm_cvtepi32_ps(b), rgb_scale); - let c_max = _mm_max_ps(_mm_max_ps(r, g), b); - let c_min = _mm_min_ps(_mm_min_ps(r, g), b); - let delta = _mm_sub_ps(c_max, c_min); - let rcp_delta = _mm_rcp_ps(delta); - let is_r_max = _mm_cmpeq_ps(c_max, r); - let is_g_max = _mm_cmpeq_ps(c_max, g); - let is_b_max = _mm_cmpeq_ps(c_max, b); - let immediate_zero_flag = _mm_cmpeq_ps(delta, _mm_setzero_ps()); - let mut h = _mm_setzero_ps(); - let v_six = _mm_set1_ps(60f32); - h = _mm_select_ps( - is_r_max, - _mm_mul_ps( - _mm_fmod_ps(_mm_mul_ps(_mm_sub_ps(g, b), rcp_delta), _mm_set1_ps(6f32)), - v_six, - ), - h, - ); - let adding_2 = _mm_set1_ps(2f32); - h = _mm_select_ps( - is_g_max, - _mm_mul_ps( - _mm_add_ps(_mm_mul_ps(_mm_sub_ps(b, r), rcp_delta), adding_2), - v_six, - ), - h, - ); - let adding_4 = _mm_set1_ps(4f32); - h = _mm_select_ps( - is_b_max, - _mm_mul_ps( - _mm_add_ps(_mm_mul_ps(_mm_sub_ps(r, g), rcp_delta), adding_4), - v_six, - ), - h, - ); - let zeros = _mm_setzero_ps(); - h = _mm_select_ps(immediate_zero_flag, zeros, h); - let s = _mm_select_ps( - _mm_cmpeq_ps(c_max, zeros), - zeros, - _mm_mul_ps(delta, _mm_rcp_ps(c_max)), - ); - h = _mm_select_ps( - _mm_cmplt_ps(h, zeros), - _mm_add_ps(h, _mm_set1_ps(360f32)), - h, - ); - let v = c_max; - (h, _mm_mul_ps(s, scale), _mm_mul_ps(v, scale)) + unsafe { + let rgb_scale = _mm_set1_ps(1f32 / 255f32); + let r = _mm_mul_ps(_mm_cvtepi32_ps(r), rgb_scale); + let g = _mm_mul_ps(_mm_cvtepi32_ps(g), rgb_scale); + let b = _mm_mul_ps(_mm_cvtepi32_ps(b), rgb_scale); + let c_max = _mm_max_ps(_mm_max_ps(r, g), b); + let c_min = _mm_min_ps(_mm_min_ps(r, g), b); + let delta = _mm_sub_ps(c_max, c_min); + let rcp_delta = _mm_rcp_ps(delta); + let is_r_max = _mm_cmpeq_ps(c_max, r); + let is_g_max = _mm_cmpeq_ps(c_max, g); + let is_b_max = _mm_cmpeq_ps(c_max, b); + let immediate_zero_flag = _mm_cmpeq_ps(delta, _mm_setzero_ps()); + let mut h = _mm_setzero_ps(); + let v_six = _mm_set1_ps(60f32); + h = _mm_select_ps( + is_r_max, + _mm_mul_ps( + _mm_fmod_ps(_mm_mul_ps(_mm_sub_ps(g, b), rcp_delta), _mm_set1_ps(6f32)), + v_six, + ), + h, + ); + let adding_2 = _mm_set1_ps(2f32); + h = _mm_select_ps( + is_g_max, + _mm_mul_ps( + _mm_add_ps(_mm_mul_ps(_mm_sub_ps(b, r), rcp_delta), adding_2), + v_six, + ), + h, + ); + let adding_4 = _mm_set1_ps(4f32); + h = _mm_select_ps( + is_b_max, + _mm_mul_ps( + _mm_add_ps(_mm_mul_ps(_mm_sub_ps(r, g), rcp_delta), adding_4), + v_six, + ), + h, + ); + let zeros = _mm_setzero_ps(); + h = _mm_select_ps(immediate_zero_flag, zeros, h); + let s = _mm_select_ps( + _mm_cmpeq_ps(c_max, zeros), + zeros, + _mm_mul_ps(delta, _mm_rcp_ps(c_max)), + ); + h = _mm_select_ps( + _mm_cmplt_ps(h, zeros), + _mm_add_ps(h, _mm_set1_ps(360f32)), + h, + ); + let v = c_max; + (h, _mm_mul_ps(s, scale), _mm_mul_ps(v, scale)) + } } -#[inline(always)] -pub unsafe fn sse_rgb_to_hsl( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_rgb_to_hsl( r: __m128i, g: __m128i, b: __m128i, scale: __m128, ) -> (__m128, __m128, __m128) { - let rgb_scale = _mm_set1_ps(1f32 / 255f32); - let r = _mm_mul_ps(_mm_cvtepi32_ps(r), rgb_scale); - let g = _mm_mul_ps(_mm_cvtepi32_ps(g), rgb_scale); - let b = _mm_mul_ps(_mm_cvtepi32_ps(b), rgb_scale); - let c_max = _mm_max_ps(_mm_max_ps(r, g), b); - let c_min = _mm_min_ps(_mm_min_ps(r, g), b); - let delta = _mm_sub_ps(c_max, c_min); - let rcp_delta = _mm_rcp_ps(delta); - let is_r_max = _mm_cmpeq_ps(c_max, r); - let is_g_max = _mm_cmpeq_ps(c_max, g); - let is_b_max = _mm_cmpeq_ps(c_max, b); - let zeros = _mm_setzero_ps(); - let immediate_zero_flag = _mm_cmpeq_ps(delta, zeros); - let v_six = _mm_set1_ps(60f32); - let mut h = _mm_setzero_ps(); - h = _mm_select_ps( - is_r_max, - _mm_mul_ps( - _mm_fmod_ps(_mm_mul_ps(_mm_sub_ps(g, b), rcp_delta), _mm_set1_ps(6f32)), - v_six, - ), - h, - ); - let adding_2 = _mm_set1_ps(2f32); - h = _mm_select_ps( - is_g_max, - _mm_mul_ps( - _mm_add_ps(_mm_mul_ps(_mm_sub_ps(b, r), rcp_delta), adding_2), - v_six, - ), - h, - ); - let adding_4 = _mm_set1_ps(4f32); - h = _mm_select_ps( - is_b_max, - _mm_mul_ps( - _mm_add_ps(_mm_mul_ps(_mm_sub_ps(r, g), rcp_delta), adding_4), - v_six, - ), - h, - ); - h = _mm_select_ps(immediate_zero_flag, zeros, h); - h = _mm_select_ps( - _mm_cmplt_ps(h, zeros), - _mm_add_ps(h, _mm_set1_ps(360f32)), - h, - ); - let l = _mm_mul_ps(_mm_add_ps(c_max, c_min), _mm_set1_ps(0.5f32)); - let s = _mm_div_ps( - delta, - _mm_sub_ps( - _mm_set1_ps(1f32), - _mm_abs_ps(_mm_prefer_fma_ps(_mm_set1_ps(-1f32), _mm_set1_ps(2f32), l)), - ), - ); - (h, _mm_mul_ps(s, scale), _mm_mul_ps(l, scale)) + unsafe { + let rgb_scale = _mm_set1_ps(1f32 / 255f32); + let r = _mm_mul_ps(_mm_cvtepi32_ps(r), rgb_scale); + let g = _mm_mul_ps(_mm_cvtepi32_ps(g), rgb_scale); + let b = _mm_mul_ps(_mm_cvtepi32_ps(b), rgb_scale); + let c_max = _mm_max_ps(_mm_max_ps(r, g), b); + let c_min = _mm_min_ps(_mm_min_ps(r, g), b); + let delta = _mm_sub_ps(c_max, c_min); + let rcp_delta = _mm_rcp_ps(delta); + let is_r_max = _mm_cmpeq_ps(c_max, r); + let is_g_max = _mm_cmpeq_ps(c_max, g); + let is_b_max = _mm_cmpeq_ps(c_max, b); + let zeros = _mm_setzero_ps(); + let immediate_zero_flag = _mm_cmpeq_ps(delta, zeros); + let v_six = _mm_set1_ps(60f32); + let mut h = _mm_setzero_ps(); + h = _mm_select_ps( + is_r_max, + _mm_mul_ps( + _mm_fmod_ps(_mm_mul_ps(_mm_sub_ps(g, b), rcp_delta), _mm_set1_ps(6f32)), + v_six, + ), + h, + ); + let adding_2 = _mm_set1_ps(2f32); + h = _mm_select_ps( + is_g_max, + _mm_mul_ps( + _mm_add_ps(_mm_mul_ps(_mm_sub_ps(b, r), rcp_delta), adding_2), + v_six, + ), + h, + ); + let adding_4 = _mm_set1_ps(4f32); + h = _mm_select_ps( + is_b_max, + _mm_mul_ps( + _mm_add_ps(_mm_mul_ps(_mm_sub_ps(r, g), rcp_delta), adding_4), + v_six, + ), + h, + ); + h = _mm_select_ps(immediate_zero_flag, zeros, h); + h = _mm_select_ps( + _mm_cmplt_ps(h, zeros), + _mm_add_ps(h, _mm_set1_ps(360f32)), + h, + ); + let l = _mm_mul_ps(_mm_add_ps(c_max, c_min), _mm_set1_ps(0.5f32)); + let s = _mm_div_ps( + delta, + _mm_sub_ps( + _mm_set1_ps(1f32), + _mm_abs_ps(_mm_prefer_fma_ps(_mm_set1_ps(-1f32), _mm_set1_ps(2f32), l)), + ), + ); + (h, _mm_mul_ps(s, scale), _mm_mul_ps(l, scale)) + } } diff --git a/src/sse/from_sigmoidal.rs b/src/sse/from_sigmoidal.rs index 0f26952..d26bf98 100644 --- a/src/sse/from_sigmoidal.rs +++ b/src/sse/from_sigmoidal.rs @@ -17,41 +17,44 @@ use crate::sse::{ }; use crate::{store_and_interleave_v3_u8, store_and_interleave_v4_u8}; -#[inline(always)] -unsafe fn vld_sigmoidal( +#[inline] +#[target_feature(enable = "sse4.1")] +fn vld_sigmoidal( src: *const f32, ) -> (__m128i, __m128i, __m128i, __m128i) { - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let v_scale_color = _mm_set1_ps(255f32); - let pixel_0 = _mm_loadu_ps(src); - let pixel_1 = _mm_loadu_ps(src.add(4)); - let pixel_2 = _mm_loadu_ps(src.add(8)); - if image_configuration.has_alpha() { - let pixel_3 = _mm_loadu_ps(src.add(12)); - let (sr, sg, sb, sa) = sse_deinterleave_rgba_ps(pixel_0, pixel_1, pixel_2, pixel_3); - - let (r, g, b) = sse_sigmoidal_to_rgb(sr, sg, sb); - const ROUNDING_FLAGS: i32 = _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC; - let a_f32 = _mm_mul_ps(sa, v_scale_color); - ( - r, - g, - b, - _mm_cvtps_epi32(_mm_round_ps::(a_f32)), - ) - } else { - let (sr, sg, sb) = sse_deinterleave_rgb_ps(pixel_0, pixel_1, pixel_2); - - let (r, g, b) = sse_sigmoidal_to_rgb(sr, sg, sb); - (r, g, b, _mm_setzero_si128()) + unsafe { + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + let v_scale_color = _mm_set1_ps(255f32); + let pixel_0 = _mm_loadu_ps(src); + let pixel_1 = _mm_loadu_ps(src.add(4)); + let pixel_2 = _mm_loadu_ps(src.add(8)); + if image_configuration.has_alpha() { + let pixel_3 = _mm_loadu_ps(src.add(12)); + let (sr, sg, sb, sa) = sse_deinterleave_rgba_ps(pixel_0, pixel_1, pixel_2, pixel_3); + + let (r, g, b) = sse_sigmoidal_to_rgb(sr, sg, sb); + const ROUNDING_FLAGS: i32 = _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC; + let a_f32 = _mm_mul_ps(sa, v_scale_color); + ( + r, + g, + b, + _mm_cvtps_epi32(_mm_round_ps::(a_f32)), + ) + } else { + let (sr, sg, sb) = sse_deinterleave_rgb_ps(pixel_0, pixel_1, pixel_2); + + let (r, g, b) = sse_sigmoidal_to_rgb(sr, sg, sb); + (r, g, b, _mm_setzero_si128()) + } } } #[target_feature(enable = "sse4.1")] -pub unsafe fn sse_from_sigmoidal_row( +pub(crate) unsafe fn sse_from_sigmoidal_row( start_cx: usize, - src: *const f32, - dst: *mut u8, + src: &[f32], + dst: &mut [u8], width: u32, ) -> usize { let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); @@ -60,72 +63,74 @@ pub unsafe fn sse_from_sigmoidal_row( let mut cx = start_cx; - while cx + 16 < width as usize { - let offset_src_ptr = src.add(cx * channels); - - let src_ptr_0 = offset_src_ptr; - - let (r_row0_, g_row0_, b_row0_, a_row0_) = - vld_sigmoidal::(src_ptr_0); - - let src_ptr_1 = offset_src_ptr.add(4 * channels); - - let (r_row1_, g_row1_, b_row1_, a_row1_) = - vld_sigmoidal::(src_ptr_1); - - let src_ptr_2 = offset_src_ptr.add(4 * 2 * channels); - - let (r_row2_, g_row2_, b_row2_, a_row2_) = - vld_sigmoidal::(src_ptr_2); - - let src_ptr_3 = offset_src_ptr.add(4 * 3 * channels); - - let (r_row3_, g_row3_, b_row3_, a_row3_) = - vld_sigmoidal::(src_ptr_3); - - let r_row01 = _mm_packs_epi32(r_row0_, r_row1_); - let g_row01 = _mm_packs_epi32(g_row0_, g_row1_); - let b_row01 = _mm_packs_epi32(b_row0_, b_row1_); - let a_row01 = _mm_packs_epi32(a_row0_, a_row1_); - - let r_row23 = _mm_packs_epi32(r_row2_, r_row3_); - let g_row23 = _mm_packs_epi32(g_row2_, g_row3_); - let b_row23 = _mm_packs_epi32(b_row2_, b_row3_); - let a_row23 = _mm_packs_epi32(a_row2_, a_row3_); - - let r_row = _mm_packus_epi16(r_row01, r_row23); - let g_row = _mm_packus_epi16(g_row01, g_row23); - let b_row = _mm_packus_epi16(b_row01, b_row23); - let a_row = _mm_packus_epi16(a_row01, a_row23); - - let dst_ptr = dst.add(cx * channels); - - match image_configuration { - ImageConfiguration::Rgb => { - store_and_interleave_v3_u8!(dst_ptr, image_configuration, r_row, g_row, b_row); - } - ImageConfiguration::Rgba => { - store_and_interleave_v4_u8!( - dst_ptr, - image_configuration, - r_row, - g_row, - b_row, - a_row - ); - } - ImageConfiguration::Bgra => { - store_and_interleave_v4_u8!( - dst_ptr, - image_configuration, - b_row, - g_row, - r_row, - a_row - ); - } - ImageConfiguration::Bgr => { - store_and_interleave_v3_u8!(dst_ptr, image_configuration, b_row, g_row, r_row); + while cx + 16 <= width as usize { + unsafe { + let offset_src_ptr = src.get_unchecked(cx * channels..).as_ptr(); + + let src_ptr_0 = offset_src_ptr; + + let (r_row0_, g_row0_, b_row0_, a_row0_) = + vld_sigmoidal::(src_ptr_0); + + let src_ptr_1 = offset_src_ptr.add(4 * channels); + + let (r_row1_, g_row1_, b_row1_, a_row1_) = + vld_sigmoidal::(src_ptr_1); + + let src_ptr_2 = offset_src_ptr.add(4 * 2 * channels); + + let (r_row2_, g_row2_, b_row2_, a_row2_) = + vld_sigmoidal::(src_ptr_2); + + let src_ptr_3 = offset_src_ptr.add(4 * 3 * channels); + + let (r_row3_, g_row3_, b_row3_, a_row3_) = + vld_sigmoidal::(src_ptr_3); + + let r_row01 = _mm_packs_epi32(r_row0_, r_row1_); + let g_row01 = _mm_packs_epi32(g_row0_, g_row1_); + let b_row01 = _mm_packs_epi32(b_row0_, b_row1_); + let a_row01 = _mm_packs_epi32(a_row0_, a_row1_); + + let r_row23 = _mm_packs_epi32(r_row2_, r_row3_); + let g_row23 = _mm_packs_epi32(g_row2_, g_row3_); + let b_row23 = _mm_packs_epi32(b_row2_, b_row3_); + let a_row23 = _mm_packs_epi32(a_row2_, a_row3_); + + let r_row = _mm_packus_epi16(r_row01, r_row23); + let g_row = _mm_packus_epi16(g_row01, g_row23); + let b_row = _mm_packus_epi16(b_row01, b_row23); + let a_row = _mm_packus_epi16(a_row01, a_row23); + + let dst_ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); + + match image_configuration { + ImageConfiguration::Rgb => { + store_and_interleave_v3_u8!(dst_ptr, image_configuration, r_row, g_row, b_row); + } + ImageConfiguration::Rgba => { + store_and_interleave_v4_u8!( + dst_ptr, + image_configuration, + r_row, + g_row, + b_row, + a_row + ); + } + ImageConfiguration::Bgra => { + store_and_interleave_v4_u8!( + dst_ptr, + image_configuration, + b_row, + g_row, + r_row, + a_row + ); + } + ImageConfiguration::Bgr => { + store_and_interleave_v3_u8!(dst_ptr, image_configuration, b_row, g_row, r_row); + } } } cx += 16; diff --git a/src/sse/gamma_curves.rs b/src/sse/gamma_curves.rs deleted file mode 100644 index 67e8ee7..0000000 --- a/src/sse/gamma_curves.rs +++ /dev/null @@ -1,145 +0,0 @@ -/* - * // Copyright 2024 (c) the Radzivon Bartoshyk. All rights reserved. - * // - * // Use of this source code is governed by a BSD-style - * // license that can be found in the LICENSE file. - */ -#![allow(dead_code)] -use crate::sse::*; -use erydanos::_mm_pow_ps; -#[cfg(target_arch = "x86")] -use std::arch::x86::*; -#[cfg(target_arch = "x86_64")] -use std::arch::x86_64::*; - -#[inline(always)] -pub unsafe fn sse_srgb_from_linear(linear: __m128) -> __m128 { - let linear = _mm_max_ps(linear, _mm_setzero_ps()); - let linear = _mm_min_ps(linear, _mm_set1_ps(1f32)); - let low_cut_off = _mm_set1_ps(0.0030412825601275209f32); - let mask = _mm_cmpge_ps(linear, low_cut_off); - - let mut low = linear; - let mut high = linear; - low = _mm_mul_ps(low, _mm_set1_ps(12.92f32)); - - high = _mm_sub_ps( - _mm_mul_ps( - _mm_pow_n_ps(high, 1.0f32 / 2.4f32), - _mm_set1_ps(1.0550107189475866f32), - ), - _mm_set1_ps(0.0550107189475866f32), - ); - _mm_select_ps(mask, high, low) -} - -#[inline(always)] -pub unsafe fn sse_srgb_to_linear(gamma: __m128) -> __m128 { - let gamma = _mm_max_ps(gamma, _mm_setzero_ps()); - let gamma = _mm_min_ps(gamma, _mm_set1_ps(1f32)); - let low_cut_off = _mm_set1_ps(12.92f32 * 0.0030412825601275209f32); - let mask = _mm_cmpge_ps(gamma, low_cut_off); - - let mut low = gamma; - let high = _mm_pow_n_ps( - _mm_mul_ps( - _mm_add_ps(gamma, _mm_set1_ps(0.0550107189475866f32)), - _mm_set1_ps(1f32 / 1.0550107189475866f32), - ), - 2.4f32, - ); - low = _mm_mul_ps(low, _mm_set1_ps(1f32 / 12.92f32)); - _mm_select_ps(mask, high, low) -} - -#[inline(always)] -pub unsafe fn sse_rec709_from_linear(linear: __m128) -> __m128 { - let linear = _mm_max_ps(linear, _mm_setzero_ps()); - let linear = _mm_min_ps(linear, _mm_set1_ps(1f32)); - let low_cut_off = _mm_set1_ps(0.018053968510807f32); - let mask = _mm_cmpge_ps(linear, low_cut_off); - - let mut low = linear; - let mut high = linear; - low = _mm_mul_ps(low, _mm_set1_ps(4.5f32)); - - high = _mm_sub_ps( - _mm_mul_ps( - _mm_pow_n_ps(high, 0.45f32), - _mm_set1_ps(1.09929682680944f32), - ), - _mm_set1_ps(0.09929682680944f32), - ); - _mm_select_ps(mask, high, low) -} - -#[inline(always)] -pub unsafe fn sse_rec709_to_linear(gamma: __m128) -> __m128 { - let gamma = _mm_max_ps(gamma, _mm_setzero_ps()); - let gamma = _mm_min_ps(gamma, _mm_set1_ps(1f32)); - let low_cut_off = _mm_set1_ps(4.5f32 * 0.018053968510807f32); - let mask = _mm_cmpge_ps(gamma, low_cut_off); - - let mut low = gamma; - let high = _mm_pow_n_ps( - _mm_mul_ps( - _mm_add_ps(gamma, _mm_set1_ps(0.09929682680944f32)), - _mm_set1_ps(1f32 / 1.09929682680944f32), - ), - 1.0f32 / 0.45f32, - ); - low = _mm_mul_ps(low, _mm_set1_ps(1f32 / 4.5f32)); - _mm_select_ps(mask, high, low) -} - -#[inline(always)] -pub unsafe fn sse_pure_gamma(gamma: __m128, value: f32) -> __m128 { - let zeros = _mm_setzero_ps(); - let zero_mask = _mm_cmple_ps(gamma, zeros); - let ones = _mm_set1_ps(1f32); - let ones_mask = _mm_cmpge_ps(gamma, ones); - let mut rs = _mm_pow_n_ps(gamma, value); - rs = _mm_select_ps(zero_mask, zeros, rs); - _mm_select_ps(ones_mask, ones, rs) -} - -#[inline(always)] -pub unsafe fn sse_smpte428_from_linear(linear: __m128) -> __m128 { - const POWER_VALUE: f32 = 1.0f32 / 2.6f32; - _mm_pow_ps( - _mm_mul_ps( - _mm_max_ps(linear, _mm_setzero_ps()), - _mm_set1_ps(0.91655527974030934f32), - ), - _mm_set1_ps(POWER_VALUE), - ) -} - -#[inline(always)] -pub unsafe fn sse_smpte428_to_linear(gamma: __m128) -> __m128 { - const SCALE: f32 = 1. / 0.91655527974030934f32; - _mm_mul_ps( - _mm_pow_ps(_mm_max_ps(gamma, _mm_setzero_ps()), _mm_set1_ps(2.6f32)), - _mm_set1_ps(SCALE), - ) -} - -#[inline(always)] -pub unsafe fn sse_gamma2p2_to_linear(gamma: __m128) -> __m128 { - sse_pure_gamma(gamma, 2.2f32) -} - -#[inline(always)] -pub unsafe fn sse_gamma2p8_to_linear(gamma: __m128) -> __m128 { - sse_pure_gamma(gamma, 2.8f32) -} - -#[inline(always)] -pub unsafe fn sse_gamma2p2_from_linear(linear: __m128) -> __m128 { - sse_pure_gamma(linear, 1f32 / 2.2f32) -} - -#[inline(always)] -pub unsafe fn sse_gamma2p8_from_linear(linear: __m128) -> __m128 { - sse_pure_gamma(linear, 1f32 / 2.8f32) -} diff --git a/src/sse/image_to_hsv.rs b/src/sse/image_to_hsv.rs index 59f2902..2f36d48 100644 --- a/src/sse/image_to_hsv.rs +++ b/src/sse/image_to_hsv.rs @@ -42,105 +42,107 @@ pub unsafe fn sse_channels_to_hsv_u16< let v_scale = _mm_set1_ps(scale); while cx + 16 <= width as usize { - let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); - let (r_chan, g_chan, b_chan, a_chan) = - load_u8_and_deinterleave!(src_ptr, image_configuration); - - let zeros = _mm_setzero_si128(); - - let r_low = _mm_unpacklo_epi8(r_chan, zeros); - let g_low = _mm_unpacklo_epi8(g_chan, zeros); - let b_low = _mm_unpacklo_epi8(b_chan, zeros); - - let r_low_low = _mm_unpacklo_epi16(r_low, zeros); - let g_low_low = _mm_unpacklo_epi16(g_low, zeros); - let b_low_low = _mm_unpacklo_epi16(b_low, zeros); - - let (x_low_low, y_low_low, z_low_low) = match target { - HsvTarget::Hsv => sse_rgb_to_hsv(r_low_low, g_low_low, b_low_low, v_scale), - HsvTarget::Hsl => sse_rgb_to_hsl(r_low_low, g_low_low, b_low_low, v_scale), - }; - - let a_low = _mm_unpacklo_epi8(a_chan, zeros); - - let r_low_high = _mm_unpackhi_epi16(r_low, zeros); - let g_low_high = _mm_unpackhi_epi16(g_low, zeros); - let b_low_high = _mm_unpackhi_epi16(b_low, zeros); - - let (x_low_high, y_low_high, z_low_high) = match target { - HsvTarget::Hsv => sse_rgb_to_hsv(r_low_high, g_low_high, b_low_high, v_scale), - HsvTarget::Hsl => sse_rgb_to_hsl(r_low_high, g_low_high, b_low_high, v_scale), - }; - - const ROUNDING_FLAGS: i32 = _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC; - let x_low = _mm_packus_epi32( - _mm_cvtps_epi32(_mm_round_ps::(x_low_low)), - _mm_cvtps_epi32(_mm_round_ps::(x_low_high)), - ); - let y_low = _mm_packus_epi32( - _mm_cvtps_epi32(_mm_round_ps::(y_low_low)), - _mm_cvtps_epi32(_mm_round_ps::(y_low_high)), - ); - let z_low = _mm_packus_epi32( - _mm_cvtps_epi32(_mm_round_ps::(z_low_low)), - _mm_cvtps_epi32(_mm_round_ps::(z_low_high)), - ); - - if USE_ALPHA { - let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); - store_and_interleave_v4_u16!(ptr, x_low, y_low, z_low, a_low); - } else { - let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); - store_and_interleave_v3_u16!(ptr, x_low, y_low, z_low); - } - - let r_high = _mm_unpackhi_epi8(r_chan, zeros); - let g_high = _mm_unpackhi_epi8(g_chan, zeros); - let b_high = _mm_unpackhi_epi8(b_chan, zeros); - - let r_high_low = _mm_unpacklo_epi16(r_high, zeros); - let g_high_low = _mm_unpacklo_epi16(g_high, zeros); - let b_high_low = _mm_unpacklo_epi16(b_high, zeros); - - let (x_high_low, y_high_low, z_high_low) = match target { - HsvTarget::Hsv => sse_rgb_to_hsv(r_high_low, g_high_low, b_high_low, v_scale), - HsvTarget::Hsl => sse_rgb_to_hsl(r_high_low, g_high_low, b_high_low, v_scale), - }; - - let a_high = _mm_unpackhi_epi8(a_chan, zeros); - - let r_high_high = _mm_unpackhi_epi16(r_high, zeros); - let g_high_high = _mm_unpackhi_epi16(g_high, zeros); - let b_high_high = _mm_unpackhi_epi16(b_high, zeros); - - let (x_high_high, y_high_high, z_high_high) = match target { - HsvTarget::Hsv => sse_rgb_to_hsv(r_high_high, g_high_high, b_high_high, v_scale), - HsvTarget::Hsl => sse_rgb_to_hsl(r_high_high, g_high_high, b_high_high, v_scale), - }; - - let x_high = _mm_packus_epi32( - _mm_cvtps_epi32(_mm_round_ps::(x_high_low)), - _mm_cvtps_epi32(_mm_round_ps::(x_high_high)), - ); - let y_high = _mm_packus_epi32( - _mm_cvtps_epi32(_mm_round_ps::(y_high_low)), - _mm_cvtps_epi32(_mm_round_ps::(y_high_high)), - ); - let z_high = _mm_packus_epi32( - _mm_cvtps_epi32(_mm_round_ps::(z_high_low)), - _mm_cvtps_epi32(_mm_round_ps::(z_high_high)), - ); - - if USE_ALPHA { - let ptr = dst - .get_unchecked_mut(cx * channels + 8 * channels..) - .as_mut_ptr(); - store_and_interleave_v4_u16!(ptr, x_high, y_high, z_high, a_high); - } else { - let ptr = dst - .get_unchecked_mut(cx * channels + 8 * channels..) - .as_mut_ptr(); - store_and_interleave_v3_u16!(ptr, x_high, y_high, z_high); + unsafe { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); + let (r_chan, g_chan, b_chan, a_chan) = + load_u8_and_deinterleave!(src_ptr, image_configuration); + + let zeros = _mm_setzero_si128(); + + let r_low = _mm_unpacklo_epi8(r_chan, zeros); + let g_low = _mm_unpacklo_epi8(g_chan, zeros); + let b_low = _mm_unpacklo_epi8(b_chan, zeros); + + let r_low_low = _mm_unpacklo_epi16(r_low, zeros); + let g_low_low = _mm_unpacklo_epi16(g_low, zeros); + let b_low_low = _mm_unpacklo_epi16(b_low, zeros); + + let (x_low_low, y_low_low, z_low_low) = match target { + HsvTarget::Hsv => sse_rgb_to_hsv(r_low_low, g_low_low, b_low_low, v_scale), + HsvTarget::Hsl => sse_rgb_to_hsl(r_low_low, g_low_low, b_low_low, v_scale), + }; + + let a_low = _mm_unpacklo_epi8(a_chan, zeros); + + let r_low_high = _mm_unpackhi_epi16(r_low, zeros); + let g_low_high = _mm_unpackhi_epi16(g_low, zeros); + let b_low_high = _mm_unpackhi_epi16(b_low, zeros); + + let (x_low_high, y_low_high, z_low_high) = match target { + HsvTarget::Hsv => sse_rgb_to_hsv(r_low_high, g_low_high, b_low_high, v_scale), + HsvTarget::Hsl => sse_rgb_to_hsl(r_low_high, g_low_high, b_low_high, v_scale), + }; + + const ROUNDING_FLAGS: i32 = _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC; + let x_low = _mm_packus_epi32( + _mm_cvtps_epi32(_mm_round_ps::(x_low_low)), + _mm_cvtps_epi32(_mm_round_ps::(x_low_high)), + ); + let y_low = _mm_packus_epi32( + _mm_cvtps_epi32(_mm_round_ps::(y_low_low)), + _mm_cvtps_epi32(_mm_round_ps::(y_low_high)), + ); + let z_low = _mm_packus_epi32( + _mm_cvtps_epi32(_mm_round_ps::(z_low_low)), + _mm_cvtps_epi32(_mm_round_ps::(z_low_high)), + ); + + if USE_ALPHA { + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); + store_and_interleave_v4_u16!(ptr, x_low, y_low, z_low, a_low); + } else { + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); + store_and_interleave_v3_u16!(ptr, x_low, y_low, z_low); + } + + let r_high = _mm_unpackhi_epi8(r_chan, zeros); + let g_high = _mm_unpackhi_epi8(g_chan, zeros); + let b_high = _mm_unpackhi_epi8(b_chan, zeros); + + let r_high_low = _mm_unpacklo_epi16(r_high, zeros); + let g_high_low = _mm_unpacklo_epi16(g_high, zeros); + let b_high_low = _mm_unpacklo_epi16(b_high, zeros); + + let (x_high_low, y_high_low, z_high_low) = match target { + HsvTarget::Hsv => sse_rgb_to_hsv(r_high_low, g_high_low, b_high_low, v_scale), + HsvTarget::Hsl => sse_rgb_to_hsl(r_high_low, g_high_low, b_high_low, v_scale), + }; + + let a_high = _mm_unpackhi_epi8(a_chan, zeros); + + let r_high_high = _mm_unpackhi_epi16(r_high, zeros); + let g_high_high = _mm_unpackhi_epi16(g_high, zeros); + let b_high_high = _mm_unpackhi_epi16(b_high, zeros); + + let (x_high_high, y_high_high, z_high_high) = match target { + HsvTarget::Hsv => sse_rgb_to_hsv(r_high_high, g_high_high, b_high_high, v_scale), + HsvTarget::Hsl => sse_rgb_to_hsl(r_high_high, g_high_high, b_high_high, v_scale), + }; + + let x_high = _mm_packus_epi32( + _mm_cvtps_epi32(_mm_round_ps::(x_high_low)), + _mm_cvtps_epi32(_mm_round_ps::(x_high_high)), + ); + let y_high = _mm_packus_epi32( + _mm_cvtps_epi32(_mm_round_ps::(y_high_low)), + _mm_cvtps_epi32(_mm_round_ps::(y_high_high)), + ); + let z_high = _mm_packus_epi32( + _mm_cvtps_epi32(_mm_round_ps::(z_high_low)), + _mm_cvtps_epi32(_mm_round_ps::(z_high_high)), + ); + + if USE_ALPHA { + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels..) + .as_mut_ptr(); + store_and_interleave_v4_u16!(ptr, x_high, y_high, z_high, a_high); + } else { + let ptr = dst + .get_unchecked_mut(cx * channels + 8 * channels..) + .as_mut_ptr(); + store_and_interleave_v3_u16!(ptr, x_high, y_high, z_high); + } } cx += 16; diff --git a/src/sse/image_to_jzazbz.rs b/src/sse/image_to_jzazbz.rs index bde95a3..84434c6 100644 --- a/src/sse/image_to_jzazbz.rs +++ b/src/sse/image_to_jzazbz.rs @@ -19,8 +19,8 @@ use crate::sse::{ }; use crate::sse::{sse_deinterleave_rgb_ps, sse_deinterleave_rgba_ps}; use crate::{ - load_f32_and_deinterleave, store_and_interleave_v3_direct_f32, - store_and_interleave_v4_direct_f32, SRGB_TO_XYZ_D65, + SRGB_TO_XYZ_D65, load_f32_and_deinterleave, store_and_interleave_v3_direct_f32, + store_and_interleave_v4_direct_f32, }; macro_rules! perceptual_quantizer { @@ -128,19 +128,21 @@ pub(crate) unsafe fn sse_image_to_jzazbz( +pub(crate) fn sse_image_to_oklab( start_cx: usize, width: u32, dst: &mut [f32], ) -> usize { - let target: OklabTarget = TARGET.into(); - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let channels = image_configuration.channel_count(); - let mut cx = start_cx; + unsafe { + let target: OklabTarget = TARGET.into(); + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + let channels = image_configuration.channel_count(); + let mut cx = start_cx; - let (c0, c1, c2, c3, c4, c5, c6, c7, c8) = ( - _mm_set1_ps(0.4122214708f32), - _mm_set1_ps(0.5363325363f32), - _mm_set1_ps(0.0514459929f32), - _mm_set1_ps(0.2119034982f32), - _mm_set1_ps(0.6806995451f32), - _mm_set1_ps(0.1073969566f32), - _mm_set1_ps(0.0883024619f32), - _mm_set1_ps(0.2817188376f32), - _mm_set1_ps(0.6299787005f32), - ); - - let (m0, m1, m2, m3, m4, m5, m6, m7, m8) = ( - _mm_set1_ps(0.2104542553f32), - _mm_set1_ps(0.7936177850f32), - _mm_set1_ps(-0.0040720468f32), - _mm_set1_ps(1.9779984951f32), - _mm_set1_ps(-2.4285922050f32), - _mm_set1_ps(0.4505937099f32), - _mm_set1_ps(0.0259040371f32), - _mm_set1_ps(0.7827717662f32), - _mm_set1_ps(-0.8086757660f32), - ); - - while cx + 4 <= width as usize { - let in_place_ptr = dst.get_unchecked_mut(cx * channels..); - let (r_chan, g_chan, b_chan, a_chan) = - load_f32_and_deinterleave!(in_place_ptr.as_ptr(), image_configuration); + let (c0, c1, c2, c3, c4, c5, c6, c7, c8) = ( + _mm_set1_ps(0.4122214708f32), + _mm_set1_ps(0.5363325363f32), + _mm_set1_ps(0.0514459929f32), + _mm_set1_ps(0.2119034982f32), + _mm_set1_ps(0.6806995451f32), + _mm_set1_ps(0.1073969566f32), + _mm_set1_ps(0.0883024619f32), + _mm_set1_ps(0.2817188376f32), + _mm_set1_ps(0.6299787005f32), + ); - let (l_oklab, a_oklab, b_oklab) = triple_to_oklab!( - r_chan, g_chan, b_chan, target, c0, c1, c2, c3, c4, c5, c6, c7, c8, m0, m1, m2, m3, m4, - m5, m6, m7, m8 + let (m0, m1, m2, m3, m4, m5, m6, m7, m8) = ( + _mm_set1_ps(0.2104542553f32), + _mm_set1_ps(0.7936177850f32), + _mm_set1_ps(-0.0040720468f32), + _mm_set1_ps(1.9779984951f32), + _mm_set1_ps(-2.4285922050f32), + _mm_set1_ps(0.4505937099f32), + _mm_set1_ps(0.0259040371f32), + _mm_set1_ps(0.7827717662f32), + _mm_set1_ps(-0.8086757660f32), ); - if image_configuration.has_alpha() { - store_and_interleave_v4_direct_f32!( - in_place_ptr.as_mut_ptr(), - l_oklab, - a_oklab, - b_oklab, - a_chan - ); - } else { - store_and_interleave_v3_direct_f32!( - in_place_ptr.as_mut_ptr(), - l_oklab, - a_oklab, - b_oklab + while cx + 4 <= width as usize { + let in_place_ptr = dst.get_unchecked_mut(cx * channels..); + let (r_chan, g_chan, b_chan, a_chan) = + load_f32_and_deinterleave!(in_place_ptr.as_ptr(), image_configuration); + + let (l_oklab, a_oklab, b_oklab) = triple_to_oklab!( + r_chan, g_chan, b_chan, target, c0, c1, c2, c3, c4, c5, c6, c7, c8, m0, m1, m2, m3, + m4, m5, m6, m7, m8 ); + + if image_configuration.has_alpha() { + store_and_interleave_v4_direct_f32!( + in_place_ptr.as_mut_ptr(), + l_oklab, + a_oklab, + b_oklab, + a_chan + ); + } else { + store_and_interleave_v3_direct_f32!( + in_place_ptr.as_mut_ptr(), + l_oklab, + a_oklab, + b_oklab + ); + } + + cx += 4; } - cx += 4; + cx } - - cx } diff --git a/src/sse/jzazbz_to_image.rs b/src/sse/jzazbz_to_image.rs index 5cb4d2e..05630dc 100644 --- a/src/sse/jzazbz_to_image.rs +++ b/src/sse/jzazbz_to_image.rs @@ -18,8 +18,8 @@ use crate::sse::{ }; use crate::sse::{sse_interleave_ps_rgb, sse_interleave_ps_rgba}; use crate::{ - load_f32_and_deinterleave_direct, store_and_interleave_v3_f32, store_and_interleave_v4_f32, - XYZ_TO_SRGB_D65, + XYZ_TO_SRGB_D65, load_f32_and_deinterleave_direct, store_and_interleave_v3_f32, + store_and_interleave_v4_f32, }; use erydanos::{_mm_cos_ps, _mm_isnan_ps, _mm_mlaf_ps, _mm_pow_ps, _mm_sin_ps}; @@ -43,28 +43,34 @@ macro_rules! perceptual_quantizer_inverse { }}; } -#[inline(always)] -unsafe fn sse_jzazbz_vld( +#[inline] +#[target_feature(enable = "sse4.1")] +fn sse_jzazbz_vld( src: *const f32, luminance_scale: __m128, ) -> (__m128, __m128, __m128, __m128) { let target: JzazbzTarget = TARGET.into(); let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let (jz, mut az, mut bz, a_f32) = load_f32_and_deinterleave_direct!(src, image_configuration); + let (jz, mut az, mut bz, a_f32) = + unsafe { load_f32_and_deinterleave_direct!(src, image_configuration) }; if target == JzazbzTarget::Jzczhz { let cz = az; let hz = bz; - az = _mm_mul_ps(cz, _mm_cos_ps(hz)); - bz = _mm_mul_ps(cz, _mm_sin_ps(hz)); + unsafe { + az = _mm_mul_ps(cz, _mm_cos_ps(hz)); + bz = _mm_mul_ps(cz, _mm_sin_ps(hz)); + } } let jz = _mm_add_ps(jz, _mm_set1_ps(1.6295499532821566e-11)); - let iz = _mm_div_ps( - jz, - _mm_mlaf_ps(jz, _mm_set1_ps(0.56f32), _mm_set1_ps(0.44f32)), - ); + let iz = unsafe { + _mm_div_ps( + jz, + _mm_mlaf_ps(jz, _mm_set1_ps(0.56f32), _mm_set1_ps(0.44f32)), + ) + }; let (m0, m1, m2, m3, m4, m5, m6, m7, m8) = ( _mm_set1_ps(1f32), @@ -81,9 +87,11 @@ unsafe fn sse_jzazbz_vld( let (mut l_l, mut l_m, mut l_s) = _mm_color_matrix_ps(iz, az, bz, m0, m1, m2, m3, m4, m5, m6, m7, m8); - l_l = perceptual_quantizer_inverse!(l_l); - l_m = perceptual_quantizer_inverse!(l_m); - l_s = perceptual_quantizer_inverse!(l_s); + unsafe { + l_l = perceptual_quantizer_inverse!(l_l); + l_m = perceptual_quantizer_inverse!(l_m); + l_s = perceptual_quantizer_inverse!(l_s); + } let (c0, c1, c2, c3, c4, c5, c6, c7, c8) = ( _mm_set1_ps(1.661373055774069e+00), @@ -105,15 +113,15 @@ unsafe fn sse_jzazbz_vld( z = _mm_mul_ps(z, luminance_scale); let (x0, x1, x2, x3, x4, x5, x6, x7, x8) = ( - _mm_set1_ps(*XYZ_TO_SRGB_D65.get_unchecked(0).get_unchecked(0)), - _mm_set1_ps(*XYZ_TO_SRGB_D65.get_unchecked(0).get_unchecked(1)), - _mm_set1_ps(*XYZ_TO_SRGB_D65.get_unchecked(0).get_unchecked(2)), - _mm_set1_ps(*XYZ_TO_SRGB_D65.get_unchecked(1).get_unchecked(0)), - _mm_set1_ps(*XYZ_TO_SRGB_D65.get_unchecked(1).get_unchecked(1)), - _mm_set1_ps(*XYZ_TO_SRGB_D65.get_unchecked(1).get_unchecked(2)), - _mm_set1_ps(*XYZ_TO_SRGB_D65.get_unchecked(2).get_unchecked(0)), - _mm_set1_ps(*XYZ_TO_SRGB_D65.get_unchecked(2).get_unchecked(1)), - _mm_set1_ps(*XYZ_TO_SRGB_D65.get_unchecked(2).get_unchecked(2)), + _mm_set1_ps(XYZ_TO_SRGB_D65[0][0]), + _mm_set1_ps(XYZ_TO_SRGB_D65[0][1]), + _mm_set1_ps(XYZ_TO_SRGB_D65[0][2]), + _mm_set1_ps(XYZ_TO_SRGB_D65[1][0]), + _mm_set1_ps(XYZ_TO_SRGB_D65[1][1]), + _mm_set1_ps(XYZ_TO_SRGB_D65[1][2]), + _mm_set1_ps(XYZ_TO_SRGB_D65[2][0]), + _mm_set1_ps(XYZ_TO_SRGB_D65[2][1]), + _mm_set1_ps(XYZ_TO_SRGB_D65[2][2]), ); let (r_l, g_l, b_l) = _mm_color_matrix_ps(x, y, z, x0, x1, x2, x3, x4, x5, x6, x7, x8); @@ -121,12 +129,10 @@ unsafe fn sse_jzazbz_vld( } #[target_feature(enable = "sse4.1")] -pub unsafe fn sse_jzazbz_to_image( +pub(crate) fn sse_jzazbz_to_image( start_cx: usize, - src: *const f32, - src_offset: u32, - dst: *mut f32, - dst_offset: u32, + src: &[f32], + dst: &mut [f32], width: u32, display_luminance: f32, ) -> usize { @@ -136,28 +142,35 @@ pub unsafe fn sse_jzazbz_to_image(src_ptr_0, luminance_scale); - - let dst_ptr = ((dst as *mut u8).add(dst_offset as usize) as *mut f32).add(cx * channels); - - if image_configuration.has_alpha() { - store_and_interleave_v4_f32!( - dst_ptr, - image_configuration, - r_row0_, - g_row0_, - b_row0_, - a_row0_ - ); - } else { - store_and_interleave_v3_f32!(dst_ptr, image_configuration, r_row0_, g_row0_, b_row0_); + sse_jzazbz_vld::(src_ptr_0.as_ptr(), luminance_scale); + + let dst_ptr = unsafe { dst.get_unchecked_mut(cx * channels..).as_mut_ptr() }; + + unsafe { + if image_configuration.has_alpha() { + store_and_interleave_v4_f32!( + dst_ptr, + image_configuration, + r_row0_, + g_row0_, + b_row0_, + a_row0_ + ); + } else { + store_and_interleave_v3_f32!( + dst_ptr, + image_configuration, + r_row0_, + g_row0_, + b_row0_ + ); + } } cx += 8; diff --git a/src/sse/math.rs b/src/sse/math.rs index 22981b0..e1eebe5 100644 --- a/src/sse/math.rs +++ b/src/sse/math.rs @@ -12,73 +12,84 @@ use std::arch::x86_64::*; use erydanos::_mm_pow_ps; -#[inline(always)] -pub unsafe fn _mm_cube_ps(x: __m128) -> __m128 { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_cube_ps(x: __m128) -> __m128 { _mm_mul_ps(_mm_mul_ps(x, x), x) } #[cfg(not(target_feature = "fma"))] #[inline] -pub unsafe fn _mm_prefer_fma_ps(a: __m128, b: __m128, c: __m128) -> __m128 { +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_prefer_fma_ps(a: __m128, b: __m128, c: __m128) -> __m128 { _mm_add_ps(_mm_mul_ps(b, c), a) } #[cfg(target_feature = "fma")] #[inline] -pub unsafe fn _mm_prefer_fma_ps(a: __m128, b: __m128, c: __m128) -> __m128 { - return _mm_fmadd_ps(b, c, a); +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_prefer_fma_ps(a: __m128, b: __m128, c: __m128) -> __m128 { + unsafe { _mm_fmadd_ps(b, c, a) } } -#[inline(always)] -pub unsafe fn _mm_select_ps(mask: __m128, true_vals: __m128, false_vals: __m128) -> __m128 { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_select_ps(mask: __m128, true_vals: __m128, false_vals: __m128) -> __m128 { _mm_blendv_ps(false_vals, true_vals, mask) } -#[inline(always)] +#[inline] +#[target_feature(enable = "sse4.1")] #[allow(dead_code)] -pub unsafe fn _mm_selecti_ps(mask: __m128i, true_vals: __m128, false_vals: __m128) -> __m128 { +pub(crate) fn _mm_selecti_ps(mask: __m128i, true_vals: __m128, false_vals: __m128) -> __m128 { _mm_blendv_ps(false_vals, true_vals, _mm_castsi128_ps(mask)) } -#[inline(always)] +#[inline] +#[target_feature(enable = "sse4.1")] #[allow(dead_code)] -pub unsafe fn _mm_select_si128(mask: __m128i, true_vals: __m128i, false_vals: __m128i) -> __m128i { +pub(crate) fn _mm_select_si128(mask: __m128i, true_vals: __m128i, false_vals: __m128i) -> __m128i { _mm_or_si128( _mm_and_si128(mask, true_vals), _mm_andnot_si128(mask, false_vals), ) } -#[inline(always)] +#[inline] +#[target_feature(enable = "sse4.1")] pub unsafe fn _mm_pow_n_ps(x: __m128, n: f32) -> __m128 { - _mm_pow_ps(x, _mm_set1_ps(n)) + unsafe { _mm_pow_ps(x, _mm_set1_ps(n)) } } -#[inline(always)] -pub unsafe fn _mm_signbit_ps(f: __m128) -> __m128i { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_signbit_ps(f: __m128) -> __m128i { _mm_and_si128(_mm_castps_si128(f), _mm_castps_si128(_mm_set1_ps(-0.0f32))) } -#[inline(always)] -pub unsafe fn _mm_mulsign_ps(x: __m128, y: __m128) -> __m128 { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_mulsign_ps(x: __m128, y: __m128) -> __m128 { _mm_castsi128_ps(_mm_xor_si128(_mm_castps_si128(x), _mm_signbit_ps(y))) } -#[inline(always)] -pub unsafe fn _mm_pow2i_ps(q: __m128i) -> __m128 { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_pow2i_ps(q: __m128i) -> __m128 { _mm_castsi128_ps(_mm_slli_epi32::<23>(_mm_add_epi32(q, _mm_set1_epi32(0x7f)))) } -#[inline(always)] -pub unsafe fn _mm_vldexp2_ps(d: __m128, e: __m128i) -> __m128 { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_vldexp2_ps(d: __m128, e: __m128i) -> __m128 { _mm_mul_ps( _mm_mul_ps(d, _mm_pow2i_ps(_mm_srli_epi32::<1>(e))), _mm_pow2i_ps(_mm_sub_epi32(e, _mm_srli_epi32::<1>(e))), ) } - -#[inline(always)] -pub unsafe fn _mm_vilogbk_ps(d: __m128) -> __m128i { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_vilogbk_ps(d: __m128) -> __m128i { let o = _mm_cmplt_ps(d, _mm_set1_ps(5.421010862427522E-20f32)); let d = _mm_select_ps(o, _mm_mul_ps(_mm_set1_ps(1.8446744073709552E19f32), d), d); let q = _mm_and_si128( @@ -95,39 +106,44 @@ pub unsafe fn _mm_vilogbk_ps(d: __m128) -> __m128i { ) } -#[inline(always)] -pub(crate) unsafe fn _mm_fmaf_ps(a: __m128, b: __m128, c: __m128) -> __m128 { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_fmaf_ps(a: __m128, b: __m128, c: __m128) -> __m128 { _mm_prefer_fma_ps(c, b, a) } -#[inline(always)] +#[inline] +#[target_feature(enable = "sse4.1")] #[allow(dead_code)] -pub(crate) unsafe fn _mm_neg_epi32(x: __m128i) -> __m128i { +pub(crate) fn _mm_neg_epi32(x: __m128i) -> __m128i { let high = _mm_set1_epi32(0i32); _mm_sub_epi32(high, x) } -#[inline(always)] -pub(crate) unsafe fn _mm_neg_ps(x: __m128) -> __m128 { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_neg_ps(x: __m128) -> __m128 { let high = _mm_set1_ps(0f32); _mm_sub_ps(high, x) } -#[inline(always)] -pub unsafe fn _mm_cmpge_epi32(a: __m128i, b: __m128i) -> __m128i { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_cmpge_epi32(a: __m128i, b: __m128i) -> __m128i { let gt = _mm_cmpgt_epi32(a, b); let eq = _mm_cmpeq_epi32(a, b); _mm_or_si128(gt, eq) } -#[inline(always)] -pub unsafe fn _mm_cmplt_epi32(a: __m128i, b: __m128i) -> __m128i { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_cmplt_epi32(a: __m128i, b: __m128i) -> __m128i { _mm_cmpgt_epi32(b, a) } #[inline] #[target_feature(enable = "sse4.1")] -pub fn _mm_color_matrix_ps( +pub(crate) fn _mm_color_matrix_ps( r: __m128, g: __m128, b: __m128, @@ -141,10 +157,8 @@ pub fn _mm_color_matrix_ps( c8: __m128, c9: __m128, ) -> (__m128, __m128, __m128) { - unsafe { - let new_r = _mm_prefer_fma_ps(_mm_prefer_fma_ps(_mm_mul_ps(g, c2), b, c3), r, c1); - let new_g = _mm_prefer_fma_ps(_mm_prefer_fma_ps(_mm_mul_ps(g, c5), b, c6), r, c4); - let new_b = _mm_prefer_fma_ps(_mm_prefer_fma_ps(_mm_mul_ps(g, c8), b, c9), r, c7); - (new_r, new_g, new_b) - } + let new_r = _mm_prefer_fma_ps(_mm_prefer_fma_ps(_mm_mul_ps(g, c2), b, c3), r, c1); + let new_g = _mm_prefer_fma_ps(_mm_prefer_fma_ps(_mm_mul_ps(g, c5), b, c6), r, c4); + let new_b = _mm_prefer_fma_ps(_mm_prefer_fma_ps(_mm_mul_ps(g, c8), b, c9), r, c7); + (new_r, new_g, new_b) } diff --git a/src/sse/mod.rs b/src/sse/mod.rs index d8d203f..63e28fd 100644 --- a/src/sse/mod.rs +++ b/src/sse/mod.rs @@ -6,8 +6,6 @@ */ mod color; -mod gamma_curves; - mod hsv_to_image; mod image_to_hsv; @@ -33,18 +31,18 @@ mod sigmoidal; mod to_sigmoidal; mod xyza_laba_to_image; -pub use cie::*; -pub use from_sigmoidal::sse_from_sigmoidal_row; +pub(crate) use cie::*; +pub(crate) use from_sigmoidal::sse_from_sigmoidal_row; pub use hsv_to_image::*; pub use image_to_hsv::*; pub(crate) use image_to_jzazbz::sse_image_to_jzazbz; pub(crate) use image_to_oklab::sse_image_to_oklab; -pub use jzazbz_to_image::sse_jzazbz_to_image; +pub(crate) use jzazbz_to_image::sse_jzazbz_to_image; pub use math::*; pub(crate) use oklab_to_image::sse_oklab_to_image; pub use support::*; pub(crate) use to_sigmoidal::sse_image_to_sigmoidal_row; pub(crate) use to_xyz_lab::sse_channels_to_xyz_or_lab; -pub use to_xyza_laba::*; -pub use xyz_lab_to_image::*; -pub use xyza_laba_to_image::*; +pub(crate) use to_xyza_laba::sse_channels_to_xyza_laba; +pub(crate) use xyz_lab_to_image::{sse_xyz_lab_vld, sse_xyz_to_channels}; +pub(crate) use xyza_laba_to_image::{sse_xyza_lab_vld, sse_xyza_to_image}; diff --git a/src/sse/oklab_to_image.rs b/src/sse/oklab_to_image.rs index a63f44a..b72efbb 100644 --- a/src/sse/oklab_to_image.rs +++ b/src/sse/oklab_to_image.rs @@ -17,8 +17,9 @@ use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; -#[inline(always)] -unsafe fn sse_oklab_vld( +#[inline] +#[target_feature(enable = "sse4.1")] +fn sse_oklab_vld( src: *const f32, oklab_target: OklabTarget, m0: __m128, @@ -40,30 +41,33 @@ unsafe fn sse_oklab_vld( c7: __m128, c8: __m128, ) -> (__m128, __m128, __m128, __m128) { - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + unsafe { + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let (l, mut a, mut b, a_f32) = load_f32_and_deinterleave!(src, image_configuration); + let (l, mut a, mut b, a_f32) = load_f32_and_deinterleave!(src, image_configuration); - if oklab_target == OklabTarget::Oklch { - let a0 = _mm_mul_ps(a, _mm_cos_ps(b)); - let b0 = _mm_mul_ps(a, _mm_sin_ps(b)); - a = a0; - b = b0; - } + if oklab_target == OklabTarget::Oklch { + let a0 = _mm_mul_ps(a, _mm_cos_ps(b)); + let b0 = _mm_mul_ps(a, _mm_sin_ps(b)); + a = a0; + b = b0; + } - let (mut l_l, mut l_m, mut l_s) = - _mm_color_matrix_ps(l, a, b, m0, m1, m2, m3, m4, m5, m6, m7, m8); + let (mut l_l, mut l_m, mut l_s) = + _mm_color_matrix_ps(l, a, b, m0, m1, m2, m3, m4, m5, m6, m7, m8); - l_l = _mm_cube_ps(l_l); - l_m = _mm_cube_ps(l_m); - l_s = _mm_cube_ps(l_s); + l_l = _mm_cube_ps(l_l); + l_m = _mm_cube_ps(l_m); + l_s = _mm_cube_ps(l_s); - let (r_l, g_l, b_l) = _mm_color_matrix_ps(l_l, l_m, l_s, c0, c1, c2, c3, c4, c5, c6, c7, c8); - (r_l, g_l, b_l, a_f32) + let (r_l, g_l, b_l) = + _mm_color_matrix_ps(l_l, l_m, l_s, c0, c1, c2, c3, c4, c5, c6, c7, c8); + (r_l, g_l, b_l, a_f32) + } } #[target_feature(enable = "sse4.1")] -pub(crate) unsafe fn sse_oklab_to_image( +pub(crate) fn sse_oklab_to_image( start_cx: usize, src: &[f32], dst: &mut [f32], @@ -99,28 +103,36 @@ pub(crate) unsafe fn sse_oklab_to_image( - src_ptr_0, target, m0, m1, m2, m3, m4, m5, m6, m7, m8, c0, c1, c2, c3, c4, c5, c6, c7, - c8, - ); + let src_ptr_0 = offset_src_ptr; - let dst_ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); - - if image_configuration.has_alpha() { - store_and_interleave_v4_f32!( - dst_ptr, - image_configuration, - r_row0_, - g_row0_, - b_row0_, - a_row0_ + let (r_row0_, g_row0_, b_row0_, a_row0_) = sse_oklab_vld::( + src_ptr_0, target, m0, m1, m2, m3, m4, m5, m6, m7, m8, c0, c1, c2, c3, c4, c5, c6, + c7, c8, ); - } else { - store_and_interleave_v3_f32!(dst_ptr, image_configuration, r_row0_, g_row0_, b_row0_); + + let dst_ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); + + if image_configuration.has_alpha() { + store_and_interleave_v4_f32!( + dst_ptr, + image_configuration, + r_row0_, + g_row0_, + b_row0_, + a_row0_ + ); + } else { + store_and_interleave_v3_f32!( + dst_ptr, + image_configuration, + r_row0_, + g_row0_, + b_row0_ + ); + } } cx += 4; diff --git a/src/sse/sigmoidal.rs b/src/sse/sigmoidal.rs index ae2f8bc..3b5b24a 100644 --- a/src/sse/sigmoidal.rs +++ b/src/sse/sigmoidal.rs @@ -12,28 +12,31 @@ use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; -#[inline(always)] -pub(crate) unsafe fn sse_color_to_sigmoidal(x: __m128) -> __m128 { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_color_to_sigmoidal(x: __m128) -> __m128 { let x = _mm_mul_ps(x, _mm_set1_ps(1f32 / 255f32)); let negg = _mm_neg_ps(x); - let den = _mm_add_ps(_mm_set1_ps(1f32), _mm_exp_ps(negg)); + let den = _mm_add_ps(_mm_set1_ps(1f32), unsafe { _mm_exp_ps(negg) }); let erase_nan_mask = _mm_cmpeq_ps(den, _mm_setzero_ps()); let rcp = _mm_rcp_ps(den); _mm_select_ps(erase_nan_mask, _mm_setzero_ps(), rcp) } -#[inline(always)] -pub(crate) unsafe fn sse_sigmoidal_to_color(x: __m128) -> __m128 { +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_sigmoidal_to_color(x: __m128) -> __m128 { let den = _mm_sub_ps(_mm_set1_ps(1f32), x); let zero_mask_1 = _mm_cmpeq_ps(den, _mm_setzero_ps()); let k = _mm_mul_ps(x, _mm_rcp_ps(den)); let zeros = _mm_setzero_ps(); let zero_mask_2 = _mm_cmple_ps(k, zeros); - let ln = _mm_ln_fast_ps(k); + let ln = unsafe { _mm_ln_fast_ps(k) }; _mm_select_ps(_mm_and_ps(zero_mask_1, zero_mask_2), zeros, ln) } -#[inline(always)] +#[inline] +#[target_feature(enable = "sse4.1")] pub(crate) unsafe fn sse_rgb_to_sigmoidal( r: __m128i, g: __m128i, @@ -45,8 +48,9 @@ pub(crate) unsafe fn sse_rgb_to_sigmoidal( (sr, sg, sb) } -#[inline(always)] -pub(crate) unsafe fn sse_sigmoidal_to_rgb( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_sigmoidal_to_rgb( sr: __m128, sg: __m128, sb: __m128, diff --git a/src/sse/support.rs b/src/sse/support.rs index a146c90..32c8c46 100644 --- a/src/sse/support.rs +++ b/src/sse/support.rs @@ -15,8 +15,9 @@ pub const fn shuffle(z: u32, y: u32, x: u32, w: u32) -> i32 { ((z << 6) | (y << 4) | (x << 2) | w) as i32 } -#[inline(always)] -pub unsafe fn sse_interleave_rgba( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_interleave_rgba( r: __m128i, g: __m128i, b: __m128i, @@ -58,8 +59,9 @@ pub(crate) fn sse_interleave_ps_rgb(a: __m128, b: __m128, c: __m128) -> (__m128, (v0, v1, v2) } -#[inline(always)] -pub unsafe fn sse_interleave_ps_rgba( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_interleave_ps_rgba( a: __m128, b: __m128, c: __m128, @@ -76,8 +78,9 @@ pub unsafe fn sse_interleave_ps_rgba( (v0, v1, v2, v3) } -#[inline(always)] -pub unsafe fn sse_deinterleave_rgba( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_deinterleave_rgba( rgba0: __m128i, rgba1: __m128i, rgba2: __m128i, @@ -113,8 +116,9 @@ pub unsafe fn sse_deinterleave_rgba( (r1, r2, r3, r4) } -#[inline(always)] -pub unsafe fn sse_deinterleave_rgb_ps( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_deinterleave_rgb_ps( t0: __m128, t1: __m128, t2: __m128, @@ -136,8 +140,9 @@ pub unsafe fn sse_deinterleave_rgb_ps( (v0, v1, v2) } -#[inline(always)] -pub unsafe fn sse_deinterleave_rgb( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_deinterleave_rgb( rgb0: __m128i, rgb1: __m128i, rgb2: __m128i, @@ -178,8 +183,9 @@ pub unsafe fn sse_deinterleave_rgb( (r0r1r2, g0g1g2, b0b1b2) } -#[inline(always)] -pub unsafe fn sse_interleave_rgb( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_interleave_rgb( r: __m128i, g: __m128i, b: __m128i, @@ -199,8 +205,9 @@ pub unsafe fn sse_interleave_rgb( (v0, v1, v2) } -#[inline(always)] -pub unsafe fn sse_interleave_rgb_epi16( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_interleave_rgb_epi16( a: __m128i, b: __m128i, c: __m128i, @@ -218,8 +225,9 @@ pub unsafe fn sse_interleave_rgb_epi16( (v0, v1, v2) } -#[inline(always)] -pub unsafe fn sse_interleave_rgba_epi16( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_interleave_rgba_epi16( a: __m128i, b: __m128i, c: __m128i, @@ -237,8 +245,9 @@ pub unsafe fn sse_interleave_rgba_epi16( (v0, v1, v2, v3) } -#[inline(always)] -pub unsafe fn sse_deinterleave_rgba_epi16( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_deinterleave_rgba_epi16( u0: __m128i, u1: __m128i, u2: __m128i, @@ -261,8 +270,9 @@ pub unsafe fn sse_deinterleave_rgba_epi16( (a, b, c, d) } -#[inline(always)] -pub unsafe fn sse_deinterleave_rgb_epi16( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_deinterleave_rgb_epi16( v0: __m128i, v1: __m128i, v2: __m128i, @@ -280,8 +290,9 @@ pub unsafe fn sse_deinterleave_rgb_epi16( (a0, b0, c0) } -#[inline(always)] -pub unsafe fn sse_deinterleave_rgba_ps( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_deinterleave_rgba_ps( t0: __m128, t1: __m128, t2: __m128, @@ -298,38 +309,50 @@ pub unsafe fn sse_deinterleave_rgba_ps( (v0, v1, v2, v3) } -#[inline(always)] -pub unsafe fn _mm_loadu_si128_x4(ptr: *const u8) -> (__m128i, __m128i, __m128i, __m128i) { - ( - _mm_loadu_si128(ptr as *const __m128i), - _mm_loadu_si128(ptr.add(16) as *const __m128i), - _mm_loadu_si128(ptr.add(32) as *const __m128i), - _mm_loadu_si128(ptr.add(48) as *const __m128i), - ) +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_loadu_si128_x4(ptr: *const u8) -> (__m128i, __m128i, __m128i, __m128i) { + unsafe { + ( + _mm_loadu_si128(ptr as *const __m128i), + _mm_loadu_si128(ptr.add(16) as *const __m128i), + _mm_loadu_si128(ptr.add(32) as *const __m128i), + _mm_loadu_si128(ptr.add(48) as *const __m128i), + ) + } } -#[inline(always)] -pub unsafe fn _mm_storeu_ps_x4(ptr: *mut f32, set: (__m128, __m128, __m128, __m128)) { - _mm_storeu_ps(ptr, set.0); - _mm_storeu_ps(ptr.add(4), set.1); - _mm_storeu_ps(ptr.add(8), set.2); - _mm_storeu_ps(ptr.add(12), set.3); +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_storeu_ps_x4(ptr: *mut f32, set: (__m128, __m128, __m128, __m128)) { + unsafe { + _mm_storeu_ps(ptr, set.0); + _mm_storeu_ps(ptr.add(4), set.1); + _mm_storeu_ps(ptr.add(8), set.2); + _mm_storeu_ps(ptr.add(12), set.3); + } } -#[inline(always)] -pub unsafe fn _mm_loadu_ps_x4(ptr: *const f32) -> (__m128, __m128, __m128, __m128) { - ( - _mm_loadu_ps(ptr), - _mm_loadu_ps(ptr.add(4)), - _mm_loadu_ps(ptr.add(8)), - _mm_loadu_ps(ptr.add(12)), - ) +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_loadu_ps_x4(ptr: *const f32) -> (__m128, __m128, __m128, __m128) { + unsafe { + ( + _mm_loadu_ps(ptr), + _mm_loadu_ps(ptr.add(4)), + _mm_loadu_ps(ptr.add(8)), + _mm_loadu_ps(ptr.add(12)), + ) + } } -#[inline(always)] -pub unsafe fn _mm_storeu_si128_x4(ptr: *mut u8, set: (__m128i, __m128i, __m128i, __m128i)) { - _mm_storeu_si128(ptr as *mut __m128i, set.0); - _mm_storeu_si128(ptr.add(16) as *mut __m128i, set.1); - _mm_storeu_si128(ptr.add(32) as *mut __m128i, set.2); - _mm_storeu_si128(ptr.add(48) as *mut __m128i, set.3); +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn _mm_storeu_si128_x4(ptr: *mut u8, set: (__m128i, __m128i, __m128i, __m128i)) { + unsafe { + _mm_storeu_si128(ptr as *mut __m128i, set.0); + _mm_storeu_si128(ptr.add(16) as *mut __m128i, set.1); + _mm_storeu_si128(ptr.add(32) as *mut __m128i, set.2); + _mm_storeu_si128(ptr.add(48) as *mut __m128i, set.3); + } } diff --git a/src/sse/to_sigmoidal.rs b/src/sse/to_sigmoidal.rs index bac2eb5..27674f1 100644 --- a/src/sse/to_sigmoidal.rs +++ b/src/sse/to_sigmoidal.rs @@ -39,225 +39,239 @@ pub(crate) unsafe fn sse_image_to_sigmoidal_row< let channels = image_configuration.channel_count(); while cx + 16 <= width as usize { - let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); - let (r_chan, g_chan, b_chan, a_chan) = - load_u8_and_deinterleave!(src_ptr, image_configuration); - - let zeros = _mm_setzero_si128(); - - let r_low = _mm_unpacklo_epi8(r_chan, zeros); - let g_low = _mm_unpacklo_epi8(g_chan, zeros); - let b_low = _mm_unpacklo_epi8(b_chan, zeros); - let a_low = _mm_cvtepu8_epi16(a_chan); - - let r_low_low = _mm_unpacklo_epi16(r_low, zeros); - let g_low_low = _mm_unpacklo_epi16(g_low, zeros); - let b_low_low = _mm_unpacklo_epi16(b_low, zeros); - - let (x_low_low, y_low_low, z_low_low) = - sse_rgb_to_sigmoidal(r_low_low, g_low_low, b_low_low); - - let u8_scale = _mm_set1_ps(1f32 / 255f32); - - if USE_ALPHA { - let a_low_low = _mm_mul_ps(_mm_cvtepi32_ps(_mm_cvtepi16_epi32(a_low)), u8_scale); - let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); - store_and_interleave_v4_f32!( - ptr, - image_configuration, - x_low_low, - y_low_low, - z_low_low, - a_low_low - ); - } else { - let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); - store_and_interleave_v3_f32!(ptr, image_configuration, x_low_low, y_low_low, z_low_low); + unsafe { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); + let (r_chan, g_chan, b_chan, a_chan) = + load_u8_and_deinterleave!(src_ptr, image_configuration); + + let zeros = _mm_setzero_si128(); + + let r_low = _mm_unpacklo_epi8(r_chan, zeros); + let g_low = _mm_unpacklo_epi8(g_chan, zeros); + let b_low = _mm_unpacklo_epi8(b_chan, zeros); + let a_low = _mm_cvtepu8_epi16(a_chan); + + let r_low_low = _mm_unpacklo_epi16(r_low, zeros); + let g_low_low = _mm_unpacklo_epi16(g_low, zeros); + let b_low_low = _mm_unpacklo_epi16(b_low, zeros); + + let (x_low_low, y_low_low, z_low_low) = + sse_rgb_to_sigmoidal(r_low_low, g_low_low, b_low_low); + + let u8_scale = _mm_set1_ps(1f32 / 255f32); + + if USE_ALPHA { + let a_low_low = _mm_mul_ps(_mm_cvtepi32_ps(_mm_cvtepi16_epi32(a_low)), u8_scale); + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); + store_and_interleave_v4_f32!( + ptr, + image_configuration, + x_low_low, + y_low_low, + z_low_low, + a_low_low + ); + } else { + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); + store_and_interleave_v3_f32!( + ptr, + image_configuration, + x_low_low, + y_low_low, + z_low_low + ); + } + + let r_low_high = _mm_unpackhi_epi16(r_low, zeros); + let g_low_high = _mm_unpackhi_epi16(g_low, zeros); + let b_low_high = _mm_unpackhi_epi16(b_low, zeros); + + let (x_low_high, y_low_high, z_low_high) = + sse_rgb_to_sigmoidal(r_low_high, g_low_high, b_low_high); + + if USE_ALPHA { + let a_low_high = + _mm_mul_ps(_mm_cvtepi32_ps(_mm_unpackhi_epi16(a_low, zeros)), u8_scale); + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels..) + .as_mut_ptr(); + store_and_interleave_v4_f32!( + ptr, + image_configuration, + x_low_high, + y_low_high, + z_low_high, + a_low_high + ); + } else { + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels..) + .as_mut_ptr(); + store_and_interleave_v3_f32!( + ptr, + image_configuration, + x_low_high, + y_low_high, + z_low_high + ); + } + + let r_high = _mm_unpackhi_epi8(r_chan, zeros); + let g_high = _mm_unpackhi_epi8(g_chan, zeros); + let b_high = _mm_unpackhi_epi8(b_chan, zeros); + + let r_high_low = _mm_unpacklo_epi16(r_high, zeros); + let g_high_low = _mm_unpacklo_epi16(g_high, zeros); + let b_high_low = _mm_unpacklo_epi16(b_high, zeros); + + let (x_high_low, y_high_low, z_high_low) = + sse_rgb_to_sigmoidal(r_high_low, g_high_low, b_high_low); + + let a_high = _mm_unpackhi_epi8(a_chan, zeros); + + if USE_ALPHA { + let a_high_low = _mm_mul_ps(_mm_cvtepi32_ps(_mm_cvtepi16_epi32(a_high)), u8_scale); + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels * 2..) + .as_mut_ptr(); + store_and_interleave_v4_f32!( + ptr, + image_configuration, + x_high_low, + y_high_low, + z_high_low, + a_high_low + ); + } else { + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels * 2..) + .as_mut_ptr(); + store_and_interleave_v3_f32!( + ptr, + image_configuration, + x_high_low, + y_high_low, + z_high_low + ); + } + + let r_high_high = _mm_unpackhi_epi16(r_high, zeros); + let g_high_high = _mm_unpackhi_epi16(g_high, zeros); + let b_high_high = _mm_unpackhi_epi16(b_high, zeros); + + let (x_high_high, y_high_high, z_high_high) = + sse_rgb_to_sigmoidal(r_high_high, g_high_high, b_high_high); + + if USE_ALPHA { + let a_high_high = _mm_mul_ps( + _mm_cvtepi32_ps(_mm_unpackhi_epi16(a_high, _mm_setzero_si128())), + u8_scale, + ); + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels * 3..) + .as_mut_ptr(); + store_and_interleave_v4_f32!( + ptr, + image_configuration, + x_high_high, + y_high_high, + z_high_high, + a_high_high + ); + } else { + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels * 3..) + .as_mut_ptr(); + store_and_interleave_v3_f32!( + ptr, + image_configuration, + x_high_high, + y_high_high, + z_high_high + ); + } } - - let r_low_high = _mm_unpackhi_epi16(r_low, zeros); - let g_low_high = _mm_unpackhi_epi16(g_low, zeros); - let b_low_high = _mm_unpackhi_epi16(b_low, zeros); - - let (x_low_high, y_low_high, z_low_high) = - sse_rgb_to_sigmoidal(r_low_high, g_low_high, b_low_high); - - if USE_ALPHA { - let a_low_high = - _mm_mul_ps(_mm_cvtepi32_ps(_mm_unpackhi_epi16(a_low, zeros)), u8_scale); - let ptr = dst - .get_unchecked_mut(cx * channels + 4 * channels..) - .as_mut_ptr(); - store_and_interleave_v4_f32!( - ptr, - image_configuration, - x_low_high, - y_low_high, - z_low_high, - a_low_high - ); - } else { - let ptr = dst - .get_unchecked_mut(cx * channels + 4 * channels..) - .as_mut_ptr(); - store_and_interleave_v3_f32!( - ptr, - image_configuration, - x_low_high, - y_low_high, - z_low_high - ); - } - - let r_high = _mm_unpackhi_epi8(r_chan, zeros); - let g_high = _mm_unpackhi_epi8(g_chan, zeros); - let b_high = _mm_unpackhi_epi8(b_chan, zeros); - - let r_high_low = _mm_unpacklo_epi16(r_high, zeros); - let g_high_low = _mm_unpacklo_epi16(g_high, zeros); - let b_high_low = _mm_unpacklo_epi16(b_high, zeros); - - let (x_high_low, y_high_low, z_high_low) = - sse_rgb_to_sigmoidal(r_high_low, g_high_low, b_high_low); - - let a_high = _mm_unpackhi_epi8(a_chan, zeros); - - if USE_ALPHA { - let a_high_low = _mm_mul_ps(_mm_cvtepi32_ps(_mm_cvtepi16_epi32(a_high)), u8_scale); - let ptr = dst - .get_unchecked_mut(cx * channels + 4 * channels * 2..) - .as_mut_ptr(); - store_and_interleave_v4_f32!( - ptr, - image_configuration, - x_high_low, - y_high_low, - z_high_low, - a_high_low - ); - } else { - let ptr = dst - .get_unchecked_mut(cx * channels + 4 * channels * 2..) - .as_mut_ptr(); - store_and_interleave_v3_f32!( - ptr, - image_configuration, - x_high_low, - y_high_low, - z_high_low - ); - } - - let r_high_high = _mm_unpackhi_epi16(r_high, zeros); - let g_high_high = _mm_unpackhi_epi16(g_high, zeros); - let b_high_high = _mm_unpackhi_epi16(b_high, zeros); - - let (x_high_high, y_high_high, z_high_high) = - sse_rgb_to_sigmoidal(r_high_high, g_high_high, b_high_high); - - if USE_ALPHA { - let a_high_high = _mm_mul_ps( - _mm_cvtepi32_ps(_mm_unpackhi_epi16(a_high, _mm_setzero_si128())), - u8_scale, - ); - let ptr = dst - .get_unchecked_mut(cx * channels + 4 * channels * 3..) - .as_mut_ptr(); - store_and_interleave_v4_f32!( - ptr, - image_configuration, - x_high_high, - y_high_high, - z_high_high, - a_high_high - ); - } else { - let ptr = dst - .get_unchecked_mut(cx * channels + 4 * channels * 3..) - .as_mut_ptr(); - store_and_interleave_v3_f32!( - ptr, - image_configuration, - x_high_high, - y_high_high, - z_high_high - ); - } - cx += 16; } while cx + 8 < width as usize { - let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); - let (r_chan, g_chan, b_chan, a_chan) = - load_u8_and_deinterleave_half!(src_ptr, image_configuration); - - let zeros = _mm_setzero_si128(); - - let r_low = _mm_unpacklo_epi8(r_chan, zeros); - let g_low = _mm_unpacklo_epi8(g_chan, zeros); - let b_low = _mm_unpacklo_epi8(b_chan, zeros); - let a_low = _mm_cvtepu8_epi16(a_chan); - - let r_low_low = _mm_unpacklo_epi16(r_low, zeros); - let g_low_low = _mm_unpacklo_epi16(g_low, zeros); - let b_low_low = _mm_unpacklo_epi16(b_low, zeros); - - let (x_low_low, y_low_low, z_low_low) = - sse_rgb_to_sigmoidal(r_low_low, g_low_low, b_low_low); - - let u8_scale = _mm_set1_ps(1f32 / 255f32); - - if USE_ALPHA { - let a_low_low = _mm_mul_ps(_mm_cvtepi32_ps(_mm_cvtepi16_epi32(a_low)), u8_scale); - let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); - store_and_interleave_v4_f32!( - ptr, - image_configuration, - x_low_low, - y_low_low, - z_low_low, - a_low_low - ); - } else { - let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); - store_and_interleave_v3_f32!(ptr, image_configuration, x_low_low, y_low_low, z_low_low); + unsafe { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); + let (r_chan, g_chan, b_chan, a_chan) = + load_u8_and_deinterleave_half!(src_ptr, image_configuration); + + let zeros = _mm_setzero_si128(); + + let r_low = _mm_unpacklo_epi8(r_chan, zeros); + let g_low = _mm_unpacklo_epi8(g_chan, zeros); + let b_low = _mm_unpacklo_epi8(b_chan, zeros); + let a_low = _mm_cvtepu8_epi16(a_chan); + + let r_low_low = _mm_unpacklo_epi16(r_low, zeros); + let g_low_low = _mm_unpacklo_epi16(g_low, zeros); + let b_low_low = _mm_unpacklo_epi16(b_low, zeros); + + let (x_low_low, y_low_low, z_low_low) = + sse_rgb_to_sigmoidal(r_low_low, g_low_low, b_low_low); + + let u8_scale = _mm_set1_ps(1f32 / 255f32); + + if USE_ALPHA { + let a_low_low = _mm_mul_ps(_mm_cvtepi32_ps(_mm_cvtepi16_epi32(a_low)), u8_scale); + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); + store_and_interleave_v4_f32!( + ptr, + image_configuration, + x_low_low, + y_low_low, + z_low_low, + a_low_low + ); + } else { + let ptr = dst.get_unchecked_mut(cx * channels..).as_mut_ptr(); + store_and_interleave_v3_f32!( + ptr, + image_configuration, + x_low_low, + y_low_low, + z_low_low + ); + } + + let r_low_high = _mm_unpackhi_epi16(r_low, zeros); + let g_low_high = _mm_unpackhi_epi16(g_low, zeros); + let b_low_high = _mm_unpackhi_epi16(b_low, zeros); + + let (x_low_high, y_low_high, z_low_high) = + sse_rgb_to_sigmoidal(r_low_high, g_low_high, b_low_high); + + if USE_ALPHA { + let a_low_high = + _mm_mul_ps(_mm_cvtepi32_ps(_mm_unpackhi_epi16(a_low, zeros)), u8_scale); + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels..) + .as_mut_ptr(); + store_and_interleave_v4_f32!( + ptr, + image_configuration, + x_low_high, + y_low_high, + z_low_high, + a_low_high + ); + } else { + let ptr = dst + .get_unchecked_mut(cx * channels + 4 * channels..) + .as_mut_ptr(); + store_and_interleave_v3_f32!( + ptr, + image_configuration, + x_low_high, + y_low_high, + z_low_high + ); + } } - - let r_low_high = _mm_unpackhi_epi16(r_low, zeros); - let g_low_high = _mm_unpackhi_epi16(g_low, zeros); - let b_low_high = _mm_unpackhi_epi16(b_low, zeros); - - let (x_low_high, y_low_high, z_low_high) = - sse_rgb_to_sigmoidal(r_low_high, g_low_high, b_low_high); - - if USE_ALPHA { - let a_low_high = - _mm_mul_ps(_mm_cvtepi32_ps(_mm_unpackhi_epi16(a_low, zeros)), u8_scale); - let ptr = dst - .get_unchecked_mut(cx * channels + 4 * channels..) - .as_mut_ptr(); - store_and_interleave_v4_f32!( - ptr, - image_configuration, - x_low_high, - y_low_high, - z_low_high, - a_low_high - ); - } else { - let ptr = dst - .get_unchecked_mut(cx * channels + 4 * channels..) - .as_mut_ptr(); - store_and_interleave_v3_f32!( - ptr, - image_configuration, - x_low_high, - y_low_high, - z_low_high - ); - } - cx += 8; } diff --git a/src/sse/to_xyz_lab.rs b/src/sse/to_xyz_lab.rs index 742858a..2685881 100644 --- a/src/sse/to_xyz_lab.rs +++ b/src/sse/to_xyz_lab.rs @@ -36,56 +36,57 @@ pub(crate) unsafe fn sse_channels_to_xyz_or_lab< let channels = image_configuration.channel_count(); let mut cx = start_cx; - let cq1 = _mm_set1_ps(*matrix.get_unchecked(0).get_unchecked(0)); - let cq2 = _mm_set1_ps(*matrix.get_unchecked(0).get_unchecked(1)); - let cq3 = _mm_set1_ps(*matrix.get_unchecked(0).get_unchecked(2)); - let cq4 = _mm_set1_ps(*matrix.get_unchecked(1).get_unchecked(0)); - let cq5 = _mm_set1_ps(*matrix.get_unchecked(1).get_unchecked(1)); - let cq6 = _mm_set1_ps(*matrix.get_unchecked(1).get_unchecked(2)); - let cq7 = _mm_set1_ps(*matrix.get_unchecked(2).get_unchecked(0)); - let cq8 = _mm_set1_ps(*matrix.get_unchecked(2).get_unchecked(1)); - let cq9 = _mm_set1_ps(*matrix.get_unchecked(2).get_unchecked(2)); + let cq1 = _mm_set1_ps(matrix[0][0]); + let cq2 = _mm_set1_ps(matrix[0][1]); + let cq3 = _mm_set1_ps(matrix[0][2]); + let cq4 = _mm_set1_ps(matrix[1][0]); + let cq5 = _mm_set1_ps(matrix[1][1]); + let cq6 = _mm_set1_ps(matrix[1][2]); + let cq7 = _mm_set1_ps(matrix[2][0]); + let cq8 = _mm_set1_ps(matrix[2][1]); + let cq9 = _mm_set1_ps(matrix[2][2]); while cx + 4 <= width as usize { - let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); - let (r_chan, g_chan, b_chan, a_chan) = - load_f32_and_deinterleave!(src_ptr, image_configuration); + unsafe { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); + let (r_chan, g_chan, b_chan, a_chan) = + load_f32_and_deinterleave!(src_ptr, image_configuration); - let (mut x_low_low, mut y_low_low, mut z_low_low) = sse_triple_to_xyz( - r_chan, g_chan, b_chan, cq1, cq2, cq3, cq4, cq5, cq6, cq7, cq8, cq9, - ); + let (mut x_low_low, mut y_low_low, mut z_low_low) = sse_triple_to_xyz( + r_chan, g_chan, b_chan, cq1, cq2, cq3, cq4, cq5, cq6, cq7, cq8, cq9, + ); - match target { - XyzTarget::Lab => { - let (l, a, b) = sse_triple_to_lab(x_low_low, y_low_low, z_low_low); - x_low_low = l; - y_low_low = a; - z_low_low = b; + match target { + XyzTarget::Lab => { + let (l, a, b) = sse_triple_to_lab(x_low_low, y_low_low, z_low_low); + x_low_low = l; + y_low_low = a; + z_low_low = b; + } + XyzTarget::Xyz => {} + XyzTarget::Luv => { + let (l, u, v) = sse_triple_to_luv(x_low_low, y_low_low, z_low_low); + x_low_low = l; + y_low_low = u; + z_low_low = v; + } + XyzTarget::Lch => { + let (l, c, h) = sse_triple_to_lch(x_low_low, y_low_low, z_low_low); + x_low_low = l; + y_low_low = c; + z_low_low = h; + } } - XyzTarget::Xyz => {} - XyzTarget::Luv => { - let (l, u, v) = sse_triple_to_luv(x_low_low, y_low_low, z_low_low); - x_low_low = l; - y_low_low = u; - z_low_low = v; - } - XyzTarget::Lch => { - let (l, c, h) = sse_triple_to_lch(x_low_low, y_low_low, z_low_low); - x_low_low = l; - y_low_low = c; - z_low_low = h; - } - } - let (v0, v1, v2) = sse_interleave_ps_rgb(x_low_low, y_low_low, z_low_low); - _mm_storeu_ps(dst.get_unchecked_mut(cx * 3), v0); - _mm_storeu_ps(dst.get_unchecked_mut(cx * 3 + 4), v1); - _mm_storeu_ps(dst.get_unchecked_mut(cx * 3 + 8), v2); + let (v0, v1, v2) = sse_interleave_ps_rgb(x_low_low, y_low_low, z_low_low); + _mm_storeu_ps(dst.get_unchecked_mut(cx * 3), v0); + _mm_storeu_ps(dst.get_unchecked_mut(cx * 3 + 4), v1); + _mm_storeu_ps(dst.get_unchecked_mut(cx * 3 + 8), v2); - if USE_ALPHA { - _mm_storeu_ps(a_linearized.get_unchecked_mut(cx), a_chan); + if USE_ALPHA { + _mm_storeu_ps(a_linearized.get_unchecked_mut(cx), a_chan); + } } - cx += 4; } diff --git a/src/sse/to_xyza_laba.rs b/src/sse/to_xyza_laba.rs index cb4b502..70ce384 100644 --- a/src/sse/to_xyza_laba.rs +++ b/src/sse/to_xyza_laba.rs @@ -16,13 +16,11 @@ use std::arch::x86::*; use std::arch::x86_64::*; #[target_feature(enable = "sse4.1")] -pub unsafe fn sse_channels_to_xyza_laba( +pub(crate) fn sse_channels_to_xyza_laba( start_cx: usize, - src: *const f32, - src_offset: usize, + src: &[f32], + dst: &mut [f32], width: u32, - dst: *mut f32, - dst_offset: usize, matrix: &[[f32; 3]; 3], ) -> usize { const CHANNELS: usize = 4; @@ -34,22 +32,20 @@ pub unsafe fn sse_channels_to_xyza_laba (__m128, __m128, __m128) { - let target: XyzTarget = TARGET.into(); - let lab_pixel_0 = _mm_loadu_ps(src); - let lab_pixel_1 = _mm_loadu_ps(src.add(4)); - let lab_pixel_2 = _mm_loadu_ps(src.add(8)); - let (mut r_f32, mut g_f32, mut b_f32) = - sse_deinterleave_rgb_ps(lab_pixel_0, lab_pixel_1, lab_pixel_2); - - match target { - XyzTarget::Lab => { - let (x, y, z) = sse_lab_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; - } - XyzTarget::Luv => { - let (x, y, z) = sse_luv_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; + unsafe { + let target: XyzTarget = TARGET.into(); + let lab_pixel_0 = _mm_loadu_ps(src); + let lab_pixel_1 = _mm_loadu_ps(src.add(4)); + let lab_pixel_2 = _mm_loadu_ps(src.add(8)); + let (mut r_f32, mut g_f32, mut b_f32) = + sse_deinterleave_rgb_ps(lab_pixel_0, lab_pixel_1, lab_pixel_2); + + match target { + XyzTarget::Lab => { + let (x, y, z) = sse_lab_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + XyzTarget::Luv => { + let (x, y, z) = sse_luv_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + XyzTarget::Lch => { + let (x, y, z) = sse_lch_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + _ => {} } - XyzTarget::Lch => { - let (x, y, z) = sse_lch_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; - } - _ => {} - } - let (linear_r, linear_g, linear_b) = - _mm_color_matrix_ps(r_f32, g_f32, b_f32, c1, c2, c3, c4, c5, c6, c7, c8, c9); - (linear_r, linear_g, linear_b) + let (linear_r, linear_g, linear_b) = + _mm_color_matrix_ps(r_f32, g_f32, b_f32, c1, c2, c3, c4, c5, c6, c7, c8, c9); + (linear_r, linear_g, linear_b) + } } #[target_feature(enable = "sse4.1")] -pub unsafe fn sse_xyz_to_channels< +pub(crate) fn sse_xyz_to_channels< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, const TARGET: u8, >( start_cx: usize, - src: *const f32, - src_offset: usize, - a_channel: *const f32, - a_offset: usize, - dst: *mut f32, - dst_offset: usize, + src: &[f32], + a_channel: &[f32], + dst: &mut [f32], width: u32, matrix: &[[f32; 3]; 3], ) -> usize { @@ -92,21 +92,20 @@ pub unsafe fn sse_xyz_to_channels< let mut cx = start_cx; - let c1 = _mm_set1_ps(*matrix.get_unchecked(0).get_unchecked(0)); - let c2 = _mm_set1_ps(*matrix.get_unchecked(0).get_unchecked(1)); - let c3 = _mm_set1_ps(*matrix.get_unchecked(0).get_unchecked(2)); - let c4 = _mm_set1_ps(*matrix.get_unchecked(1).get_unchecked(0)); - let c5 = _mm_set1_ps(*matrix.get_unchecked(1).get_unchecked(1)); - let c6 = _mm_set1_ps(*matrix.get_unchecked(1).get_unchecked(2)); - let c7 = _mm_set1_ps(*matrix.get_unchecked(2).get_unchecked(0)); - let c8 = _mm_set1_ps(*matrix.get_unchecked(2).get_unchecked(1)); - let c9 = _mm_set1_ps(*matrix.get_unchecked(2).get_unchecked(2)); + let c1 = _mm_set1_ps(matrix[0][0]); + let c2 = _mm_set1_ps(matrix[0][1]); + let c3 = _mm_set1_ps(matrix[0][2]); + let c4 = _mm_set1_ps(matrix[1][0]); + let c5 = _mm_set1_ps(matrix[1][1]); + let c6 = _mm_set1_ps(matrix[1][2]); + let c7 = _mm_set1_ps(matrix[2][0]); + let c8 = _mm_set1_ps(matrix[2][1]); + let c9 = _mm_set1_ps(matrix[2][2]); let src_channels = 3usize; - while cx + 4 < width as usize { - let offset_src_ptr = - ((src as *const u8).add(src_offset) as *const f32).add(cx * src_channels); + while cx + 4 <= width as usize { + let offset_src_ptr = unsafe { src.get_unchecked(cx * src_channels..).as_ptr() }; let src_ptr_0 = offset_src_ptr; @@ -115,22 +114,32 @@ pub unsafe fn sse_xyz_to_channels< src_ptr_0, c1, c2, c3, c4, c5, c6, c7, c8, c9, ); - let dst_ptr = ((dst as *mut u8).add(dst_offset) as *mut f32).add(cx * channels); + let dst_ptr = unsafe { dst.get_unchecked_mut(cx * channels..).as_mut_ptr() }; if USE_ALPHA { - let offset_a_src_ptr = ((a_channel as *const u8).add(a_offset) as *const f32).add(cx); - let a_row = _mm_loadu_ps(offset_a_src_ptr); - - store_and_interleave_v4_f32!( - dst_ptr, - image_configuration, - r_row0_, - g_row0_, - b_row0_, - a_row - ); + let offset_a_src_ptr = unsafe { a_channel.get_unchecked(cx) }; + let a_row = unsafe { _mm_loadu_ps(offset_a_src_ptr) }; + + unsafe { + store_and_interleave_v4_f32!( + dst_ptr, + image_configuration, + r_row0_, + g_row0_, + b_row0_, + a_row + ); + } } else { - store_and_interleave_v3_f32!(dst_ptr, image_configuration, r_row0_, g_row0_, b_row0_); + unsafe { + store_and_interleave_v3_f32!( + dst_ptr, + image_configuration, + r_row0_, + g_row0_, + b_row0_ + ); + } } cx += 4; diff --git a/src/sse/xyza_laba_to_image.rs b/src/sse/xyza_laba_to_image.rs index 6d85315..b2c0ad8 100644 --- a/src/sse/xyza_laba_to_image.rs +++ b/src/sse/xyza_laba_to_image.rs @@ -14,8 +14,9 @@ use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; -#[inline(always)] -pub unsafe fn sse_xyza_lab_vld( +#[inline] +#[target_feature(enable = "sse4.1")] +pub(crate) fn sse_xyza_lab_vld( src: *const f32, c1: __m128, c2: __m128, @@ -27,52 +28,52 @@ pub unsafe fn sse_xyza_lab_vld (__m128, __m128, __m128, __m128) { - let target: XyzTarget = TARGET.into(); - let pixel_0 = _mm_loadu_ps(src); - let pixel_1 = _mm_loadu_ps(src.add(4)); - let pixel_2 = _mm_loadu_ps(src.add(8)); - let pixel_3 = _mm_loadu_ps(src.add(12)); - let (mut r_f32, mut g_f32, mut b_f32, a_f32) = - sse_deinterleave_rgba_ps(pixel_0, pixel_1, pixel_2, pixel_3); - - match target { - XyzTarget::Lab => { - let (x, y, z) = sse_lab_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; - } - XyzTarget::Luv => { - let (x, y, z) = sse_luv_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; - } - XyzTarget::Lch => { - let (x, y, z) = sse_lch_to_xyz(r_f32, g_f32, b_f32); - r_f32 = x; - g_f32 = y; - b_f32 = z; + unsafe { + let target: XyzTarget = TARGET.into(); + let pixel_0 = _mm_loadu_ps(src); + let pixel_1 = _mm_loadu_ps(src.add(4)); + let pixel_2 = _mm_loadu_ps(src.add(8)); + let pixel_3 = _mm_loadu_ps(src.add(12)); + let (mut r_f32, mut g_f32, mut b_f32, a_f32) = + sse_deinterleave_rgba_ps(pixel_0, pixel_1, pixel_2, pixel_3); + + match target { + XyzTarget::Lab => { + let (x, y, z) = sse_lab_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + XyzTarget::Luv => { + let (x, y, z) = sse_luv_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + XyzTarget::Lch => { + let (x, y, z) = sse_lch_to_xyz(r_f32, g_f32, b_f32); + r_f32 = x; + g_f32 = y; + b_f32 = z; + } + _ => {} } - _ => {} - } - let (linear_r, linear_g, linear_b) = - _mm_color_matrix_ps(r_f32, g_f32, b_f32, c1, c2, c3, c4, c5, c6, c7, c8, c9); + let (linear_r, linear_g, linear_b) = + _mm_color_matrix_ps(r_f32, g_f32, b_f32, c1, c2, c3, c4, c5, c6, c7, c8, c9); - r_f32 = linear_r; - g_f32 = linear_g; - b_f32 = linear_b; - (r_f32, g_f32, b_f32, a_f32) + r_f32 = linear_r; + g_f32 = linear_g; + b_f32 = linear_b; + (r_f32, g_f32, b_f32, a_f32) + } } #[target_feature(enable = "sse4.1")] -pub unsafe fn sse_xyza_to_image( +pub(crate) fn sse_xyza_to_image( start_cx: usize, - src: *const f32, - src_offset: usize, - dst: *mut f32, - dst_offset: usize, + src: &[f32], + dst: &mut [f32], width: u32, matrix: &[[f32; 3]; 3], ) -> usize { @@ -85,20 +86,20 @@ pub unsafe fn sse_xyza_to_image { @@ -117,10 +118,12 @@ pub unsafe fn sse_xyza_to_image Rgb { + match source { + XyzTarget::Lab => Lab::new(px[0], px[1], px[2]).to_linear_rgb(matrix), + XyzTarget::Xyz => Xyz::new(px[0], px[1], px[2]).to_linear_rgb(matrix), + XyzTarget::Luv => Luv::new(px[0], px[1], px[2]).to_linear_rgb(matrix), + XyzTarget::Lch => LCh::new(px[0], px[1], px[2]).to_linear_rgb(matrix), + } +} + +fn write_lut_rgb(dst: &mut [u8], transient: &[f32], lut: &[u8; 2049], cfg: ImageConfiguration) { + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(transient.as_chunks::<3>().0.iter()) + { + let r = (src[cfg.r_index()].clamp(0., 1.) * 2048.).round() as u16; + let g = (src[cfg.g_index()].clamp(0., 1.) * 2048.).round() as u16; + let b = (src[cfg.b_index()].clamp(0., 1.) * 2048.).round() as u16; + dst[cfg.r_index()] = lut[r.min(2048) as usize]; + dst[cfg.g_index()] = lut[g.min(2048) as usize]; + dst[cfg.b_index()] = lut[b.min(2048) as usize]; + } +} + +fn write_lut_rgba(dst: &mut [u8], transient: &[f32], lut: &[u8; 2049], cfg: ImageConfiguration) { + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(transient.as_chunks::<4>().0.iter()) + { + let r = (src[cfg.r_index()].clamp(0., 1.) * 2048.).round() as u16; + let g = (src[cfg.g_index()].clamp(0., 1.) * 2048.).round() as u16; + let b = (src[cfg.b_index()].clamp(0., 1.) * 2048.).round() as u16; + dst[cfg.r_index()] = lut[r.min(2048) as usize]; + dst[cfg.g_index()] = lut[g.min(2048) as usize]; + dst[cfg.b_index()] = lut[b.min(2048) as usize]; + dst[cfg.a_index()] = (src[cfg.a_index()] * 255.).clamp(0., 255.).round() as u8; + } +} fn xyz_to_channels( - src: &[f32], - src_stride: u32, - a_channel: &[f32], - a_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, + a_plane: Option<&ImageBuffer<'_, f32>>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let source: XyzTarget = TARGET.into(); +) -> Result<(), ColorError> { + dst.validate()?; + src.validate()?; + dst.try_match_immutable(src)?; + if src.channels != 3 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } + if USE_ALPHA && dst.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(4)); + } else if !USE_ALPHA && dst.channels != 3 { + return Err(ColorError::UnsupportedChannelsCount(3)); + } + if let Some(plane) = a_plane + && USE_ALPHA + { + dst.try_match_immutable(plane)?; + } + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + let source: XyzTarget = TARGET.into(); + if USE_ALPHA && !image_configuration.has_alpha() { panic!("Alpha may be set only on images with alpha"); } + if USE_ALPHA && a_plane.is_none() { + panic!("Alpha plane must be provided when USE_ALPHA is true"); + } - let channels = image_configuration.channel_count(); - - #[allow(clippy::type_complexity)] let mut _wide_row_handler: Option< - unsafe fn( - usize, - *const f32, - usize, - *const f32, - usize, - *mut f32, - usize, - u32, - &[[f32; 3]; 3], - ) -> usize, + unsafe fn(usize, &[f32], &[f32], &mut [f32], u32, &[[f32; 3]; 3]) -> usize, > = None; #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] if std::arch::is_x86_feature_detected!("sse4.1") { + use crate::sse::sse_xyz_to_channels; _wide_row_handler = Some(sse_xyz_to_channels::); } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] if std::arch::is_x86_feature_detected!("avx2") { + use crate::avx::avx_xyz_to_channels; _wide_row_handler = Some(avx_xyz_to_channels::); } #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] { + use crate::neon::neon_xyz_to_channels; _wide_row_handler = Some(neon_xyz_to_channels::); } - let src_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - src.as_ptr() as *mut u8, - src_stride as usize * height as usize, - ) - }; + let channels = image_configuration.channel_count(); - let mut lut_table = vec![0u8; 2049]; - for (i, element) in lut_table.iter_mut().enumerate() { - *element = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.).min(255.) as u8; + let mut lut_table = [0u8; 2049]; + for (i, lut) in lut_table.iter_mut().enumerate() { + *lut = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.) + .round() + .min(255.) as u8; } - #[cfg(feature = "rayon")] - { - if USE_ALPHA { - let a_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - a_channel.as_ptr() as *mut u8, - a_stride as usize * height as usize, - ) - }; - - dst.par_chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.par_chunks_exact_mut(src_stride as usize)) - .zip(a_slice_safe_align.par_chunks_exact(a_stride as usize)) - .for_each(|((dst, src), a_channel)| unsafe { - let mut _cx = 0usize; - - let mut transient_row = vec![0f32; width as usize * channels]; - - if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher( - _cx, - src.as_ptr() as *const f32, - 0, - a_channel.as_ptr() as *const f32, - 0, - transient_row.as_mut_ptr(), - 0, - width, - matrix, - ); - } - - let src_ptr = src.as_ptr() as *mut f32; - - for x in _cx..width as usize { - let src_slice = src_ptr.add(x * 3); - let l_x = src_slice.read_unaligned(); - let l_y = src_slice.add(1).read_unaligned(); - let l_z = src_slice.add(2).read_unaligned(); - let rgb = match source { - XyzTarget::Lab => { - let lab = Lab::new(l_x, l_y, l_z); - lab.to_linear_rgb(matrix) - } - XyzTarget::Xyz => { - let xyz = Xyz::new(l_x, l_y, l_z); - xyz.to_linear_rgb(matrix) - } - XyzTarget::Luv => { - let luv = Luv::new(l_x, l_y, l_z); - luv.to_linear_rgb(matrix) - } - XyzTarget::Lch => { - let lch = LCh::new(l_x, l_y, l_z); - lch.to_linear_rgb(matrix) - } - }; - - let dst = transient_row.get_unchecked_mut((x * channels)..); - *dst.get_unchecked_mut(image_configuration.r_index()) = rgb.r; - *dst.get_unchecked_mut(image_configuration.g_index()) = rgb.g; - *dst.get_unchecked_mut(image_configuration.b_index()) = rgb.b; - if image_configuration.has_alpha() { - let a_ptr = a_channel.as_ptr() as *const f32; - let a_f = a_ptr.add(x).read_unaligned(); - *dst.get_unchecked_mut(image_configuration.a_index()) = a_f; - } - } - - for (dst_chunk, src_chunks) in dst - .chunks_exact_mut(channels) - .zip(transient_row.chunks_exact(channels)) - { - let r_cast = (src_chunks[image_configuration.r_index()].min(1.).max(0.) - * 2048f32) - .round() as usize; - let g_cast = (src_chunks[image_configuration.g_index()].min(1.).max(0.) - * 2048f32) - .round() as usize; - let b_cast = (src_chunks[image_configuration.b_index()].min(1.).max(0.) - * 2048f32) - .round() as usize; - - dst_chunk[image_configuration.r_index()] = - *lut_table.get_unchecked(r_cast.min(2048)); - dst_chunk[image_configuration.g_index()] = - *lut_table.get_unchecked(g_cast.min(2048)); - dst_chunk[image_configuration.b_index()] = - *lut_table.get_unchecked(b_cast.min(2048)); - - if image_configuration.has_alpha() { - let a_cast = (src_chunks[image_configuration.a_index()] * 255.) - .min(255.) - .max(0.) as u8; - dst_chunk[image_configuration.a_index()] = a_cast; - } - } - }); - } else { - dst.par_chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.par_chunks_exact_mut(src_stride as usize)) - .for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; - - let mut transient_row = vec![0f32; width as usize * channels]; - - if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher( - _cx, - src.as_ptr() as *const f32, - 0, - a_channel.as_ptr(), - 0, - transient_row.as_mut_ptr(), - 0, - width, - matrix, - ); - } - - let src_ptr = src.as_ptr() as *mut f32; - - for x in _cx..width as usize { - let src_slice = src_ptr.add(x * 3); - let l_x = src_slice.read_unaligned(); - let l_y = src_slice.add(1).read_unaligned(); - let l_z = src_slice.add(2).read_unaligned(); - let rgb = match source { - XyzTarget::Lab => { - let lab = Lab::new(l_x, l_y, l_z); - lab.to_linear_rgb(matrix) - } - XyzTarget::Xyz => { - let xyz = Xyz::new(l_x, l_y, l_z); - xyz.to_linear_rgb(matrix) - } - XyzTarget::Luv => { - let luv = Luv::new(l_x, l_y, l_z); - luv.to_linear_rgb(matrix) - } - XyzTarget::Lch => { - let lch = LCh::new(l_x, l_y, l_z); - lch.to_linear_rgb(matrix) - } - }; - - let dst = transient_row.get_unchecked_mut((x * channels)..); - *dst.get_unchecked_mut(image_configuration.r_index()) = rgb.r; - *dst.get_unchecked_mut(image_configuration.g_index()) = rgb.g; - *dst.get_unchecked_mut(image_configuration.b_index()) = rgb.b; - } - - for (dst_chunk, src_chunks) in dst - .chunks_exact_mut(channels) - .zip(transient_row.chunks_exact(channels)) - { - let r_cast = (src_chunks[image_configuration.r_index()].min(1.).max(0.) - * 2048f32) - .round() as usize; - let g_cast = (src_chunks[image_configuration.g_index()].min(1.).max(0.) - * 2048f32) - .round() as usize; - let b_cast = (src_chunks[image_configuration.b_index()].min(1.).max(0.) - * 2048f32) - .round() as usize; - - *dst_chunk.get_unchecked_mut(image_configuration.r_index()) = - *lut_table.get_unchecked(r_cast.min(2048)); - *dst_chunk.get_unchecked_mut(image_configuration.g_index()) = - *lut_table.get_unchecked(g_cast.min(2048)); - *dst_chunk.get_unchecked_mut(image_configuration.b_index()) = - *lut_table.get_unchecked(b_cast.min(2048)); - } - }); - } - } + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; + let width = src.width; + + let mut transient_row = vec![0f32; width as usize * channels]; + + let src_rows = src.data.chunks(src.stride()); + let dst_rows = dst.data.borrow_mut().chunks_mut(dst_stride); + + if USE_ALPHA { + let a_plane = a_plane.unwrap(); + for ((dst, src), a_row) in dst_rows + .zip(src_rows) + .zip(a_plane.data.chunks(a_plane.stride())) + { + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; + let a_row = &a_row[..width as usize]; + + let mut cx = 0usize; + if let Some(dispatcher) = _wide_row_handler { + cx = unsafe { dispatcher(0, src, a_row, &mut transient_row, width, matrix) }; + } - #[cfg(not(feature = "rayon"))] - { - if USE_ALPHA { - let a_slice_safe_align = unsafe { - slice::from_raw_parts_mut( - a_channel.as_ptr() as *mut u8, - a_stride as usize * height as usize, - ) - }; - - for ((dst, src), a_channel) in dst - .chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.chunks_exact_mut(src_stride as usize)) - .zip(a_slice_safe_align.chunks_exact(a_stride as usize)) + for ((transient, src), &a) in transient_row[cx * channels..] + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src[cx * 3..].as_chunks::<3>().0.iter()) + .zip(a_row[cx..].iter()) { - unsafe { - let mut _cx = 0usize; - - let mut transient_row = vec![0f32; width as usize * channels]; - - if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher( - _cx, - src.as_ptr() as *const f32, - 0, - a_channel.as_ptr() as *const f32, - 0, - transient_row.as_mut_ptr(), - 0, - width, - matrix, - ); - } - - let src_ptr = src.as_ptr() as *mut f32; - - for x in _cx..width as usize { - let src_slice = src_ptr.add(x * 3); - let l_x = src_slice.read_unaligned(); - let l_y = src_slice.add(1).read_unaligned(); - let l_z = src_slice.add(2).read_unaligned(); - let rgb = match source { - XyzTarget::Lab => { - let lab = Lab::new(l_x, l_y, l_z); - lab.to_linear_rgb(matrix) - } - XyzTarget::Xyz => { - let xyz = Xyz::new(l_x, l_y, l_z); - xyz.to_linear_rgb(matrix) - } - XyzTarget::Luv => { - let luv = Luv::new(l_x, l_y, l_z); - luv.to_linear_rgb(matrix) - } - XyzTarget::Lch => { - let lch = LCh::new(l_x, l_y, l_z); - lch.to_linear_rgb(matrix) - } - }; - - let dst = transient_row.get_unchecked_mut((x * channels)..); - *dst.get_unchecked_mut(image_configuration.get_r_channel_offset()) = rgb.r; - *dst.get_unchecked_mut(image_configuration.get_g_channel_offset()) = rgb.g; - *dst.get_unchecked_mut(image_configuration.get_b_channel_offset()) = rgb.b; - if image_configuration.has_alpha() { - let a_ptr = a_channel.as_ptr() as *const f32; - let a_f = a_ptr.add(x).read_unaligned(); - *dst.get_unchecked_mut(image_configuration.get_a_channel_offset()) = - a_f; - } - } - - for (dst_chunk, src_chunks) in dst - .chunks_exact_mut(channels) - .zip(transient_row.chunks_exact(channels)) - { - let r_cast = (src_chunks[image_configuration.get_r_channel_offset()] - .min(1.) - .max(0.) - * 2048f32) - .round() as usize; - let g_cast = (src_chunks[image_configuration.get_g_channel_offset()] - .min(1.) - .max(0.) - * 2048f32) - .round() as usize; - let b_cast = (src_chunks[image_configuration.get_b_channel_offset()] - .min(1.) - .max(0.) - * 2048f32) - .round() as usize; - - dst_chunk[image_configuration.get_r_channel_offset()] = - *lut_table.get_unchecked(r_cast.min(2048)); - dst_chunk[image_configuration.get_g_channel_offset()] = - *lut_table.get_unchecked(g_cast.min(2048)); - dst_chunk[image_configuration.get_b_channel_offset()] = - *lut_table.get_unchecked(b_cast.min(2048)); - - if image_configuration.has_alpha() { - let a_cast = (src_chunks[image_configuration.get_a_channel_offset()] - * 255.) - .min(255.) - .max(0.) as u8; - dst_chunk[image_configuration.get_a_channel_offset()] = a_cast; - } - } - } + let rgb = xyz_target_to_rgb(source, src, matrix); + transient[image_configuration.r_index()] = rgb.r; + transient[image_configuration.g_index()] = rgb.g; + transient[image_configuration.b_index()] = rgb.b; + transient[image_configuration.a_index()] = a; + } + + write_lut_rgba(dst, &transient_row, &lut_table, image_configuration); + } + } else { + for (dst, src) in dst_rows.zip(src_rows) { + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; + + let mut cx = 0usize; + if let Some(dispatcher) = _wide_row_handler { + cx = unsafe { dispatcher(0, src, &[], &mut transient_row, width, matrix) }; } - } else { - for (dst, src) in dst - .chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.chunks_exact_mut(src_stride as usize)) + + for (transient, src) in transient_row[cx * channels..] + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src[cx * 3..].as_chunks::<3>().0.iter()) { - unsafe { - let mut _cx = 0usize; - - let mut transient_row = vec![0f32; width as usize * channels]; - - if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher( - _cx, - src.as_ptr() as *const f32, - 0, - a_channel.as_ptr(), - 0, - transient_row.as_mut_ptr(), - 0, - width, - matrix, - ); - } - - let src_ptr = src.as_ptr() as *mut f32; - - for x in _cx..width as usize { - let src_slice = src_ptr.add(x * 3); - let l_x = src_slice.read_unaligned(); - let l_y = src_slice.add(1).read_unaligned(); - let l_z = src_slice.add(2).read_unaligned(); - let rgb = match source { - XyzTarget::Lab => { - let lab = Lab::new(l_x, l_y, l_z); - lab.to_linear_rgb(matrix) - } - XyzTarget::Xyz => { - let xyz = Xyz::new(l_x, l_y, l_z); - xyz.to_linear_rgb(matrix) - } - XyzTarget::Luv => { - let luv = Luv::new(l_x, l_y, l_z); - luv.to_linear_rgb(matrix) - } - XyzTarget::Lch => { - let lch = LCh::new(l_x, l_y, l_z); - lch.to_linear_rgb(matrix) - } - }; - - let dst = transient_row.get_unchecked_mut((x * channels)..); - *dst.get_unchecked_mut(image_configuration.get_r_channel_offset()) = rgb.r; - *dst.get_unchecked_mut(image_configuration.get_g_channel_offset()) = rgb.g; - *dst.get_unchecked_mut(image_configuration.get_b_channel_offset()) = rgb.b; - } - - for (dst_chunk, src_chunks) in dst - .chunks_exact_mut(channels) - .zip(transient_row.chunks_exact(channels)) - { - let r_cast = (src_chunks[image_configuration.get_r_channel_offset()] - .min(1.) - .max(0.) - * 2048f32) - .round() as usize; - let g_cast = (src_chunks[image_configuration.get_g_channel_offset()] - .min(1.) - .max(0.) - * 2048f32) - .round() as usize; - let b_cast = (src_chunks[image_configuration.get_b_channel_offset()] - .min(1.) - .max(0.) - * 2048f32) - .round() as usize; - - dst_chunk[image_configuration.get_r_channel_offset()] = - *lut_table.get_unchecked(r_cast.min(2048)); - dst_chunk[image_configuration.get_g_channel_offset()] = - *lut_table.get_unchecked(g_cast.min(2048)); - dst_chunk[image_configuration.get_b_channel_offset()] = - *lut_table.get_unchecked(b_cast.min(2048)); - } - } + let rgb = xyz_target_to_rgb(source, src, matrix); + transient[image_configuration.r_index()] = rgb.r; + transient[image_configuration.g_index()] = rgb.g; + transient[image_configuration.b_index()] = rgb.b; } + + write_lut_rgb(dst, &transient_row, &lut_table, image_configuration); } } + + Ok(()) } -/// This function converts XYZ to RGB. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains XYZ data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive RGB data -/// * `dst_stride` - Bytes per row for dst data -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn xyz_to_rgb( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty = ImageBuffer::from_vec(vec![0.; 1], 1, 1, 1, 1)?; xyz_to_channels::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Xyz as u8 }>( src, - src_stride, - &empty_vec, - 0, dst, - dst_stride, - width, - height, + Some(&empty), matrix, transfer_function, - ); + ) } -/// This function converts XYZ to BGR. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains XYZ data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive BGR data -/// * `dst_stride` - Bytes per row for dst data -/// * `matrix` - Transformation matrix from BGR to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn xyz_to_bgr( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty = ImageBuffer::from_vec(vec![0.; 1], 1, 1, 1, 1)?; xyz_to_channels::<{ ImageConfiguration::Bgr as u8 }, false, { XyzTarget::Xyz as u8 }>( src, - src_stride, - &empty_vec, - 0, dst, - dst_stride, - width, - height, + Some(&empty), matrix, transfer_function, - ); + ) } -/// This function converts XYZ to sRGB D65 White point. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains XYZ data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive RGB data -/// * `dst_stride` - Bytes per row for dst data pub fn xyz_to_srgb( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, -) { - let empty_vec = vec![]; + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, +) -> Result<(), ColorError> { + let empty = ImageBuffer::from_vec(vec![0.; 1], 1, 1, 1, 1)?; xyz_to_channels::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Xyz as u8 }>( src, - src_stride, - &empty_vec, - 0, dst, - dst_stride, - width, - height, + Some(&empty), &XYZ_TO_SRGB_D65, TransferFunction::Srgb, - ); + ) } -/// This function converts LAB to RGB. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LAB data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive RGB data -/// * `dst_stride` - Bytes per row for dst data -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn lab_to_rgb( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty = ImageBuffer::from_vec(vec![0.; 1], 1, 1, 1, 1)?; xyz_to_channels::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Lab as u8 }>( src, - src_stride, - &empty_vec, - 0, dst, - dst_stride, - width, - height, + Some(&empty), matrix, transfer_function, - ); + ) } -/// This function converts LAB to BGR. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LAB data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive BGR data -/// * `dst_stride` - Bytes per row for dst data -/// * `matrix` - Transformation matrix from BGR to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn lab_to_bgr( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty = ImageBuffer::from_vec(vec![0.; 1], 1, 1, 1, 1)?; xyz_to_channels::<{ ImageConfiguration::Bgr as u8 }, false, { XyzTarget::Lab as u8 }>( src, - src_stride, - &empty_vec, - 0, dst, - dst_stride, - width, - height, + Some(&empty), matrix, transfer_function, - ); + ) } -/// This function converts LAB to RGB. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LAB data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive RGB data -/// * `dst_stride` - Bytes per row for dst data pub fn lab_to_srgb( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, -) { - let empty_vec = vec![]; + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, +) -> Result<(), ColorError> { + let empty = ImageBuffer::from_vec(vec![0.; 1], 1, 1, 1, 1)?; xyz_to_channels::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Lab as u8 }>( src, - src_stride, - &empty_vec, - 0, dst, - dst_stride, - width, - height, + Some(&empty), &XYZ_TO_SRGB_D65, TransferFunction::Srgb, - ); + ) } -/// This function converts LAB with separate alpha channel to RGBA. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LAB data -/// * `src_stride` - Bytes per row for src data. -/// * `a_plane` - A slice contains Alpha data -/// * `a_stride` - Bytes per row for alpha plane data -/// * `dst` - A mutable slice to receive RGB data -/// * `dst_stride` - Bytes per row for dst data -/// * `width` - Image width -/// * `height` - Image height pub fn laba_to_srgb( - src: &[f32], - src_stride: u32, - a_plane: &[f32], - a_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, -) { + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, + a_plane: &ImageBuffer<'_, f32>, +) -> Result<(), ColorError> { xyz_to_channels::<{ ImageConfiguration::Rgba as u8 }, true, { XyzTarget::Lab as u8 }>( src, - src_stride, - a_plane, - a_stride, dst, - dst_stride, - width, - height, + Some(a_plane), &XYZ_TO_SRGB_D65, TransferFunction::Srgb, - ); + ) } -/// This function converts XYZ with separate alpha channel to RGBA. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LAB data -/// * `src_stride` - Bytes per row for src data. -/// * `a_plane` - A slice contains Alpha data -/// * `a_stride` - Bytes per row for alpha plane data -/// * `dst` - A mutable slice to receive RGB data -/// * `dst_stride` - Bytes per row for dst data -/// * `width` - Image width -/// * `height` - Image height -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn xyza_to_rgba( - src: &[f32], - src_stride: u32, - a_plane: &[f32], - a_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, + a_plane: &ImageBuffer<'_, f32>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { xyz_to_channels::<{ ImageConfiguration::Rgba as u8 }, true, { XyzTarget::Xyz as u8 }>( src, - src_stride, - a_plane, - a_stride, dst, - dst_stride, - width, - height, + Some(a_plane), matrix, transfer_function, - ); + ) } -/// This function converts LUV to RGB. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LAB data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive RGB data -/// * `dst_stride` - Bytes per row for dst data -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn luv_to_rgb( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty = ImageBuffer::from_vec(vec![0.; 1], 1, 1, 1, 1)?; xyz_to_channels::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Luv as u8 }>( src, - src_stride, - &empty_vec, - 0, dst, - dst_stride, - width, - height, + Some(&empty), matrix, transfer_function, - ); + ) } -/// This function converts LUV to RGB. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LAB data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive RGB data -/// * `dst_stride` - Bytes per row for dst data -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn luv_to_bgr( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty = ImageBuffer::from_vec(vec![0.; 1], 1, 1, 1, 1)?; xyz_to_channels::<{ ImageConfiguration::Bgr as u8 }, false, { XyzTarget::Luv as u8 }>( src, - src_stride, - &empty_vec, - 0, dst, - dst_stride, - width, - height, + Some(&empty), matrix, transfer_function, - ); + ) } -/// This function converts LCH to RGB. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LAB data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive RGB data -/// * `dst_stride` - Bytes per row for dst data -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn lch_to_rgb( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty = ImageBuffer::from_vec(vec![0.; 1], 1, 1, 1, 1)?; xyz_to_channels::<{ ImageConfiguration::Rgb as u8 }, false, { XyzTarget::Lch as u8 }>( src, - src_stride, - &empty_vec, - 0, dst, - dst_stride, - width, - height, + Some(&empty), matrix, transfer_function, - ); + ) } -/// This function converts LCH to RGB. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LAB data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive RGB data -/// * `dst_stride` - Bytes per row for dst data -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn lch_to_bgr( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let empty_vec = vec![]; +) -> Result<(), ColorError> { + let empty = ImageBuffer::from_vec(vec![0.; 1], 1, 1, 1, 1)?; xyz_to_channels::<{ ImageConfiguration::Bgr as u8 }, false, { XyzTarget::Lch as u8 }>( src, - src_stride, - &empty_vec, - 0, dst, - dst_stride, - width, - height, + Some(&empty), matrix, transfer_function, - ); + ) +} + +#[cfg(test)] +mod tests_xyz_to_channels { + use super::*; + use crate::BufferStore; + + fn make_src_f32( + data: Vec, + width: u32, + height: u32, + channels: u32, + ) -> ImageBuffer<'static, f32> { + ImageBuffer::from_vec(data, width, height, width * channels, channels).unwrap() + } + + fn make_dst(width: u32, height: u32, channels: u32) -> ImageBufferMut<'static, u8> { + ImageBufferMut::new( + BufferStore::Owned(vec![0u8; (width * height * channels) as usize]), + width, + height, + width * channels, + channels, + ) + .unwrap() + } + + fn make_alpha(data: Vec, width: u32, height: u32) -> ImageBuffer<'static, f32> { + ImageBuffer::from_vec(data, width, height, width, 1).unwrap() + } + + fn assert_approx(a: u8, b: u8, eps: u8, label: &str) { + assert!( + (a as i16 - b as i16).abs() <= eps as i16, + "{label}: got {a}, expected {b}" + ); + } + + // Convert known linear RGB → colour space for use as round-trip input + fn linear_rgb_to_xyz(r: f32, g: f32, b: f32) -> (f32, f32, f32) { + let rgb = Rgb::::new(r, g, b); + let xyz = Xyz::from_linear_rgb(rgb, &crate::SRGB_TO_XYZ_D65); + (xyz.x, xyz.y, xyz.z) + } + + fn linear_rgb_to_lab(r: f32, g: f32, b: f32) -> (f32, f32, f32) { + let rgb = Rgb::::new(r, g, b); + let xyz = Xyz::from_linear_rgb(rgb, &crate::SRGB_TO_XYZ_D65); + let lab = Lab::from_xyz(xyz); + (lab.l, lab.a, lab.b) + } + + fn linear_rgb_to_luv(r: f32, g: f32, b: f32) -> (f32, f32, f32) { + let rgb = Rgb::::new(r, g, b); + let xyz = Xyz::from_linear_rgb(rgb, &crate::SRGB_TO_XYZ_D65); + let luv = Luv::from_xyz(xyz); + (luv.l, luv.u, luv.v) + } + + fn linear_rgb_to_lch(r: f32, g: f32, b: f32) -> (f32, f32, f32) { + let rgb = Rgb::::new(r, g, b); + let xyz = Xyz::from_linear_rgb(rgb, &crate::SRGB_TO_XYZ_D65); + let luv = Luv::from_xyz(xyz); + let lch = LCh::from_luv(luv); + (lch.l, lch.c, lch.h) + } + + // ── xyz_to_rgb ──────────────────────────────────────────────────────────── + + #[test] + fn xyz_to_rgb_black_roundtrip() { + let (x, y, z) = linear_rgb_to_xyz(0.0, 0.0, 0.0); + let src = make_src_f32(vec![x, y, z], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + xyz_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 0, 2, "R black"); + assert_approx(d[1], 0, 2, "G black"); + assert_approx(d[2], 0, 2, "B black"); + } + + #[test] + fn xyz_to_rgb_white_roundtrip() { + let (x, y, z) = linear_rgb_to_xyz(1.0, 1.0, 1.0); + let src = make_src_f32(vec![x, y, z], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + xyz_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R white"); + assert_approx(d[1], 255, 2, "G white"); + assert_approx(d[2], 255, 2, "B white"); + } + + #[test] + fn xyz_to_srgb_matches_xyz_to_rgb() { + let (x, y, z) = linear_rgb_to_xyz(0.4, 0.6, 0.8); + let src1 = make_src_f32(vec![x, y, z], 1, 1, 3); + let src2 = make_src_f32(vec![x, y, z], 1, 1, 3); + let mut dst1 = make_dst(1, 1, 3); + let mut dst2 = make_dst(1, 1, 3); + xyz_to_srgb(&src1, &mut dst1).unwrap(); + xyz_to_rgb(&src2, &mut dst2, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst1.data.borrow(), &*dst2.data.borrow()); + } + + // ── xyz_to_bgr ──────────────────────────────────────────────────────────── + + #[test] + fn xyz_to_bgr_white_roundtrip() { + let (x, y, z) = linear_rgb_to_xyz(1.0, 1.0, 1.0); + let src = make_src_f32(vec![x, y, z], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + xyz_to_bgr(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "B white"); + assert_approx(d[1], 255, 2, "G white"); + assert_approx(d[2], 255, 2, "R white"); + } + + #[test] + fn xyz_to_rgb_bgr_agree_on_grey() { + let (x, y, z) = linear_rgb_to_xyz(0.5, 0.5, 0.5); + let src_rgb = make_src_f32(vec![x, y, z], 1, 1, 3); + let src_bgr = make_src_f32(vec![x, y, z], 1, 1, 3); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + xyz_to_rgb( + &src_rgb, + &mut dst_rgb, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + xyz_to_bgr( + &src_bgr, + &mut dst_bgr, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + assert_eq!( + &*dst_rgb.data.borrow(), + &*dst_bgr.data.borrow(), + "grey invariant to channel order" + ); + } + + #[test] + fn xyz_to_bgr_channel_order_for_red() { + let (x, y, z) = linear_rgb_to_xyz(1.0, 0.0, 0.0); + let src = make_src_f32(vec![x, y, z], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + xyz_to_bgr(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + // BGR: d[0]=B d[1]=G d[2]=R + assert!(d[0] < 20, "B low for red, got {}", d[0]); + assert!(d[1] < 20, "G low for red, got {}", d[1]); + assert!(d[2] > 200, "R dominant for red, got {}", d[2]); + } + + // ── lab_to_rgb ──────────────────────────────────────────────────────────── + + #[test] + fn lab_to_rgb_black_roundtrip() { + let (l, a, b) = linear_rgb_to_lab(0.0, 0.0, 0.0); + let src = make_src_f32(vec![l, a, b], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + lab_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 0, 2, "R black"); + assert_approx(d[1], 0, 2, "G black"); + assert_approx(d[2], 0, 2, "B black"); + } + + #[test] + fn lab_to_rgb_white_roundtrip() { + let (l, a, b) = linear_rgb_to_lab(1.0, 1.0, 1.0); + let src = make_src_f32(vec![l, a, b], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + lab_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R white"); + assert_approx(d[1], 255, 2, "G white"); + assert_approx(d[2], 255, 2, "B white"); + } + + #[test] + fn lab_to_srgb_matches_lab_to_rgb() { + let (l, a, b) = linear_rgb_to_lab(0.3, 0.5, 0.8); + let src1 = make_src_f32(vec![l, a, b], 1, 1, 3); + let src2 = make_src_f32(vec![l, a, b], 1, 1, 3); + let mut dst1 = make_dst(1, 1, 3); + let mut dst2 = make_dst(1, 1, 3); + lab_to_srgb(&src1, &mut dst1).unwrap(); + lab_to_rgb(&src2, &mut dst2, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + assert_eq!(&*dst1.data.borrow(), &*dst2.data.borrow()); + } + + #[test] + fn lab_to_bgr_white_roundtrip() { + let (l, a, b) = linear_rgb_to_lab(1.0, 1.0, 1.0); + let src = make_src_f32(vec![l, a, b], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + lab_to_bgr(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "B"); + assert_approx(d[1], 255, 2, "G"); + assert_approx(d[2], 255, 2, "R"); + } + + #[test] + fn lab_to_rgb_bgr_agree_on_grey() { + let (l, a, b) = linear_rgb_to_lab(0.5, 0.5, 0.5); + let src_rgb = make_src_f32(vec![l, a, b], 1, 1, 3); + let src_bgr = make_src_f32(vec![l, a, b], 1, 1, 3); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + lab_to_rgb( + &src_rgb, + &mut dst_rgb, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + lab_to_bgr( + &src_bgr, + &mut dst_bgr, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + assert_eq!(&*dst_rgb.data.borrow(), &*dst_bgr.data.borrow()); + } + + // ── laba_to_srgb ────────────────────────────────────────────────────────── + + #[test] + fn laba_to_srgb_white_opaque() { + let (l, a, b) = linear_rgb_to_lab(1.0, 1.0, 1.0); + let src = make_src_f32(vec![l, a, b], 1, 1, 3); + let alpha = make_alpha(vec![1.0], 1, 1); + let mut dst = make_dst(1, 1, 4); + laba_to_srgb(&src, &mut dst, &alpha).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R"); + assert_approx(d[1], 255, 2, "G"); + assert_approx(d[2], 255, 2, "B"); + assert_approx(d[3], 255, 1, "A"); + } + + #[test] + fn laba_to_srgb_alpha_denormalized() { + let (l, a, b) = linear_rgb_to_lab(0.5, 0.5, 0.5); + for alpha in [0.0f32, 0.25, 0.5, 0.75, 1.0] { + let src = make_src_f32(vec![l, a, b], 1, 1, 3); + let a_plane = make_alpha(vec![alpha], 1, 1); + let mut dst = make_dst(1, 1, 4); + laba_to_srgb(&src, &mut dst, &a_plane).unwrap(); + let d = dst.data.borrow(); + let expected = (alpha * 255.).round() as u8; + assert_approx(d[3], expected, 1, &format!("alpha={alpha}")); + } + } + + #[test] + fn laba_to_srgb_rgb_encoded_when_alpha_zero() { + let (l, a, b) = linear_rgb_to_lab(1.0, 1.0, 1.0); + let src = make_src_f32(vec![l, a, b], 1, 1, 3); + let a_plane = make_alpha(vec![0.0], 1, 1); + let mut dst = make_dst(1, 1, 4); + laba_to_srgb(&src, &mut dst, &a_plane).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R encoded despite alpha=0"); + assert_eq!(d[3], 0, "A must be 0"); + } + + // ── xyza_to_rgba ────────────────────────────────────────────────────────── + + #[test] + fn xyza_to_rgba_white_opaque() { + let (x, y, z) = linear_rgb_to_xyz(1.0, 1.0, 1.0); + let src = make_src_f32(vec![x, y, z], 1, 1, 3); + let alpha = make_alpha(vec![1.0], 1, 1); + let mut dst = make_dst(1, 1, 4); + xyza_to_rgba( + &src, + &mut dst, + &alpha, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R"); + assert_approx(d[1], 255, 2, "G"); + assert_approx(d[2], 255, 2, "B"); + assert_approx(d[3], 255, 1, "A"); + } + + #[test] + fn xyza_to_rgba_alpha_denormalized() { + let (x, y, z) = linear_rgb_to_xyz(0.5, 0.5, 0.5); + for alpha in [0.0f32, 0.25, 0.5, 0.75, 1.0] { + let src = make_src_f32(vec![x, y, z], 1, 1, 3); + let a_plane = make_alpha(vec![alpha], 1, 1); + let mut dst = make_dst(1, 1, 4); + xyza_to_rgba( + &src, + &mut dst, + &a_plane, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + let d = dst.data.borrow(); + let expected = (alpha * 255.).round() as u8; + assert_approx(d[3], expected, 1, &format!("alpha={alpha}")); + } + } + + // ── luv_to_rgb / luv_to_bgr ─────────────────────────────────────────────── + + #[test] + fn luv_to_rgb_white_roundtrip() { + let (l, u, v) = linear_rgb_to_luv(1.0, 1.0, 1.0); + let src = make_src_f32(vec![l, u, v], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + luv_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R white"); + assert_approx(d[1], 255, 2, "G white"); + assert_approx(d[2], 255, 2, "B white"); + } + + #[test] + fn luv_to_rgb_black_roundtrip() { + let (l, u, v) = linear_rgb_to_luv(0.0, 0.0, 0.0); + let src = make_src_f32(vec![l, u, v], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + luv_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 0, 2, "R black"); + assert_approx(d[1], 0, 2, "G black"); + assert_approx(d[2], 0, 2, "B black"); + } + + #[test] + fn luv_to_rgb_bgr_agree_on_grey() { + let (l, u, v) = linear_rgb_to_luv(0.5, 0.5, 0.5); + let src_rgb = make_src_f32(vec![l, u, v], 1, 1, 3); + let src_bgr = make_src_f32(vec![l, u, v], 1, 1, 3); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + luv_to_rgb( + &src_rgb, + &mut dst_rgb, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + luv_to_bgr( + &src_bgr, + &mut dst_bgr, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + assert_eq!(&*dst_rgb.data.borrow(), &*dst_bgr.data.borrow()); + } + + // ── lch_to_rgb / lch_to_bgr ─────────────────────────────────────────────── + + #[test] + fn lch_to_rgb_white_roundtrip() { + let (l, c, h) = linear_rgb_to_lch(1.0, 1.0, 1.0); + let src = make_src_f32(vec![l, c, h], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + lch_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R white"); + assert_approx(d[1], 255, 2, "G white"); + assert_approx(d[2], 255, 2, "B white"); + } + + #[test] + fn lch_to_rgb_black_roundtrip() { + let (l, c, h) = linear_rgb_to_lch(0.0, 0.0, 0.0); + let src = make_src_f32(vec![l, c, h], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + lch_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 0, 2, "R black"); + assert_approx(d[1], 0, 2, "G black"); + assert_approx(d[2], 0, 2, "B black"); + } + + #[test] + fn lch_to_rgb_bgr_agree_on_grey() { + let (l, c, h) = linear_rgb_to_lch(0.5, 0.5, 0.5); + let src_rgb = make_src_f32(vec![l, c, h], 1, 1, 3); + let src_bgr = make_src_f32(vec![l, c, h], 1, 1, 3); + let mut dst_rgb = make_dst(1, 1, 3); + let mut dst_bgr = make_dst(1, 1, 3); + lch_to_rgb( + &src_rgb, + &mut dst_rgb, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + lch_to_bgr( + &src_bgr, + &mut dst_bgr, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + assert_eq!(&*dst_rgb.data.borrow(), &*dst_bgr.data.borrow()); + } + + // ── channel ordering ────────────────────────────────────────────────────── + + #[test] + fn xyz_to_rgb_channel_order_for_red() { + let (x, y, z) = linear_rgb_to_xyz(1.0, 0.0, 0.0); + let src = make_src_f32(vec![x, y, z], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + xyz_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert!(d[0] > 200, "R dominant for red, got {}", d[0]); + assert!(d[1] < 20, "G low for red, got {}", d[1]); + assert!(d[2] < 20, "B low for red, got {}", d[2]); + } + + #[test] + fn lab_to_rgb_channel_order_for_blue() { + let (l, a, b) = linear_rgb_to_lab(0.0, 0.0, 1.0); + let src = make_src_f32(vec![l, a, b], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + lab_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + assert!(d[0] < 20, "R low for blue, got {}", d[0]); + assert!(d[1] < 20, "G low for blue, got {}", d[1]); + assert!(d[2] > 200, "B dominant for blue, got {}", d[2]); + } + + // ── multi-pixel / multi-row consistency ─────────────────────────────────── + + #[test] + fn xyz_to_rgb_multi_pixel_consistent() { + let (x, y, z) = linear_rgb_to_xyz(0.4, 0.6, 0.2); + let src = make_src_f32(vec![x, y, z].repeat(4), 4, 1, 3); + let mut dst = make_dst(4, 1, 3); + xyz_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2]]; + for (i, chunk) in d.chunks(3).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn lab_to_rgb_multi_row_consistent() { + let (l, a, b) = linear_rgb_to_lab(0.6, 0.3, 0.8); + let src = make_src_f32(vec![l, a, b].repeat(4), 2, 2, 3); + let mut dst = make_dst(2, 2, 3); + lab_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2]]; + for (i, chunk) in d.chunks(3).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn laba_to_srgb_multi_pixel_consistent() { + let (l, a, b) = linear_rgb_to_lab(0.3, 0.5, 0.7); + let src = make_src_f32(vec![l, a, b].repeat(4), 4, 1, 3); + let alpha = make_alpha(vec![0.8; 4], 4, 1); + let mut dst = make_dst(4, 1, 4); + laba_to_srgb(&src, &mut dst, &alpha).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2], d[3]]; + for (i, chunk) in d.chunks(4).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + // ── all colour spaces recover same sRGB for known input ─────────────────── + + #[test] + fn all_colour_spaces_agree_on_midgrey() { + let r = 0.5f32; + let g = 0.5f32; + let b = 0.5f32; + + let (x, y, z) = linear_rgb_to_xyz(r, g, b); + let (l, a, bv) = linear_rgb_to_lab(r, g, b); + let (lu, u, v) = linear_rgb_to_luv(r, g, b); + let (lc, c, h) = linear_rgb_to_lch(r, g, b); + + let mut dst_xyz = make_dst(1, 1, 3); + let mut dst_lab = make_dst(1, 1, 3); + let mut dst_luv = make_dst(1, 1, 3); + let mut dst_lch = make_dst(1, 1, 3); + + xyz_to_rgb( + &make_src_f32(vec![x, y, z], 1, 1, 3), + &mut dst_xyz, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + lab_to_rgb( + &make_src_f32(vec![l, a, bv], 1, 1, 3), + &mut dst_lab, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + luv_to_rgb( + &make_src_f32(vec![lu, u, v], 1, 1, 3), + &mut dst_luv, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + lch_to_rgb( + &make_src_f32(vec![lc, c, h], 1, 1, 3), + &mut dst_lch, + &XYZ_TO_SRGB_D65, + TransferFunction::Srgb, + ) + .unwrap(); + + let xyz = dst_xyz.data.borrow(); + let lab = dst_lab.data.borrow(); + let luv = dst_luv.data.borrow(); + let lch = dst_lch.data.borrow(); + + for ch in 0..3 { + assert_approx(lab[ch], xyz[ch], 2, &format!("lab vs xyz ch{ch}")); + assert_approx(luv[ch], xyz[ch], 2, &format!("luv vs xyz ch{ch}")); + assert_approx(lch[ch], xyz[ch], 2, &format!("lch vs xyz ch{ch}")); + } + } + + // ── error paths ─────────────────────────────────────────────────────────── + + #[test] + fn xyz_to_rgb_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0f32; 2], 1, 1, 2, 2).unwrap(); + let mut dst = make_dst(1, 1, 3); + assert!(xyz_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).is_err()); + } + + #[test] + fn lab_to_rgb_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0f32; 2], 1, 1, 2, 2).unwrap(); + let mut dst = make_dst(1, 1, 3); + assert!(lab_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).is_err()); + } + + #[test] + fn luv_to_rgb_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0f32; 2], 1, 1, 2, 2).unwrap(); + let mut dst = make_dst(1, 1, 3); + assert!(luv_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).is_err()); + } + + #[test] + fn lch_to_rgb_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0f32; 2], 1, 1, 2, 2).unwrap(); + let mut dst = make_dst(1, 1, 3); + assert!(lch_to_rgb(&src, &mut dst, &XYZ_TO_SRGB_D65, TransferFunction::Srgb).is_err()); + } } diff --git a/src/xyza_laba_to_image.rs b/src/xyza_laba_to_image.rs index c4ce0d6..9e6a3bd 100644 --- a/src/xyza_laba_to_image.rs +++ b/src/xyza_laba_to_image.rs @@ -5,421 +5,226 @@ * // license that can be found in the LICENSE file. */ -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -use crate::avx::avx_xyza_to_image; use crate::gamma_curves::TransferFunction; use crate::image::ImageConfiguration; -#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -use crate::neon::neon_xyza_to_image; -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -use crate::sse::sse_xyza_to_image; +use crate::xyz_lab_to_image::xyz_target_to_rgb; use crate::xyz_target::XyzTarget; -use crate::{LCh, Lab, Luv, Xyz}; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; -use std::slice; +use crate::{ColorError, ImageBuffer, ImageBufferMut}; -#[allow(clippy::type_complexity)] fn xyz_with_alpha_to_channels( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { - let source: XyzTarget = TARGET.into(); +) -> Result<(), ColorError> { + dst.validate()?; + src.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + let source: XyzTarget = TARGET.into(); + if !image_configuration.has_alpha() { panic!("Alpha may be set only on images with alpha"); } let mut _wide_row_handler: Option< - unsafe fn(usize, *const f32, usize, *mut f32, usize, u32, &[[f32; 3]; 3]) -> usize, + unsafe fn(usize, &[f32], &mut [f32], u32, &[[f32; 3]; 3]) -> usize, > = None; #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] { + use crate::neon::neon_xyza_to_image; _wide_row_handler = Some(neon_xyza_to_image::); } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] if std::arch::is_x86_feature_detected!("sse4.1") { + use crate::sse::sse_xyza_to_image; _wide_row_handler = Some(sse_xyza_to_image::); } #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] if std::arch::is_x86_feature_detected!("avx2") { + use crate::avx::avx_xyza_to_image; _wide_row_handler = Some(avx_xyza_to_image::); } - let mut lut_table = vec![0u8; 2049]; - for (i, element) in lut_table.iter_mut().enumerate() { - *element = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.).min(255.) as u8; - } - - let src_slice_safe_align = unsafe { - slice::from_raw_parts( - src.as_ptr() as *const u8, - src_stride as usize * height as usize, - ) - }; + let channels = image_configuration.channel_count(); - let iter; - #[cfg(feature = "rayon")] - { - iter = dst - .par_chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.par_chunks_exact(src_stride as usize)); - } - #[cfg(not(feature = "rayon"))] - { - iter = dst - .chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.chunks_exact(src_stride as usize)); + let mut lut_table = [0u8; 65536]; + for (i, lut) in lut_table[..2049].iter_mut().enumerate() { + *lut = (transfer_function.gamma(i as f32 * (1. / 2048.0)) * 255.) + .round() + .min(255.) as u8; } - iter.for_each(|(dst, src)| unsafe { - let channels = image_configuration.channel_count(); + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; + let width = src.width; - let mut _cx = 0usize; + let mut transient_row = vec![0f32; width as usize * channels]; - let mut transient_row = vec![0f32; width as usize * channels]; + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) + { + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; + let mut cx = 0usize; if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher( - _cx, - src.as_ptr() as *const f32, - 0, - transient_row.as_mut_ptr(), - 0, - width, - matrix, - ) + cx = unsafe { dispatcher(0, src, &mut transient_row, width, matrix) }; } - let src_ptr = src.as_ptr() as *mut f32; - - for x in _cx..width as usize { - let px = x * 4; - let l_x = src_ptr.add(px).read_unaligned(); - let l_y = src_ptr.add(px + 1).read_unaligned(); - let l_z = src_ptr.add(px + 2).read_unaligned(); - let rgb = match source { - XyzTarget::Lab => { - let lab = Lab::new(l_x, l_y, l_z); - lab.to_linear_rgb(matrix) - } - XyzTarget::Xyz => { - let xyz = Xyz::new(l_x, l_y, l_z); - xyz.to_linear_rgb(matrix) - } - XyzTarget::Luv => { - let luv = Luv::new(l_x, l_y, l_z); - luv.to_linear_rgb(matrix) - } - XyzTarget::Lch => { - let lch = LCh::new(l_x, l_y, l_z); - lch.to_linear_rgb(matrix) - } - }; - - let l_a = src_ptr.add(px + 3).read_unaligned(); - let dst = transient_row.get_unchecked_mut((x * channels)..); - *dst.get_unchecked_mut(image_configuration.r_index()) = rgb.r; - *dst.get_unchecked_mut(image_configuration.g_index()) = rgb.g; - *dst.get_unchecked_mut(image_configuration.b_index()) = rgb.b; - *dst.get_unchecked_mut(image_configuration.a_index()) = l_a; + for (transient, src) in transient_row[cx * channels..] + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src[cx * 4..].as_chunks::<4>().0.iter()) + { + let rgb = xyz_target_to_rgb(source, src, matrix); + transient[image_configuration.r_index()] = rgb.r; + transient[image_configuration.g_index()] = rgb.g; + transient[image_configuration.b_index()] = rgb.b; + transient[image_configuration.a_index()] = src[3]; } - for (dst_chunk, src_chunks) in dst - .chunks_exact_mut(channels) - .zip(transient_row.chunks_exact(channels)) + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(transient_row.as_chunks::<4>().0.iter()) { - let r_cast = - (src_chunks[image_configuration.r_index()].min(1.).max(0.) * 2048f32).round(); - let g_cast = - (src_chunks[image_configuration.g_index()].min(1.).max(0.) * 2048f32).round(); - let b_cast = - (src_chunks[image_configuration.b_index()].min(1.).max(0.) * 2048f32).round(); - let a_cast = (src_chunks[image_configuration.a_index()] * 255.) - .min(255.) - .max(0.) as u8; - - dst_chunk[image_configuration.r_index()] = *lut_table.get_unchecked(r_cast as usize); - dst_chunk[image_configuration.g_index()] = *lut_table.get_unchecked(g_cast as usize); - dst_chunk[image_configuration.b_index()] = *lut_table.get_unchecked(b_cast as usize); - dst_chunk[image_configuration.a_index()] = a_cast; + let r = (src[image_configuration.r_index()] * 2048.).round() as u16; + let g = (src[image_configuration.g_index()] * 2048.).round() as u16; + let b = (src[image_configuration.b_index()] * 2048.).round() as u16; + dst[image_configuration.r_index()] = lut_table[r.min(2048) as usize]; + dst[image_configuration.g_index()] = lut_table[g.min(2048) as usize]; + dst[image_configuration.b_index()] = lut_table[b.min(2048) as usize]; + dst[image_configuration.a_index()] = (src[image_configuration.a_index()] * 255.) + .clamp(0., 255.) + .round() as u8; } - }); + } + + Ok(()) } -/// This function converts LAB with interleaved alpha channel to RGBA. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LAB data -/// * `src_stride` - Bytes per row for src data. -/// * `dst` - A mutable slice to receive RGBA data -/// * `dst_stride` - Bytes per row for dst data -/// * `width` - Image width -/// * `height` - Image height -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn lab_with_alpha_to_rgba( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { xyz_with_alpha_to_channels::<{ ImageConfiguration::Rgba as u8 }, { XyzTarget::Lab as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } -/// This function converts LAB with separate alpha channel to BGRA. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LAB data -/// * `src_stride` - Bytes per row for src data. -/// * `dst` - A mutable slice to receive BGRA data -/// * `dst_stride` - Bytes per row for dst data -/// * `width` - Image width -/// * `height` - Image height -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn lab_with_alpha_to_bgra( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { xyz_with_alpha_to_channels::<{ ImageConfiguration::Bgra as u8 }, { XyzTarget::Lab as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } -/// This function converts LUV with separate alpha channel to RGBA. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LUV data -/// * `src_stride` - Bytes per row for src data. -/// * `dst` - A mutable slice to receive RGBA data -/// * `dst_stride` - Bytes per row for dst data -/// * `width` - Image width -/// * `height` - Image height -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn luv_with_alpha_to_rgba( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { xyz_with_alpha_to_channels::<{ ImageConfiguration::Rgba as u8 }, { XyzTarget::Luv as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } -/// This function converts LUV with separate alpha channel to BGRA. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LUV data -/// * `src_stride` - Bytes per row for src data. -/// * `a_plane` - A slice contains Alpha data -/// * `a_stride` - Bytes per row for alpha plane data -/// * `dst` - A mutable slice to receive BGRA data -/// * `dst_stride` - Bytes per row for dst data -/// * `width` - Image width -/// * `height` - Image height -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn luv_with_alpha_to_bgra( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { xyz_with_alpha_to_channels::<{ ImageConfiguration::Bgra as u8 }, { XyzTarget::Lab as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } -/// This function converts XYZ with separate alpha channel to RGBA. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains XYZa data -/// * `src_stride` - Bytes per row for src data. -/// * `dst` - A mutable slice to receive RGBA data -/// * `dst_stride` - Bytes per row for dst data -/// * `width` - Image width -/// * `height` - Image height -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn xyz_with_alpha_to_rgba( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { xyz_with_alpha_to_channels::<{ ImageConfiguration::Rgba as u8 }, { XyzTarget::Xyz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } -/// This function converts XYZ with separate alpha channel to BGRA. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains XYZ data -/// * `src_stride` - Bytes per row for src data. -/// * `dst` - A mutable slice to receive BGRA data -/// * `dst_stride` - Bytes per row for dst data -/// * `width` - Image width -/// * `height` - Image height -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn xyz_with_alpha_to_bgra( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { xyz_with_alpha_to_channels::<{ ImageConfiguration::Bgra as u8 }, { XyzTarget::Xyz as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } -/// This function converts LCH with separate alpha channel to RGBA. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LCHa data -/// * `src_stride` - Bytes per row for src data. -/// * `dst` - A mutable slice to receive RGBA data -/// * `dst_stride` - Bytes per row for dst data -/// * `width` - Image width -/// * `height` - Image height -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn lch_with_alpha_to_rgba( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { xyz_with_alpha_to_channels::<{ ImageConfiguration::Rgba as u8 }, { XyzTarget::Lch as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } -/// This function converts LCH with separate alpha channel to BGRA. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains LCHa data -/// * `src_stride` - Bytes per row for src data. -/// * `dst` - A mutable slice to receive BGRA data -/// * `dst_stride` - Bytes per row for dst data -/// * `width` - Image width -/// * `height` - Image height -/// * `matrix` - Transformation matrix from RGB to XYZ. If you don't have specific just pick `XYZ_TO_SRGB_D65` -/// * `transfer_function` - Transfer function. If you don't have specific pick `Srgb` pub fn lch_with_alpha_to_bgra( - src: &[f32], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, f32>, + dst: &mut ImageBufferMut<'_, u8>, matrix: &[[f32; 3]; 3], transfer_function: TransferFunction, -) { +) -> Result<(), ColorError> { xyz_with_alpha_to_channels::<{ ImageConfiguration::Bgra as u8 }, { XyzTarget::Lch as u8 }>( src, - src_stride, dst, - dst_stride, - width, - height, matrix, transfer_function, - ); + ) } From 5ea7d230627c3783d72c3e94b55d74ec5ed8319e Mon Sep 17 00:00:00 2001 From: Radzivon Bartoshyk Date: Sat, 11 Apr 2026 18:47:01 +0100 Subject: [PATCH 5/9] API improvements --- .github/workflows/build_push.yml | 8 +- Cargo.lock | 2 +- Cargo.toml | 2 +- src/app/src/main.rs | 1 - src/gamma_curves.rs | 6 +- src/hsv_to_image.rs | 771 ++++++++++++++++++++----------- src/image_to_xyz_lab.rs | 6 +- src/image_xyza_laba.rs | 1 - src/lib.rs | 3 +- src/neon/hsv_to_image.rs | 69 +-- src/neon/image_to_hsv.rs | 452 ++++++------------ src/neon/mod.rs | 2 +- src/sse/hsv_to_image.rs | 81 ++-- src/sse/image_to_hsv.rs | 2 +- src/sse/mod.rs | 8 +- src/sse/support.rs | 2 +- 16 files changed, 762 insertions(+), 654 deletions(-) diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 0a3cf03..7b2573c 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -20,9 +20,14 @@ jobs: build: name: Build runs-on: ubuntu-latest + strategy: + matrix: + toolchain: [ stable, "1.89.0" ] steps: - uses: actions/checkout@v6 - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} - run: rustup target add aarch64-unknown-linux-gnu x86_64-unknown-linux-gnu i686-unknown-linux-gnu powerpc-unknown-linux-gnu - run: RUSTFLAGS="-C target-feature=+neon" cargo build --all-features --target aarch64-unknown-linux-gnu - run: RUSTFLAGS="-C target-feature=+sse4.1" cargo build --all-features --target i686-unknown-linux-gnu @@ -31,4 +36,5 @@ jobs: - run: RUSTFLAGS="-C target-feature=+sse4.1" cargo build --all-features --target x86_64-unknown-linux-gnu - run: RUSTFLAGS="-C target-feature=+avx2" cargo build --all-features --target x86_64-unknown-linux-gnu - name: Test release pipeline - run: cargo publish --dry-run \ No newline at end of file + run: cargo publish --dry-run + if: matrix.toolchain == 'stable' \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 2a1997f..aecdae3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,7 +163,7 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorutils-rs" -version = "0.7.6" +version = "0.8.0" dependencies = [ "erydanos", "half", diff --git a/Cargo.toml b/Cargo.toml index a56dfb8..37eb986 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ workspace = { members = ["src/app"] } [package] name = "colorutils-rs" -version = "0.7.6" +version = "0.8.0" edition = "2024" description = "High performance utilities for color format handling and conversion." readme = "README.md" diff --git a/src/app/src/main.rs b/src/app/src/main.rs index 368c9bc..dd8508a 100644 --- a/src/app/src/main.rs +++ b/src/app/src/main.rs @@ -88,7 +88,6 @@ fn main() { // let dst_stride = width * 4 * std::mem::size_of::() as u32; // append_alpha(&mut destination, dst_stride, &store, store_stride as u32, &alpha_store, alpha_stride as u32, width, height); - let lab_stride = width as usize * 3usize * std::mem::size_of::(); // // let mut src_shift = 0usize; // for _ in 0..height as usize { diff --git a/src/gamma_curves.rs b/src/gamma_curves.rs index fb03695..958b618 100644 --- a/src/gamma_curves.rs +++ b/src/gamma_curves.rs @@ -183,11 +183,11 @@ pub fn bt1361_to_linear(gamma: f32) -> f32 { /// Pure gamma transfer function for gamma 2.2 pub fn pure_gamma_function(x: f32, gamma: f32) -> f32 { if x <= 0f32 { - 0f32 + 0. } else if x >= 1f32 { - return 1f32; + 1. } else { - return x.powf(gamma); + x.powf(gamma) } } diff --git a/src/hsv_to_image.rs b/src/hsv_to_image.rs index 77ef02e..9f2d885 100644 --- a/src/hsv_to_image.rs +++ b/src/hsv_to_image.rs @@ -5,338 +5,577 @@ * // license that can be found in the LICENSE file. */ -use std::slice; - use crate::image::ImageConfiguration; use crate::image_to_hsv_support::HsvTarget; -#[cfg(all(target_arch = "aarch64", target_feature = "neon"))] -use crate::neon::neon_hsv_u16_to_image; -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -use crate::sse::sse_hsv_u16_to_image; -use crate::{Hsl, Hsv}; -#[cfg(feature = "rayon")] -use rayon::iter::{IndexedParallelIterator, ParallelIterator}; -#[cfg(feature = "rayon")] -use rayon::prelude::{ParallelSlice, ParallelSliceMut}; - -#[allow(clippy::type_complexity)] +use crate::{ColorError, Hsl, Hsv, ImageBuffer, ImageBufferMut}; + fn hsv_u16_to_channels< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, const TARGET: u8, >( - src: &[u16], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u16>, + dst: &mut ImageBufferMut<'_, u8>, scale: f32, -) { - let target: HsvTarget = TARGET.into(); +) -> Result<(), ColorError> { + dst.validate()?; + src.validate()?; + dst.try_match_immutable_with_channels(src)?; + if src.channels != 3 && src.channels != 4 { + return Err(ColorError::UnsupportedChannelsCount(src.channels)); + } + let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); + let target: HsvTarget = TARGET.into(); + if USE_ALPHA && !image_configuration.has_alpha() { panic!("Alpha may be set only on images with alpha"); } - let mut _wide_row_handler: Option< - unsafe fn(usize, *const u16, usize, u32, *mut u8, usize, f32) -> usize, - > = None; + let mut _wide_row_handler: Option usize> = + None; #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] if std::arch::is_x86_feature_detected!("sse4.1") { + use crate::sse::sse_hsv_u16_to_image; _wide_row_handler = Some(sse_hsv_u16_to_image::); } #[cfg(all(target_arch = "aarch64", target_feature = "neon"))] { + use crate::neon::neon_hsv_u16_to_image; _wide_row_handler = Some(neon_hsv_u16_to_image::); } - let channels = image_configuration.channel_count(); + let inv_scale = 1f32 / scale; - let scale = 1f32 / scale; - - #[cfg(feature = "rayon")] - { - let src_slice_safe_align = unsafe { - slice::from_raw_parts( - src.as_ptr() as *const u8, - src_stride as usize * height as usize, - ) - }; - dst.par_chunks_exact_mut(dst_stride as usize) - .zip(src_slice_safe_align.par_chunks_exact(src_stride as usize)) - .for_each(|(dst, src)| unsafe { - let mut _cx = 0usize; - - if let Some(dispatcher) = _wide_row_handler { - _cx = dispatcher( - _cx, - src.as_ptr() as *const u16, - 0, - width, - dst.as_mut_ptr(), - 0, - scale, - ); - } - - let src_ptr = src.as_ptr() as *const u16; - let dst_ptr = dst.as_mut_ptr(); - - let dst_slice = slice::from_raw_parts_mut(dst_ptr, width as usize * channels); - - for x in _cx..width as usize { - let px = x * channels; - let src = src_ptr.add(px); - let h = src.read_unaligned(); - let s = src.add(1).read_unaligned(); - let v = src.add(2).read_unaligned(); - - let s_f = s as f32 * scale; - let v_f = v as f32 * scale; - - let hx = x * channels; - let rgb = match target { - HsvTarget::Hsv => { - let hsv = Hsv::from_components(h as f32, s_f, v_f); - hsv.to_rgb8() - } - HsvTarget::Hsl => { - let hsl = Hsl::from_components(h as f32, s_f, v_f); - hsl.to_rgb8() - } - }; - - *dst_slice.get_unchecked_mut(hx + image_configuration.r_index()) = rgb.r; - *dst_slice.get_unchecked_mut(hx + image_configuration.g_index()) = rgb.g; - *dst_slice.get_unchecked_mut(hx + image_configuration.b_index()) = rgb.b; - - if image_configuration.has_alpha() { - let a = src.add(3).read_unaligned(); - *dst_slice.get_unchecked_mut(hx + image_configuration.a_index()) = a as u8; - } - } - }); - } + let dst_stride = dst.stride(); + let src_r_width = src.width * src.channels; + let dst_r_width = dst.width * dst.channels; + let width = src.width; - #[cfg(not(feature = "rayon"))] + for (dst, src) in dst + .data + .borrow_mut() + .chunks_mut(dst_stride) + .zip(src.data.chunks(src.stride())) { - let mut src_offset = 0usize; - let mut dst_offset = 0usize; - - for _ in 0..height as usize { - let mut _cx = 0usize; - - if let Some(dispatcher) = _wide_row_handler { - unsafe { - _cx = dispatcher( - _cx, - src.as_ptr(), - src_offset, - width, - dst.as_mut_ptr(), - dst_offset, - scale, - ); - } - } - - let src_ptr = unsafe { (src.as_ptr() as *const u8).add(src_offset) as *const u16 }; - let dst_ptr = unsafe { dst.as_mut_ptr().add(dst_offset) }; - - let dst_slice = - unsafe { slice::from_raw_parts_mut(dst_ptr, width as usize * channels) }; + let src = &src[..src_r_width as usize]; + let dst = &mut dst[..dst_r_width as usize]; - for x in _cx..width as usize { - let px = x * channels; - let src = unsafe { src_ptr.add(px) }; - let h = unsafe { src.read_unaligned() }; - let s = unsafe { src.add(1).read_unaligned() }; - let v = unsafe { src.add(2).read_unaligned() }; + let mut cx = 0usize; - let s_f = s as f32 * scale; - let v_f = v as f32 * scale; + if let Some(dispatch) = _wide_row_handler { + cx = unsafe { dispatch(0, src, dst, width, inv_scale) }; + } - let hx = x * channels; + if image_configuration == ImageConfiguration::Bgr + || image_configuration == ImageConfiguration::Rgb + { + let dst = &mut dst[cx * 3..]; + let src = &src[cx * 3..]; + for (dst, src) in dst + .as_chunks_mut::<3>() + .0 + .iter_mut() + .zip(src.as_chunks::<3>().0.iter()) + { + let h = src[0] as f32; + let s = src[1] as f32 * inv_scale; + let v = src[2] as f32 * inv_scale; let rgb = match target { - HsvTarget::Hsv => { - let hsv = Hsv::from_components(h as f32, s_f, v_f); - hsv.to_rgb8() - } - HsvTarget::Hsl => { - let hsl = Hsl::from_components(h as f32, s_f, v_f); - hsl.to_rgb8() - } + HsvTarget::Hsv => Hsv::from_components(h, s, v).to_rgb8(), + HsvTarget::Hsl => Hsl::from_components(h, s, v).to_rgb8(), }; - - unsafe { - *dst_slice.get_unchecked_mut(hx + image_configuration.get_r_channel_offset()) = - rgb.r; - *dst_slice.get_unchecked_mut(hx + image_configuration.get_g_channel_offset()) = - rgb.g; - *dst_slice.get_unchecked_mut(hx + image_configuration.get_b_channel_offset()) = - rgb.b; - } - - if image_configuration.has_alpha() { - let a = unsafe { src.add(3).read_unaligned() }; - unsafe { - *dst_slice - .get_unchecked_mut(hx + image_configuration.get_a_channel_offset()) = - a as u8; - } + dst[image_configuration.r_index()] = rgb.r; + dst[image_configuration.g_index()] = rgb.g; + dst[image_configuration.b_index()] = rgb.b; + } + } else if image_configuration == ImageConfiguration::Rgba + || image_configuration == ImageConfiguration::Bgra + { + let dst = &mut dst[cx * 4..]; + let src = &src[cx * 4..]; + for (dst, src) in dst + .as_chunks_mut::<4>() + .0 + .iter_mut() + .zip(src.as_chunks::<4>().0.iter()) + { + let h = src[0] as f32; + let s = src[1] as f32 * inv_scale; + let v = src[2] as f32 * inv_scale; + let rgb = match target { + HsvTarget::Hsv => Hsv::from_components(h, s, v).to_rgb8(), + HsvTarget::Hsl => Hsl::from_components(h, s, v).to_rgb8(), + }; + dst[image_configuration.r_index()] = rgb.r; + dst[image_configuration.g_index()] = rgb.g; + dst[image_configuration.b_index()] = rgb.b; + if USE_ALPHA { + dst[image_configuration.a_index()] = src[3] as u8; } } - - src_offset += src_stride as usize; - dst_offset += dst_stride as usize; } } + + Ok(()) } -/// This function converts HSV to RGB. This is much more effective than naive direct transformation +/// Converts HSV data stored as `u16` to RGB. /// -/// # Arguments -/// * `src` - A slice contains HSV data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive RGB data -/// * `dst_stride` - Bytes per row for dst data -/// * `scale` - Natural range for S and V is [0,1] it may be more convenient and required for u16 transformation to scale it by 100 or any other number to keep S,V in range [0, scale] +/// Hue is expressed in degrees `[0, 360)`. Saturation and Value are scaled to +/// `[0, scale]` — pass `scale = 1.0` to keep them in the natural `[0, 1]` range, +/// or `scale = 100.0` if they are stored as percentages. pub fn hsv_to_rgb( - src: &[u16], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u16>, + dst: &mut ImageBufferMut<'_, u8>, scale: f32, -) { +) -> Result<(), ColorError> { hsv_u16_to_channels::<{ ImageConfiguration::Rgb as u8 }, false, { HsvTarget::Hsv as u8 }>( - src, src_stride, dst, dst_stride, width, height, scale, - ); + src, dst, scale, + ) } -/// This function converts HSV to BGRA. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains HSV data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive BGRA data -/// * `dst_stride` - Bytes per row for dst data -/// * `scale` - Natural range for S and V is [0,1] it may be more convenient and required for u16 transformation to scale it by 100 or any other number to keep S,V in range [0, scale] pub fn hsv_to_bgra( - src: &[u16], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u16>, + dst: &mut ImageBufferMut<'_, u8>, scale: f32, -) { +) -> Result<(), ColorError> { hsv_u16_to_channels::<{ ImageConfiguration::Bgra as u8 }, true, { HsvTarget::Hsv as u8 }>( - src, src_stride, dst, dst_stride, width, height, scale, - ); + src, dst, scale, + ) } -/// This function converts HSV to RGBA. Alpha channel is copied and leaved unchanged. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains HSV data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive RGBA data -/// * `dst_stride` - Bytes per row for dst data -/// * `scale` - Natural range for S and V is [0,1] it may be more convenient and required for u16 transformation to scale it by 100 or any other number to keep S,V in range [0, scale] pub fn hsv_to_rgba( - src: &[u16], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u16>, + dst: &mut ImageBufferMut<'_, u8>, scale: f32, -) { +) -> Result<(), ColorError> { hsv_u16_to_channels::<{ ImageConfiguration::Rgba as u8 }, true, { HsvTarget::Hsv as u8 }>( - src, src_stride, dst, dst_stride, width, height, scale, - ); + src, dst, scale, + ) } -/// This function converts HSL to RGB. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains HSL data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive RGB data -/// * `dst_stride` - Bytes per row for dst data -/// * `scale` - Natural range for S and L is [0,1] it may be more convenient and required for u16 transformation to scale it by 100 or any other number to keep S,L in range [0, scale] pub fn hsl_to_rgb( - src: &[u16], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u16>, + dst: &mut ImageBufferMut<'_, u8>, scale: f32, -) { +) -> Result<(), ColorError> { hsv_u16_to_channels::<{ ImageConfiguration::Rgb as u8 }, false, { HsvTarget::Hsl as u8 }>( - src, src_stride, dst, dst_stride, width, height, scale, - ); + src, dst, scale, + ) } -/// This function converts HSL to BGRA. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains HSL data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive BGRA data -/// * `dst_stride` - Bytes per row for dst data -/// * `scale` - Natural range for S and V is [0,1] it may be more convenient and required for u16 transformation to scale it by 100 or any other number to keep S,V in range [0, scale] pub fn hsl_to_bgra( - src: &[u16], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u16>, + dst: &mut ImageBufferMut<'_, u8>, scale: f32, -) { +) -> Result<(), ColorError> { hsv_u16_to_channels::<{ ImageConfiguration::Bgra as u8 }, true, { HsvTarget::Hsl as u8 }>( - src, src_stride, dst, dst_stride, width, height, scale, - ); + src, dst, scale, + ) } -/// This function converts HSL to RGBA. Alpha channel is copied and leaved unchanged. This is much more effective than naive direct transformation -/// -/// # Arguments -/// * `src` - A slice contains HSL data -/// * `src_stride` - Bytes per row for src data. -/// * `width` - Image width -/// * `height` - Image height -/// * `dst` - A mutable slice to receive RGBA data -/// * `dst_stride` - Bytes per row for dst data -/// * `scale` - Natural range for S and V is [0,1] it may be more convenient and required for u16 transformation to scale it by 100 or any other number to keep S,V in range [0, scale] pub fn hsl_to_rgba( - src: &[u16], - src_stride: u32, - dst: &mut [u8], - dst_stride: u32, - width: u32, - height: u32, + src: &ImageBuffer<'_, u16>, + dst: &mut ImageBufferMut<'_, u8>, scale: f32, -) { +) -> Result<(), ColorError> { hsv_u16_to_channels::<{ ImageConfiguration::Rgba as u8 }, true, { HsvTarget::Hsl as u8 }>( - src, src_stride, dst, dst_stride, width, height, scale, - ); + src, dst, scale, + ) +} + +#[cfg(test)] +mod tests_hsv_u16_to_channels { + use super::*; + use crate::{BufferStore, Rgb}; + + const SCALE: f32 = 1.0; + const SCALE_100: f32 = 100.0; + + fn make_src( + data: Vec, + width: u32, + height: u32, + channels: u32, + ) -> ImageBuffer<'static, u16> { + ImageBuffer::from_vec(data, width, height, width * channels, channels).unwrap() + } + + fn make_dst(width: u32, height: u32, channels: u32) -> ImageBufferMut<'static, u8> { + ImageBufferMut::new( + BufferStore::Owned(vec![0u8; (width * height * channels) as usize]), + width, + height, + width * channels, + channels, + ) + .unwrap() + } + + fn assert_approx(a: u8, b: u8, eps: u8, label: &str) { + assert!( + (a as i16 - b as i16).abs() <= eps as i16, + "{label}: got {a}, expected {b}" + ); + } + + // Convert known RGB u8 → HSV u16 so we can test the inverse + fn rgb_to_hsv_u16(r: u8, g: u8, b: u8, scale: f32) -> (u16, u16, u16) { + let rgb = Rgb::::new(r, g, b); + let hsv: Hsv = Hsv::from(rgb); + ( + hsv.h as u16, + (hsv.s * scale).round() as u16, + (hsv.v * scale).round() as u16, + ) + } + + fn rgb_to_hsl_u16(r: u8, g: u8, b: u8, scale: f32) -> (u16, u16, u16) { + let rgb = Rgb::::new(r, g, b); + let hsl: Hsl = Hsl::from_rgb(rgb); + ( + hsl.h as u16, + (hsl.s * scale).round() as u16, + (hsl.l * scale).round() as u16, + ) + } + + // ── hsv_to_rgb ──────────────────────────────────────────────────────────── + + #[test] + fn hsv_rgb_black_roundtrip() { + let (h, s, v) = rgb_to_hsv_u16(0, 0, 0, SCALE); + let src = make_src(vec![h, s, v], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + hsv_to_rgb(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 0, 2, "R black"); + assert_approx(d[1], 0, 2, "G black"); + assert_approx(d[2], 0, 2, "B black"); + } + + #[test] + fn hsv_rgb_white_roundtrip() { + let (h, s, v) = rgb_to_hsv_u16(255, 255, 255, SCALE); + let src = make_src(vec![h, s, v], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + hsv_to_rgb(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R white"); + assert_approx(d[1], 255, 2, "G white"); + assert_approx(d[2], 255, 2, "B white"); + } + + #[test] + fn hsv_rgb_red_roundtrip() { + let (h, s, v) = rgb_to_hsv_u16(255, 0, 0, SCALE); + let src = make_src(vec![h, s, v], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + hsv_to_rgb(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert!(d[0] > 200, "R dominant for red, got {}", d[0]); + assert!(d[1] < 20, "G low for red, got {}", d[1]); + assert!(d[2] < 20, "B low for red, got {}", d[2]); + } + + #[test] + fn hsv_rgb_blue_channel_order() { + let (h, s, v) = rgb_to_hsv_u16(0, 0, 255, SCALE); + let src = make_src(vec![h, s, v], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + hsv_to_rgb(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert!(d[0] < 20, "R low for blue, got {}", d[0]); + assert!(d[1] < 20, "G low for blue, got {}", d[1]); + assert!(d[2] > 200, "B dominant for blue, got {}", d[2]); + } + + // ── scale variants ──────────────────────────────────────────────────────── + + #[test] + fn hsv_rgb_scale_100_roundtrip() { + let (h, s, v) = rgb_to_hsv_u16(128, 64, 200, SCALE_100); + let src = make_src(vec![h, s, v], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + hsv_to_rgb(&src, &mut dst, SCALE_100).unwrap(); + let d = dst.data.borrow(); + // Verify the pixel is not all-zero — scale was applied correctly + assert!( + d[0] > 0 || d[1] > 0 || d[2] > 0, + "output should be non-zero" + ); + } + + #[test] + fn hsv_rgba_white_opaque_roundtrip() { + let (h, s, v) = rgb_to_hsv_u16(255, 255, 255, SCALE); + let src = make_src(vec![h, s, v, 255], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + hsv_to_rgba(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R"); + assert_approx(d[1], 255, 2, "G"); + assert_approx(d[2], 255, 2, "B"); + assert_eq!(d[3], 255, "A opaque"); + } + + #[test] + fn hsv_rgba_alpha_passthrough() { + let (h, s, v) = rgb_to_hsv_u16(128, 128, 128, SCALE); + for alpha in [0u16, 64, 128, 192, 255] { + let src = make_src(vec![h, s, v, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + hsv_to_rgba(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[3], alpha as u8, "alpha={alpha} not passed through"); + } + } + + #[test] + fn hsv_bgra_white_opaque_roundtrip() { + let (h, s, v) = rgb_to_hsv_u16(255, 255, 255, SCALE); + let src = make_src(vec![h, s, v, 255], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + hsv_to_bgra(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + // BGRA: d[0]=B d[1]=G d[2]=R d[3]=A + assert_approx(d[0], 255, 2, "B white"); + assert_approx(d[1], 255, 2, "G white"); + assert_approx(d[2], 255, 2, "R white"); + assert_eq!(d[3], 255, "A"); + } + + #[test] + fn hsv_bgra_alpha_passthrough() { + let (h, s, v) = rgb_to_hsv_u16(100, 100, 100, SCALE); + for alpha in [0u16, 128, 255] { + let src = make_src(vec![h, s, v, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + hsv_to_bgra(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[3], alpha as u8, "alpha={alpha}"); + } + } + + #[test] + fn hsv_rgba_bgra_agree_on_grey() { + let (h, s, v) = rgb_to_hsv_u16(150, 150, 150, SCALE); + let src_rgba = make_src(vec![h, s, v, 200], 1, 1, 4); + let src_bgra = make_src(vec![h, s, v, 200], 1, 1, 4); + let mut dst_rgba = make_dst(1, 1, 4); + let mut dst_bgra = make_dst(1, 1, 4); + hsv_to_rgba(&src_rgba, &mut dst_rgba, SCALE).unwrap(); + hsv_to_bgra(&src_bgra, &mut dst_bgra, SCALE).unwrap(); + assert_eq!( + &*dst_rgba.data.borrow(), + &*dst_bgra.data.borrow(), + "grey invariant to channel order" + ); + } + + #[test] + fn hsv_bgra_channel_order_for_red() { + let (h, s, v) = rgb_to_hsv_u16(255, 0, 0, SCALE); + let src = make_src(vec![h, s, v, 255], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + hsv_to_bgra(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert!(d[0] < 20, "B low for red in BGRA, got {}", d[0]); + assert!(d[1] < 20, "G low for red in BGRA, got {}", d[1]); + assert!(d[2] > 200, "R dominant for red in BGRA, got {}", d[2]); + } + + // ── hsl_to_rgb ──────────────────────────────────────────────────────────── + + #[test] + fn hsl_rgb_black_roundtrip() { + let (h, s, l) = rgb_to_hsl_u16(0, 0, 0, SCALE); + let src = make_src(vec![h, s, l], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + hsl_to_rgb(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 0, 2, "R black"); + assert_approx(d[1], 0, 2, "G black"); + assert_approx(d[2], 0, 2, "B black"); + } + + #[test] + fn hsl_rgb_white_roundtrip() { + let (h, s, l) = rgb_to_hsl_u16(255, 255, 255, SCALE); + let src = make_src(vec![h, s, l], 1, 1, 3); + let mut dst = make_dst(1, 1, 3); + hsl_to_rgb(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R white"); + assert_approx(d[1], 255, 2, "G white"); + assert_approx(d[2], 255, 2, "B white"); + } + + #[test] + fn hsl_rgba_white_opaque_roundtrip() { + let (h, s, l) = rgb_to_hsl_u16(255, 255, 255, SCALE); + let src = make_src(vec![h, s, l, 255], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + hsl_to_rgba(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "R"); + assert_approx(d[1], 255, 2, "G"); + assert_approx(d[2], 255, 2, "B"); + assert_eq!(d[3], 255, "A"); + } + + #[test] + fn hsl_rgba_alpha_passthrough() { + let (h, s, l) = rgb_to_hsl_u16(128, 128, 128, SCALE); + for alpha in [0u16, 64, 128, 192, 255] { + let src = make_src(vec![h, s, l, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + hsl_to_rgba(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[3], alpha as u8, "alpha={alpha}"); + } + } + + #[test] + fn hsl_bgra_white_opaque_roundtrip() { + let (h, s, l) = rgb_to_hsl_u16(255, 255, 255, SCALE); + let src = make_src(vec![h, s, l, 255], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + hsl_to_bgra(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert_approx(d[0], 255, 2, "B white"); + assert_approx(d[1], 255, 2, "G white"); + assert_approx(d[2], 255, 2, "R white"); + assert_eq!(d[3], 255, "A"); + } + + #[test] + fn hsl_bgra_alpha_passthrough() { + let (h, s, l) = rgb_to_hsl_u16(100, 100, 100, SCALE); + for alpha in [0u16, 128, 255] { + let src = make_src(vec![h, s, l, alpha], 1, 1, 4); + let mut dst = make_dst(1, 1, 4); + hsl_to_bgra(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + assert_eq!(d[3], alpha as u8, "alpha={alpha}"); + } + } + + #[test] + fn hsl_rgba_bgra_agree_on_grey() { + let (h, s, l) = rgb_to_hsl_u16(180, 180, 180, SCALE); + let src_rgba = make_src(vec![h, s, l, 128], 1, 1, 4); + let src_bgra = make_src(vec![h, s, l, 128], 1, 1, 4); + let mut dst_rgba = make_dst(1, 1, 4); + let mut dst_bgra = make_dst(1, 1, 4); + hsl_to_rgba(&src_rgba, &mut dst_rgba, SCALE).unwrap(); + hsl_to_bgra(&src_bgra, &mut dst_bgra, SCALE).unwrap(); + assert_eq!( + &*dst_rgba.data.borrow(), + &*dst_bgra.data.borrow(), + "grey invariant to channel order" + ); + } + + // ── HSV vs HSL differ for chromatic input ───────────────────────────────── + + #[test] + fn hsv_and_hsl_differ_for_chromatic_input() { + // Same hue/saturation/value-or-lightness → different colour models + // should produce different RGB output for a saturated colour + let h = 120u16; // green hue + let s = 1u16; // full saturation (scale=1) + let v = 1u16; // full value / lightness (scale=1) + + let src_hsv = make_src(vec![h, s, v], 1, 1, 3); + let src_hsl = make_src(vec![h, s, v], 1, 1, 3); + let mut dst_hsv = make_dst(1, 1, 3); + let mut dst_hsl = make_dst(1, 1, 3); + hsv_to_rgb(&src_hsv, &mut dst_hsv, SCALE).unwrap(); + hsl_to_rgb(&src_hsl, &mut dst_hsl, SCALE).unwrap(); + + // HSV(120°,1,1) = pure green (0,255,0) + // HSL(120°,1,1) = white (255,255,255) — lightness=1 is always white + // They must not be identical + assert_ne!( + &*dst_hsv.data.borrow(), + &*dst_hsl.data.borrow(), + "HSV and HSL should produce different output for same raw input" + ); + } + + // ── multi-pixel / multi-row consistency ─────────────────────────────────── + + #[test] + fn hsv_rgb_multi_pixel_consistent() { + let (h, s, v) = rgb_to_hsv_u16(100, 150, 200, SCALE); + let src = make_src(vec![h, s, v].repeat(4), 4, 1, 3); + let mut dst = make_dst(4, 1, 3); + hsv_to_rgb(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2]]; + for (i, chunk) in d.chunks(3).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn hsv_rgba_multi_pixel_consistent() { + let (h, s, v) = rgb_to_hsv_u16(80, 120, 200, SCALE); + let src = make_src(vec![h, s, v, 180].repeat(4), 4, 1, 4); + let mut dst = make_dst(4, 1, 4); + hsv_to_rgba(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2], d[3]]; + for (i, chunk) in d.chunks(4).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + #[test] + fn hsl_rgb_multi_row_consistent() { + let (h, s, l) = rgb_to_hsl_u16(60, 120, 180, SCALE); + let src = make_src(vec![h, s, l].repeat(4), 2, 2, 3); + let mut dst = make_dst(2, 2, 3); + hsl_to_rgb(&src, &mut dst, SCALE).unwrap(); + let d = dst.data.borrow(); + let first = [d[0], d[1], d[2]]; + for (i, chunk) in d.chunks(3).enumerate() { + assert_eq!(chunk, first, "px={i}"); + } + } + + // ── error paths ─────────────────────────────────────────────────────────── + + #[test] + fn hsv_rgb_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0u16; 2], 1, 1, 2, 2).unwrap(); + let mut dst = make_dst(1, 1, 3); + assert!(hsv_to_rgb(&src, &mut dst, SCALE).is_err()); + } + + #[test] + fn hsv_rgba_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0u16; 2], 1, 1, 2, 2).unwrap(); + let mut dst = make_dst(1, 1, 4); + assert!(hsv_to_rgba(&src, &mut dst, SCALE).is_err()); + } + + #[test] + fn hsl_rgb_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0u16; 2], 1, 1, 2, 2).unwrap(); + let mut dst = make_dst(1, 1, 3); + assert!(hsl_to_rgb(&src, &mut dst, SCALE).is_err()); + } + + #[test] + fn hsl_rgba_rejects_wrong_channel_count() { + let src = ImageBuffer::from_vec(vec![0u16; 2], 1, 1, 2, 2).unwrap(); + let mut dst = make_dst(1, 1, 4); + assert!(hsl_to_rgba(&src, &mut dst, SCALE).is_err()); + } } diff --git a/src/image_to_xyz_lab.rs b/src/image_to_xyz_lab.rs index 23eb4ee..aa49d9b 100644 --- a/src/image_to_xyz_lab.rs +++ b/src/image_to_xyz_lab.rs @@ -31,10 +31,8 @@ fn channels_to_xyz( src: &ImageBuffer<'_, u8>, dst: &mut ImageBufferMut<'_, f32>, diff --git a/src/lib.rs b/src/lib.rs index a75e528..4b9cb2e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,8 @@ #![allow( clippy::too_many_arguments, clippy::excessive_precision, - clippy::manual_clamp + clippy::manual_clamp, + clippy::type_complexity )] #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] mod avx; diff --git a/src/neon/hsv_to_image.rs b/src/neon/hsv_to_image.rs index ff94d9b..0c146ba 100644 --- a/src/neon/hsv_to_image.rs +++ b/src/neon/hsv_to_image.rs @@ -11,18 +11,16 @@ use crate::image::ImageConfiguration; use crate::image_to_hsv_support::HsvTarget; use crate::neon::{neon_hsl_to_rgb, neon_hsv_to_rgb}; -#[inline] -pub unsafe fn neon_hsv_u16_to_image< +#[target_feature(enable = "neon")] +pub(crate) fn neon_hsv_u16_to_image< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, const TARGET: u8, >( start_cx: usize, - src: *const u16, - src_offset: usize, + src: &[u16], + dst: &mut [u8], width: u32, - dst: *mut u8, - dst_offset: usize, scale: f32, ) -> usize { let target: HsvTarget = TARGET.into(); @@ -34,22 +32,21 @@ pub unsafe fn neon_hsv_u16_to_image< let channels = image_configuration.channel_count(); let v_scale = vdupq_n_f32(scale); - let dst_ptr = dst.add(dst_offset); - while cx + 16 < width as usize { + while cx + 16 <= width as usize { let (h_chan, s_chan, v_chan, a_chan_lo); - let src_ptr = ((src as *const u8).add(src_offset) as *const u16).add(cx * channels); + let src_ptr = unsafe { src.get_unchecked(cx * channels..).as_ptr() }; match image_configuration { ImageConfiguration::Rgb | ImageConfiguration::Bgr => { - let hsv_pixel = vld3q_u16(src_ptr); + let hsv_pixel = unsafe { vld3q_u16(src_ptr) }; h_chan = hsv_pixel.0; s_chan = hsv_pixel.1; v_chan = hsv_pixel.2; a_chan_lo = vdupq_n_u16(255); } ImageConfiguration::Rgba | ImageConfiguration::Bgra => { - let hsv_pixel = vld4q_u16(src_ptr); + let hsv_pixel = unsafe { vld4q_u16(src_ptr) }; h_chan = hsv_pixel.0; s_chan = hsv_pixel.1; v_chan = hsv_pixel.2; @@ -79,18 +76,18 @@ pub unsafe fn neon_hsv_u16_to_image< let g_chan_16_lo = vcombine_u16(vmovn_u32(g_low), vmovn_u32(g_high)); let b_chan_16_lo = vcombine_u16(vmovn_u32(b_low), vmovn_u32(b_high)); - let src_ptr = src_ptr.add(8 * channels); + let src_ptr = unsafe { src_ptr.add(8 * channels) }; let (h_chan, s_chan, v_chan, a_chan_hi); match image_configuration { ImageConfiguration::Rgb | ImageConfiguration::Bgr => { - let hsv_pixel = vld3q_u16(src_ptr); + let hsv_pixel = unsafe { vld3q_u16(src_ptr) }; h_chan = hsv_pixel.0; s_chan = hsv_pixel.1; v_chan = hsv_pixel.2; a_chan_hi = vdupq_n_u16(255); } ImageConfiguration::Rgba | ImageConfiguration::Bgra => { - let hsv_pixel = vld4q_u16(src_ptr); + let hsv_pixel = unsafe { vld4q_u16(src_ptr) }; h_chan = hsv_pixel.0; s_chan = hsv_pixel.1; v_chan = hsv_pixel.2; @@ -134,7 +131,9 @@ pub unsafe fn neon_hsv_u16_to_image< uint8x16x4_t(b_chan, g_chan, r_chan, a_chan) } }; - vst4q_u8(dst_ptr.add(cx * channels), pixel_set); + unsafe { + vst4q_u8(dst.get_unchecked_mut(cx * channels), pixel_set); + } } else { let pixel_set = match image_configuration { ImageConfiguration::Rgb | ImageConfiguration::Rgba => { @@ -144,26 +143,28 @@ pub unsafe fn neon_hsv_u16_to_image< uint8x16x3_t(b_chan, g_chan, r_chan) } }; - vst3q_u8(dst_ptr.add(cx * channels), pixel_set); + unsafe { + vst3q_u8(dst.get_unchecked_mut(cx * channels), pixel_set); + } } cx += 16; } - while cx + 8 < width as usize { + while cx + 8 <= width as usize { let (h_chan, s_chan, v_chan, a_chan); - let src_ptr = ((src as *const u8).add(src_offset) as *const u16).add(cx * channels); + let src_ptr = unsafe { src.get_unchecked(cx * channels..).as_ptr() }; match image_configuration { ImageConfiguration::Rgb | ImageConfiguration::Bgr => { - let hsv_pixel = vld3q_u16(src_ptr); + let hsv_pixel = unsafe { vld3q_u16(src_ptr) }; h_chan = hsv_pixel.0; s_chan = hsv_pixel.1; v_chan = hsv_pixel.2; a_chan = vdupq_n_u16(255); } ImageConfiguration::Rgba | ImageConfiguration::Bgra => { - let hsv_pixel = vld4q_u16(src_ptr); + let hsv_pixel = unsafe { vld4q_u16(src_ptr) }; h_chan = hsv_pixel.0; s_chan = hsv_pixel.1; v_chan = hsv_pixel.2; @@ -202,11 +203,15 @@ pub unsafe fn neon_hsv_u16_to_image< ImageConfiguration::Rgb => {} ImageConfiguration::Rgba => { let pixel_set = uint8x8x4_t(r_chan, g_chan, b_chan, a_chan); - vst4_u8(dst_ptr.add(cx * channels), pixel_set); + unsafe { + vst4_u8(dst.get_unchecked_mut(cx * channels), pixel_set); + } } ImageConfiguration::Bgra => { let pixel_set = uint8x8x4_t(b_chan, g_chan, r_chan, a_chan); - vst4_u8(dst_ptr.add(cx * channels), pixel_set); + unsafe { + vst4_u8(dst.get_unchecked_mut(cx * channels), pixel_set); + } } ImageConfiguration::Bgr => {} } @@ -214,19 +219,27 @@ pub unsafe fn neon_hsv_u16_to_image< match image_configuration { ImageConfiguration::Rgb => { let pixel_set = uint8x8x3_t(r_chan, g_chan, b_chan); - vst3_u8(dst_ptr.add(cx * channels), pixel_set); + unsafe { + vst3_u8(dst.get_unchecked_mut(cx * channels), pixel_set); + } } ImageConfiguration::Rgba => { - let pixel_set = uint8x8x4_t(r_chan, g_chan, b_chan, vdup_n_u8(0)); - vst4_u8(dst_ptr.add(cx * channels), pixel_set); + let pixel_set = uint8x8x4_t(r_chan, g_chan, b_chan, vdup_n_u8(255)); + unsafe { + vst4_u8(dst.get_unchecked_mut(cx * channels), pixel_set); + } } ImageConfiguration::Bgra => { - let pixel_set = uint8x8x4_t(b_chan, g_chan, r_chan, vdup_n_u8(0)); - vst4_u8(dst_ptr.add(cx * channels), pixel_set); + let pixel_set = uint8x8x4_t(b_chan, g_chan, r_chan, vdup_n_u8(255)); + unsafe { + vst4_u8(dst.get_unchecked_mut(cx * channels), pixel_set); + } } ImageConfiguration::Bgr => { let pixel_set = uint8x8x3_t(b_chan, g_chan, r_chan); - vst3_u8(dst_ptr.add(cx * channels), pixel_set); + unsafe { + vst3_u8(dst.get_unchecked_mut(cx * channels), pixel_set); + } } } } diff --git a/src/neon/image_to_hsv.rs b/src/neon/image_to_hsv.rs index b490e27..e973a5a 100644 --- a/src/neon/image_to_hsv.rs +++ b/src/neon/image_to_hsv.rs @@ -11,19 +11,16 @@ use crate::neon::{neon_rgb_to_hsl, neon_rgb_to_hsv}; use crate::{load_u8_and_deinterleave, load_u8_and_deinterleave_half}; use std::arch::aarch64::*; -#[allow(dead_code)] -#[inline(always)] -pub(crate) unsafe fn neon_channels_to_hsv< +#[target_feature(enable = "neon")] +pub(crate) fn neon_channels_to_hsv_u16< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, const TARGET: u8, >( start_cx: usize, - src: *const u8, - src_offset: usize, + src: &[u8], width: u32, - dst: *mut f32, - dst_offset: usize, + dst: &mut [u16], scale: f32, ) -> usize { let target: HsvTarget = TARGET.into(); @@ -37,324 +34,177 @@ pub(crate) unsafe fn neon_channels_to_hsv< let v_scale = vdupq_n_f32(scale); - let dst_ptr = (dst as *mut u8).add(dst_offset) as *mut f32; - - while cx + 16 < width as usize { - let (r_chan, g_chan, b_chan, a_chan); - let src_ptr = src.add(src_offset + cx * channels); - match image_configuration { - ImageConfiguration::Rgb | ImageConfiguration::Bgr => { - let ldr = vld3q_u8(src_ptr); - if image_configuration == ImageConfiguration::Rgb { - r_chan = ldr.0; - g_chan = ldr.1; - b_chan = ldr.2; - } else { - r_chan = ldr.2; - g_chan = ldr.1; - b_chan = ldr.0; - } - a_chan = vdupq_n_u8(0); - } - ImageConfiguration::Rgba => { - let ldr = vld4q_u8(src_ptr); - r_chan = ldr.0; - g_chan = ldr.1; - b_chan = ldr.2; - a_chan = ldr.3; - } - ImageConfiguration::Bgra => { - let ldr = vld4q_u8(src_ptr); - r_chan = ldr.2; - g_chan = ldr.1; - b_chan = ldr.0; - a_chan = ldr.3; + while cx + 16 <= width as usize { + unsafe { + let src_ptr = src.get_unchecked(cx * channels); + let (r_chan, g_chan, b_chan, a_chan) = + load_u8_and_deinterleave!(src_ptr, image_configuration); + + let r_low = vmovl_u8(vget_low_u8(r_chan)); + let g_low = vmovl_u8(vget_low_u8(g_chan)); + let b_low = vmovl_u8(vget_low_u8(b_chan)); + + let r_low_low = vmovl_u16(vget_low_u16(r_low)); + let g_low_low = vmovl_u16(vget_low_u16(g_low)); + let b_low_low = vmovl_u16(vget_low_u16(b_low)); + + let (x_low_low, y_low_low, z_low_low) = match target { + HsvTarget::Hsv => neon_rgb_to_hsv(r_low_low, g_low_low, b_low_low, v_scale), + HsvTarget::Hsl => neon_rgb_to_hsl(r_low_low, g_low_low, b_low_low, v_scale), + }; + + let a_low = vmovl_u8(vget_low_u8(a_chan)); + + let r_low_high = vmovl_high_u16(r_low); + let g_low_high = vmovl_high_u16(g_low); + let b_low_high = vmovl_high_u16(b_low); + + let (x_low_high, y_low_high, z_low_high) = match target { + HsvTarget::Hsv => neon_rgb_to_hsv(r_low_high, g_low_high, b_low_high, v_scale), + HsvTarget::Hsl => neon_rgb_to_hsl(r_low_high, g_low_high, b_low_high, v_scale), + }; + + let x_low = vcombine_u16( + vmovn_u32(vcvtaq_u32_f32(x_low_low)), + vmovn_u32(vcvtaq_u32_f32(x_low_high)), + ); + let y_low = vcombine_u16( + vmovn_u32(vcvtaq_u32_f32(y_low_low)), + vmovn_u32(vcvtaq_u32_f32(y_low_high)), + ); + let z_low = vcombine_u16( + vmovn_u32(vcvtaq_u32_f32(z_low_low)), + vmovn_u32(vcvtaq_u32_f32(z_low_high)), + ); + + if USE_ALPHA { + let xyz_low_low = uint16x8x4_t(x_low, y_low, z_low, a_low); + vst4q_u16( + dst.get_unchecked_mut(cx * channels..).as_mut_ptr(), + xyz_low_low, + ); + } else { + let xyz_low_low = uint16x8x3_t(x_low, y_low, z_low); + vst3q_u16( + dst.get_unchecked_mut(cx * channels..).as_mut_ptr(), + xyz_low_low, + ); } - } - let r_low = vmovl_u8(vget_low_u8(r_chan)); - let g_low = vmovl_u8(vget_low_u8(g_chan)); - let b_low = vmovl_u8(vget_low_u8(b_chan)); - - let r_low_low = vmovl_u16(vget_low_u16(r_low)); - let g_low_low = vmovl_u16(vget_low_u16(g_low)); - let b_low_low = vmovl_u16(vget_low_u16(b_low)); - - let (x_low_low, y_low_low, z_low_low) = match target { - HsvTarget::Hsv => neon_rgb_to_hsv(r_low_low, g_low_low, b_low_low, v_scale), - HsvTarget::Hsl => neon_rgb_to_hsl(r_low_low, g_low_low, b_low_low, v_scale), - }; - - let a_low = vmovl_u8(vget_low_u8(a_chan)); - if USE_ALPHA { - let a_low_low = - vmulq_n_f32(vcvtq_f32_u32(vmovl_u16(vget_low_u16(a_low))), 1f32 / 255f32); - - let xyz_low_low = float32x4x4_t(x_low_low, y_low_low, z_low_low, a_low_low); - vst4q_f32(dst_ptr.add(cx * channels), xyz_low_low); - } else { - let xyz_low_low = float32x4x3_t(x_low_low, y_low_low, z_low_low); - vst3q_f32(dst_ptr.add(cx * channels), xyz_low_low); - } + let r_high = vmovl_high_u8(r_chan); + let g_high = vmovl_high_u8(g_chan); + let b_high = vmovl_high_u8(b_chan); - let r_low_high = vmovl_high_u16(r_low); - let g_low_high = vmovl_high_u16(g_low); - let b_low_high = vmovl_high_u16(b_low); - - let (x_low_high, y_low_high, z_low_high) = match target { - HsvTarget::Hsv => neon_rgb_to_hsv(r_low_high, g_low_high, b_low_high, v_scale), - HsvTarget::Hsl => neon_rgb_to_hsl(r_low_high, g_low_high, b_low_high, v_scale), - }; - if USE_ALPHA { - let a_low_high = vmulq_n_f32(vcvtq_f32_u32(vmovl_high_u16(a_low)), 1f32 / 255f32); - let xyz_low_low = float32x4x4_t(x_low_high, y_low_high, z_low_high, a_low_high); - vst4q_f32(dst_ptr.add(cx * channels + 4 * channels), xyz_low_low); - } else { - let xyz_low_low = float32x4x3_t(x_low_high, y_low_high, z_low_high); - vst3q_f32(dst_ptr.add(cx * channels + 4 * channels), xyz_low_low); - } + let r_high_low = vmovl_u16(vget_low_u16(r_high)); + let g_high_low = vmovl_u16(vget_low_u16(g_high)); + let b_high_low = vmovl_u16(vget_low_u16(b_high)); - let r_high = vmovl_high_u8(r_chan); - let g_high = vmovl_high_u8(g_chan); - let b_high = vmovl_high_u8(b_chan); + let (x_high_low, y_high_low, z_high_low) = match target { + HsvTarget::Hsv => neon_rgb_to_hsv(r_high_low, g_high_low, b_high_low, v_scale), + HsvTarget::Hsl => neon_rgb_to_hsl(r_high_low, g_high_low, b_high_low, v_scale), + }; - let r_high_low = vmovl_u16(vget_low_u16(r_high)); - let g_high_low = vmovl_u16(vget_low_u16(g_high)); - let b_high_low = vmovl_u16(vget_low_u16(b_high)); + let a_high = vmovl_high_u8(a_chan); - let (x_high_low, y_high_low, z_high_low) = match target { - HsvTarget::Hsv => neon_rgb_to_hsv(r_high_low, g_high_low, b_high_low, v_scale), - HsvTarget::Hsl => neon_rgb_to_hsl(r_high_low, g_high_low, b_high_low, v_scale), - }; + let r_high_high = vmovl_high_u16(r_high); + let g_high_high = vmovl_high_u16(g_high); + let b_high_high = vmovl_high_u16(b_high); - let a_high = vmovl_high_u8(a_chan); + let (x_high_high, y_high_high, z_high_high) = match target { + HsvTarget::Hsv => neon_rgb_to_hsv(r_high_high, g_high_high, b_high_high, v_scale), + HsvTarget::Hsl => neon_rgb_to_hsl(r_high_high, g_high_high, b_high_high, v_scale), + }; - if USE_ALPHA { - let a_high_low = vmulq_n_f32( - vcvtq_f32_u32(vmovl_u16(vget_low_u16(a_high))), - 1f32 / 255f32, + let x_high = vcombine_u16( + vmovn_u32(vcvtaq_u32_f32(x_high_low)), + vmovn_u32(vcvtaq_u32_f32(x_high_high)), + ); + let y_high = vcombine_u16( + vmovn_u32(vcvtaq_u32_f32(y_high_low)), + vmovn_u32(vcvtaq_u32_f32(y_high_high)), + ); + let z_high = vcombine_u16( + vmovn_u32(vcvtaq_u32_f32(z_high_low)), + vmovn_u32(vcvtaq_u32_f32(z_high_high)), ); - let xyz_low_low = float32x4x4_t(x_high_low, y_high_low, z_high_low, a_high_low); - vst4q_f32(dst_ptr.add(cx * channels + 4 * channels * 2), xyz_low_low); - } else { - let xyz_low_low = float32x4x3_t(x_high_low, y_high_low, z_high_low); - vst3q_f32(dst_ptr.add(cx * channels + 4 * channels * 2), xyz_low_low); - } - let r_high_high = vmovl_high_u16(r_high); - let g_high_high = vmovl_high_u16(g_high); - let b_high_high = vmovl_high_u16(b_high); - - let (x_high_high, y_high_high, z_high_high) = match target { - HsvTarget::Hsv => neon_rgb_to_hsv(r_high_high, g_high_high, b_high_high, v_scale), - HsvTarget::Hsl => neon_rgb_to_hsl(r_high_high, g_high_high, b_high_high, v_scale), - }; - - if USE_ALPHA { - let a_high_high = vmulq_n_f32(vcvtq_f32_u32(vmovl_high_u16(a_high)), 1f32 / 255f32); - let xyz_low_low = float32x4x4_t(x_high_high, y_high_high, z_high_high, a_high_high); - vst4q_f32(dst_ptr.add(cx * channels + 4 * channels * 3), xyz_low_low); - } else { - let xyz_low_low = float32x4x3_t(x_high_high, y_high_high, z_high_high); - vst3q_f32(dst_ptr.add(cx * channels + 4 * channels * 3), xyz_low_low); + if USE_ALPHA { + let xyz_low_low = uint16x8x4_t(x_high, y_high, z_high, a_high); + vst4q_u16( + dst.get_unchecked_mut(cx * channels + 8 * channels..) + .as_mut_ptr(), + xyz_low_low, + ); + } else { + let xyz_low_low = uint16x8x3_t(x_high, y_high, z_high); + vst3q_u16( + dst.get_unchecked_mut(cx * channels + 8 * channels..) + .as_mut_ptr(), + xyz_low_low, + ); + } } cx += 16; } - cx -} + while cx + 8 <= width as usize { + unsafe { + let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); -#[inline(always)] -pub(crate) unsafe fn neon_channels_to_hsv_u16< - const CHANNELS_CONFIGURATION: u8, - const USE_ALPHA: bool, - const TARGET: u8, ->( - start_cx: usize, - src: &[u8], - width: u32, - dst: &mut [u16], - scale: f32, -) -> usize { - let target: HsvTarget = TARGET.into(); - let image_configuration: ImageConfiguration = CHANNELS_CONFIGURATION.into(); - let mut cx = start_cx; - if USE_ALPHA && !image_configuration.has_alpha() { - panic!("Use alpha flag used on image without alpha"); - } + let (r_chan, g_chan, b_chan, a_chan) = + load_u8_and_deinterleave_half!(src_ptr, image_configuration); - let channels = image_configuration.channel_count(); + let r_low = vmovl_u8(vget_low_u8(r_chan)); + let g_low = vmovl_u8(vget_low_u8(g_chan)); + let b_low = vmovl_u8(vget_low_u8(b_chan)); - let v_scale = vdupq_n_f32(scale); + let r_low_low = vmovl_u16(vget_low_u16(r_low)); + let g_low_low = vmovl_u16(vget_low_u16(g_low)); + let b_low_low = vmovl_u16(vget_low_u16(b_low)); - while cx + 16 <= width as usize { - let src_ptr = src.get_unchecked(cx * channels); - let (r_chan, g_chan, b_chan, a_chan) = - load_u8_and_deinterleave!(src_ptr, image_configuration); - - let r_low = vmovl_u8(vget_low_u8(r_chan)); - let g_low = vmovl_u8(vget_low_u8(g_chan)); - let b_low = vmovl_u8(vget_low_u8(b_chan)); - - let r_low_low = vmovl_u16(vget_low_u16(r_low)); - let g_low_low = vmovl_u16(vget_low_u16(g_low)); - let b_low_low = vmovl_u16(vget_low_u16(b_low)); - - let (x_low_low, y_low_low, z_low_low) = match target { - HsvTarget::Hsv => neon_rgb_to_hsv(r_low_low, g_low_low, b_low_low, v_scale), - HsvTarget::Hsl => neon_rgb_to_hsl(r_low_low, g_low_low, b_low_low, v_scale), - }; - - let a_low = vmovl_u8(vget_low_u8(a_chan)); - - let r_low_high = vmovl_high_u16(r_low); - let g_low_high = vmovl_high_u16(g_low); - let b_low_high = vmovl_high_u16(b_low); - - let (x_low_high, y_low_high, z_low_high) = match target { - HsvTarget::Hsv => neon_rgb_to_hsv(r_low_high, g_low_high, b_low_high, v_scale), - HsvTarget::Hsl => neon_rgb_to_hsl(r_low_high, g_low_high, b_low_high, v_scale), - }; - - let x_low = vcombine_u16( - vmovn_u32(vcvtaq_u32_f32(x_low_low)), - vmovn_u32(vcvtaq_u32_f32(x_low_high)), - ); - let y_low = vcombine_u16( - vmovn_u32(vcvtaq_u32_f32(y_low_low)), - vmovn_u32(vcvtaq_u32_f32(y_low_high)), - ); - let z_low = vcombine_u16( - vmovn_u32(vcvtaq_u32_f32(z_low_low)), - vmovn_u32(vcvtaq_u32_f32(z_low_high)), - ); - - if USE_ALPHA { - let xyz_low_low = uint16x8x4_t(x_low, y_low, z_low, a_low); - vst4q_u16( - dst.get_unchecked_mut(cx * channels..).as_mut_ptr(), - xyz_low_low, - ); - } else { - let xyz_low_low = uint16x8x3_t(x_low, y_low, z_low); - vst3q_u16( - dst.get_unchecked_mut(cx * channels..).as_mut_ptr(), - xyz_low_low, - ); - } + let (x_low_low, y_low_low, z_low_low) = match target { + HsvTarget::Hsv => neon_rgb_to_hsv(r_low_low, g_low_low, b_low_low, v_scale), + HsvTarget::Hsl => neon_rgb_to_hsl(r_low_low, g_low_low, b_low_low, v_scale), + }; - let r_high = vmovl_high_u8(r_chan); - let g_high = vmovl_high_u8(g_chan); - let b_high = vmovl_high_u8(b_chan); - - let r_high_low = vmovl_u16(vget_low_u16(r_high)); - let g_high_low = vmovl_u16(vget_low_u16(g_high)); - let b_high_low = vmovl_u16(vget_low_u16(b_high)); - - let (x_high_low, y_high_low, z_high_low) = match target { - HsvTarget::Hsv => neon_rgb_to_hsv(r_high_low, g_high_low, b_high_low, v_scale), - HsvTarget::Hsl => neon_rgb_to_hsl(r_high_low, g_high_low, b_high_low, v_scale), - }; - - let a_high = vmovl_high_u8(a_chan); - - let r_high_high = vmovl_high_u16(r_high); - let g_high_high = vmovl_high_u16(g_high); - let b_high_high = vmovl_high_u16(b_high); - - let (x_high_high, y_high_high, z_high_high) = match target { - HsvTarget::Hsv => neon_rgb_to_hsv(r_high_high, g_high_high, b_high_high, v_scale), - HsvTarget::Hsl => neon_rgb_to_hsl(r_high_high, g_high_high, b_high_high, v_scale), - }; - - let x_high = vcombine_u16( - vmovn_u32(vcvtaq_u32_f32(x_high_low)), - vmovn_u32(vcvtaq_u32_f32(x_high_high)), - ); - let y_high = vcombine_u16( - vmovn_u32(vcvtaq_u32_f32(y_high_low)), - vmovn_u32(vcvtaq_u32_f32(y_high_high)), - ); - let z_high = vcombine_u16( - vmovn_u32(vcvtaq_u32_f32(z_high_low)), - vmovn_u32(vcvtaq_u32_f32(z_high_high)), - ); - - if USE_ALPHA { - let xyz_low_low = uint16x8x4_t(x_high, y_high, z_high, a_high); - vst4q_u16( - dst.get_unchecked_mut(cx * channels + 8 * channels..) - .as_mut_ptr(), - xyz_low_low, - ); - } else { - let xyz_low_low = uint16x8x3_t(x_high, y_high, z_high); - vst3q_u16( - dst.get_unchecked_mut(cx * channels + 8 * channels..) - .as_mut_ptr(), - xyz_low_low, - ); - } + let a_low = vmovl_u8(vget_low_u8(a_chan)); - cx += 16; - } + let r_low_high = vmovl_high_u16(r_low); + let g_low_high = vmovl_high_u16(g_low); + let b_low_high = vmovl_high_u16(b_low); - while cx + 8 <= width as usize { - let src_ptr = src.get_unchecked(cx * channels..).as_ptr(); - - let (r_chan, g_chan, b_chan, a_chan) = - load_u8_and_deinterleave_half!(src_ptr, image_configuration); - - let r_low = vmovl_u8(vget_low_u8(r_chan)); - let g_low = vmovl_u8(vget_low_u8(g_chan)); - let b_low = vmovl_u8(vget_low_u8(b_chan)); - - let r_low_low = vmovl_u16(vget_low_u16(r_low)); - let g_low_low = vmovl_u16(vget_low_u16(g_low)); - let b_low_low = vmovl_u16(vget_low_u16(b_low)); - - let (x_low_low, y_low_low, z_low_low) = match target { - HsvTarget::Hsv => neon_rgb_to_hsv(r_low_low, g_low_low, b_low_low, v_scale), - HsvTarget::Hsl => neon_rgb_to_hsl(r_low_low, g_low_low, b_low_low, v_scale), - }; - - let a_low = vmovl_u8(vget_low_u8(a_chan)); - - let r_low_high = vmovl_high_u16(r_low); - let g_low_high = vmovl_high_u16(g_low); - let b_low_high = vmovl_high_u16(b_low); - - let (x_low_high, y_low_high, z_low_high) = match target { - HsvTarget::Hsv => neon_rgb_to_hsv(r_low_high, g_low_high, b_low_high, v_scale), - HsvTarget::Hsl => neon_rgb_to_hsl(r_low_high, g_low_high, b_low_high, v_scale), - }; - - let x_low = vcombine_u16( - vmovn_u32(vcvtaq_u32_f32(x_low_low)), - vmovn_u32(vcvtaq_u32_f32(x_low_high)), - ); - let y_low = vcombine_u16( - vmovn_u32(vcvtaq_u32_f32(y_low_low)), - vmovn_u32(vcvtaq_u32_f32(y_low_high)), - ); - let z_low = vcombine_u16( - vmovn_u32(vcvtaq_u32_f32(z_low_low)), - vmovn_u32(vcvtaq_u32_f32(z_low_high)), - ); - - if USE_ALPHA { - let xyz_low_low = uint16x8x4_t(x_low, y_low, z_low, a_low); - vst4q_u16( - dst.get_unchecked_mut(cx * channels..).as_mut_ptr(), - xyz_low_low, + let (x_low_high, y_low_high, z_low_high) = match target { + HsvTarget::Hsv => neon_rgb_to_hsv(r_low_high, g_low_high, b_low_high, v_scale), + HsvTarget::Hsl => neon_rgb_to_hsl(r_low_high, g_low_high, b_low_high, v_scale), + }; + + let x_low = vcombine_u16( + vmovn_u32(vcvtaq_u32_f32(x_low_low)), + vmovn_u32(vcvtaq_u32_f32(x_low_high)), ); - } else { - let xyz_low_low = uint16x8x3_t(x_low, y_low, z_low); - vst3q_u16( - dst.get_unchecked_mut(cx * channels..).as_mut_ptr(), - xyz_low_low, + let y_low = vcombine_u16( + vmovn_u32(vcvtaq_u32_f32(y_low_low)), + vmovn_u32(vcvtaq_u32_f32(y_low_high)), ); + let z_low = vcombine_u16( + vmovn_u32(vcvtaq_u32_f32(z_low_low)), + vmovn_u32(vcvtaq_u32_f32(z_low_high)), + ); + + if USE_ALPHA { + let xyz_low_low = uint16x8x4_t(x_low, y_low, z_low, a_low); + vst4q_u16( + dst.get_unchecked_mut(cx * channels..).as_mut_ptr(), + xyz_low_low, + ); + } else { + let xyz_low_low = uint16x8x3_t(x_low, y_low, z_low); + vst3q_u16( + dst.get_unchecked_mut(cx * channels..).as_mut_ptr(), + xyz_low_low, + ); + } } cx += 8; diff --git a/src/neon/mod.rs b/src/neon/mod.rs index 5922083..b3c391e 100644 --- a/src/neon/mod.rs +++ b/src/neon/mod.rs @@ -25,7 +25,7 @@ mod xyza_laba_to_image; pub(crate) use colors::*; pub(crate) use from_sigmoidal::neon_from_sigmoidal_row; -pub use hsv_to_image::*; +pub(crate) use hsv_to_image::*; pub(crate) use image_to_hsv::neon_channels_to_hsv_u16; pub(crate) use image_to_jzazbz::neon_image_to_jzazbz; pub(crate) use image_to_oklab::neon_image_to_oklab; diff --git a/src/sse/hsv_to_image.rs b/src/sse/hsv_to_image.rs index 46ea16a..c9c794d 100644 --- a/src/sse/hsv_to_image.rs +++ b/src/sse/hsv_to_image.rs @@ -18,17 +18,15 @@ use std::arch::x86::*; use std::arch::x86_64::*; #[target_feature(enable = "sse4.1")] -pub unsafe fn sse_hsv_u16_to_image< +pub(crate) fn sse_hsv_u16_to_image< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, const TARGET: u8, >( start_cx: usize, - src: *const u16, - src_offset: usize, + src: &[u16], + dst: &mut [u8], width: u32, - dst: *mut u8, - dst_offset: usize, scale: f32, ) -> usize { let target: HsvTarget = TARGET.into(); @@ -42,16 +40,13 @@ pub unsafe fn sse_hsv_u16_to_image< let v_scale = _mm_set1_ps(scale); - let dst_ptr = dst.add(dst_offset); - let src_load_ptr = (src as *const u8).add(src_offset) as *const u16; - - while cx + 16 < width as usize { + while cx + 16 <= width as usize { let (h_chan, s_chan, v_chan, a_chan_lo); - let src_ptr = src_load_ptr.add(cx * channels); + let src_ptr = unsafe { src.get_unchecked(cx * channels..).as_ptr() }; - let row0 = _mm_loadu_si128(src_ptr as *const __m128i); - let row1 = _mm_loadu_si128(src_ptr.add(8) as *const __m128i); - let row2 = _mm_loadu_si128(src_ptr.add(16) as *const __m128i); + let row0 = unsafe { _mm_loadu_si128(src_ptr as *const __m128i) }; + let row1 = unsafe { _mm_loadu_si128(src_ptr.add(8) as *const __m128i) }; + let row2 = unsafe { _mm_loadu_si128(src_ptr.add(16) as *const __m128i) }; match image_configuration { ImageConfiguration::Rgb | ImageConfiguration::Bgr => { @@ -62,7 +57,7 @@ pub unsafe fn sse_hsv_u16_to_image< a_chan_lo = _mm_set1_epi16(255); } ImageConfiguration::Rgba | ImageConfiguration::Bgra => { - let row3 = _mm_loadu_si128(src_ptr.add(24) as *const __m128i); + let row3 = unsafe { _mm_loadu_si128(src_ptr.add(24) as *const __m128i) }; let (h_c, s_c, v_c, a_c) = sse_deinterleave_rgba_epi16(row0, row1, row2, row3); h_chan = h_c; s_chan = s_c; @@ -96,12 +91,12 @@ pub unsafe fn sse_hsv_u16_to_image< let b_chan_16_lo = _mm_packus_epi32(b_low, b_high); let (h_chan, s_chan, v_chan, a_chan_hi); - let src_ptr = src_load_ptr.add(cx * channels); + let src_ptr = unsafe { src.get_unchecked(cx * channels..).as_ptr() }; - let src_ptr = src_ptr.add(8 * channels); - let row0 = _mm_loadu_si128(src_ptr as *const __m128i); - let row1 = _mm_loadu_si128(src_ptr.add(8) as *const __m128i); - let row2 = _mm_loadu_si128(src_ptr.add(16) as *const __m128i); + let src_ptr = unsafe { src_ptr.add(8 * channels) }; + let row0 = unsafe { _mm_loadu_si128(src_ptr as *const __m128i) }; + let row1 = unsafe { _mm_loadu_si128(src_ptr.add(8) as *const __m128i) }; + let row2 = unsafe { _mm_loadu_si128(src_ptr.add(16) as *const __m128i) }; match image_configuration { ImageConfiguration::Rgb | ImageConfiguration::Bgr => { @@ -112,7 +107,7 @@ pub unsafe fn sse_hsv_u16_to_image< a_chan_hi = _mm_set1_epi16(255); } ImageConfiguration::Rgba | ImageConfiguration::Bgra => { - let row3 = _mm_loadu_si128(src_ptr.add(24) as *const __m128i); + let row3 = unsafe { _mm_loadu_si128(src_ptr.add(24) as *const __m128i) }; let (h_c, s_c, v_c, a_c) = sse_deinterleave_rgba_epi16(row0, row1, row2, row3); h_chan = h_c; s_chan = s_c; @@ -149,7 +144,7 @@ pub unsafe fn sse_hsv_u16_to_image< let g_chan = _mm_packus_epi16(g_chan_16_lo, g_chan_16_hi); let b_chan = _mm_packus_epi16(b_chan_16_lo, b_chan_16_hi); - let ptr = dst_ptr.add(cx * channels); + let ptr = unsafe { dst.get_unchecked_mut(cx * channels..).as_mut_ptr() }; if USE_ALPHA { let a_chan = _mm_packus_epi16(a_chan_lo, a_chan_hi); let (rgba0, rgba1, rgba2, rgba3) = match image_configuration { @@ -160,10 +155,12 @@ pub unsafe fn sse_hsv_u16_to_image< sse_interleave_rgba(b_chan, g_chan, r_chan, a_chan) } }; - _mm_storeu_si128(ptr as *mut __m128i, rgba0); - _mm_storeu_si128(ptr.add(16) as *mut __m128i, rgba1); - _mm_storeu_si128(ptr.add(32) as *mut __m128i, rgba2); - _mm_storeu_si128(ptr.add(48) as *mut __m128i, rgba3); + unsafe { + _mm_storeu_si128(ptr as *mut __m128i, rgba0); + _mm_storeu_si128(ptr.add(16) as *mut __m128i, rgba1); + _mm_storeu_si128(ptr.add(32) as *mut __m128i, rgba2); + _mm_storeu_si128(ptr.add(48) as *mut __m128i, rgba3); + } } else { let (rgba0, rgba1, rgba2) = match image_configuration { ImageConfiguration::Rgb | ImageConfiguration::Rgba => { @@ -173,21 +170,23 @@ pub unsafe fn sse_hsv_u16_to_image< sse_interleave_rgb(b_chan, g_chan, r_chan) } }; - _mm_storeu_si128(ptr as *mut __m128i, rgba0); - _mm_storeu_si128(ptr.add(16) as *mut __m128i, rgba1); - _mm_storeu_si128(ptr.add(32) as *mut __m128i, rgba2); + unsafe { + _mm_storeu_si128(ptr as *mut __m128i, rgba0); + _mm_storeu_si128(ptr.add(16) as *mut __m128i, rgba1); + _mm_storeu_si128(ptr.add(32) as *mut __m128i, rgba2); + } } cx += 16; } - while cx + 8 < width as usize { + while cx + 8 <= width as usize { let (h_chan, s_chan, v_chan, a_chan_lo); - let src_ptr = src_load_ptr.add(cx * channels); + let src_ptr = unsafe { src.get_unchecked(cx * channels..).as_ptr() }; - let row0 = _mm_loadu_si128(src_ptr as *const __m128i); - let row1 = _mm_loadu_si128(src_ptr.add(8) as *const __m128i); - let row2 = _mm_loadu_si128(src_ptr.add(16) as *const __m128i); + let row0 = unsafe { _mm_loadu_si128(src_ptr as *const __m128i) }; + let row1 = unsafe { _mm_loadu_si128(src_ptr.add(8) as *const __m128i) }; + let row2 = unsafe { _mm_loadu_si128(src_ptr.add(16) as *const __m128i) }; match image_configuration { ImageConfiguration::Rgb | ImageConfiguration::Bgr => { @@ -198,7 +197,7 @@ pub unsafe fn sse_hsv_u16_to_image< a_chan_lo = _mm_set1_epi16(255); } ImageConfiguration::Rgba | ImageConfiguration::Bgra => { - let row3 = _mm_loadu_si128(src_ptr.add(24) as *const __m128i); + let row3 = unsafe { _mm_loadu_si128(src_ptr.add(24) as *const __m128i) }; let (h_c, s_c, v_c, a_c) = sse_deinterleave_rgba_epi16(row0, row1, row2, row3); h_chan = h_c; s_chan = s_c; @@ -235,7 +234,7 @@ pub unsafe fn sse_hsv_u16_to_image< let g_chan = _mm_packus_epi16(g_chan_16_lo, zeros); let b_chan = _mm_packus_epi16(b_chan_16_lo, zeros); - let ptr = dst_ptr.add(cx * channels); + let ptr = unsafe { dst.get_unchecked_mut(cx * channels..).as_mut_ptr() }; if USE_ALPHA { let a_chan = _mm_packus_epi16(a_chan_lo, _mm_setzero_si128()); let (rgba0, rgba1, _, _) = match image_configuration { @@ -246,8 +245,10 @@ pub unsafe fn sse_hsv_u16_to_image< sse_interleave_rgba(b_chan, g_chan, r_chan, a_chan) } }; - _mm_storeu_si128(ptr as *mut __m128i, rgba0); - _mm_storeu_si128(ptr.add(16) as *mut __m128i, rgba1); + unsafe { + _mm_storeu_si128(ptr as *mut __m128i, rgba0); + _mm_storeu_si128(ptr.add(16) as *mut __m128i, rgba1); + } } else { let (rgba0, rgba1, _) = match image_configuration { ImageConfiguration::Rgb | ImageConfiguration::Rgba => { @@ -257,8 +258,10 @@ pub unsafe fn sse_hsv_u16_to_image< sse_interleave_rgb(b_chan, g_chan, r_chan) } }; - _mm_storeu_si128(ptr as *mut __m128i, rgba0); - std::ptr::copy_nonoverlapping(&rgba1 as *const _ as *const u8, ptr.add(16), 8); + unsafe { + _mm_storeu_si128(ptr as *mut __m128i, rgba0); + std::ptr::copy_nonoverlapping(&rgba1 as *const _ as *const u8, ptr.add(16), 8); + } } cx += 8; diff --git a/src/sse/image_to_hsv.rs b/src/sse/image_to_hsv.rs index 2f36d48..f2161d5 100644 --- a/src/sse/image_to_hsv.rs +++ b/src/sse/image_to_hsv.rs @@ -19,7 +19,7 @@ use std::arch::x86::*; use std::arch::x86_64::*; #[target_feature(enable = "sse4.1")] -pub unsafe fn sse_channels_to_hsv_u16< +pub(crate) fn sse_channels_to_hsv_u16< const CHANNELS_CONFIGURATION: u8, const USE_ALPHA: bool, const TARGET: u8, diff --git a/src/sse/mod.rs b/src/sse/mod.rs index 63e28fd..1ee9468 100644 --- a/src/sse/mod.rs +++ b/src/sse/mod.rs @@ -33,14 +33,14 @@ mod xyza_laba_to_image; pub(crate) use cie::*; pub(crate) use from_sigmoidal::sse_from_sigmoidal_row; -pub use hsv_to_image::*; -pub use image_to_hsv::*; +pub(crate) use hsv_to_image::*; +pub(crate) use image_to_hsv::*; pub(crate) use image_to_jzazbz::sse_image_to_jzazbz; pub(crate) use image_to_oklab::sse_image_to_oklab; pub(crate) use jzazbz_to_image::sse_jzazbz_to_image; -pub use math::*; +pub(crate) use math::*; pub(crate) use oklab_to_image::sse_oklab_to_image; -pub use support::*; +pub(crate) use support::*; pub(crate) use to_sigmoidal::sse_image_to_sigmoidal_row; pub(crate) use to_xyz_lab::sse_channels_to_xyz_or_lab; pub(crate) use to_xyza_laba::sse_channels_to_xyza_laba; diff --git a/src/sse/support.rs b/src/sse/support.rs index 32c8c46..ae74f79 100644 --- a/src/sse/support.rs +++ b/src/sse/support.rs @@ -10,7 +10,7 @@ use std::arch::x86::*; #[cfg(target_arch = "x86_64")] use std::arch::x86_64::*; -pub const fn shuffle(z: u32, y: u32, x: u32, w: u32) -> i32 { +pub(crate) const fn shuffle(z: u32, y: u32, x: u32, w: u32) -> i32 { // Checked: we want to reinterpret the bits ((z << 6) | (y << 4) | (x << 2) | w) as i32 } From d265978da29756ce55b012d701c9539b0e986c9b Mon Sep 17 00:00:00 2001 From: Radzivon Bartoshyk Date: Sat, 11 Apr 2026 18:48:12 +0100 Subject: [PATCH 6/9] API improvements --- .github/workflows/build_push.yml | 37 +++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 7b2573c..f9727b7 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -37,4 +37,39 @@ jobs: - run: RUSTFLAGS="-C target-feature=+avx2" cargo build --all-features --target x86_64-unknown-linux-gnu - name: Test release pipeline run: cargo publish --dry-run - if: matrix.toolchain == 'stable' \ No newline at end of file + if: matrix.toolchain == 'stable' + + test_x86_64: + name: Test x86_64 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Run tests SSE4.1 + run: RUSTFLAGS="-C target-feature=+sse4.1" cargo test --all-features + - name: Run tests AVX2 + run: RUSTFLAGS="-C target-feature=+avx2" cargo test --all-features + - name: Run tests no features + run: cargo test --no-default-features + + test_i686: + name: Test i686 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - run: rustup target add i686-unknown-linux-gnu + - run: sudo apt-get install -y gcc-multilib + - name: Run tests SSE4.1 + run: RUSTFLAGS="-C target-feature=+sse4.1" cargo test --all-features --target i686-unknown-linux-gnu + - name: Run tests AVX2 + run: RUSTFLAGS="-C target-feature=+avx2" cargo test --no-default-features --target i686-unknown-linux-gnu + + test_aarch64: + name: Test AArch64 NEON + runs-on: ubuntu-24.04-arm + steps: + - uses: actions/checkout@v6 + - uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Run tests + run: cargo test --all-features \ No newline at end of file From dc75dbb81229f02147f76334c61a35b5fa415391 Mon Sep 17 00:00:00 2001 From: Radzivon Bartoshyk Date: Sat, 11 Apr 2026 18:50:32 +0100 Subject: [PATCH 7/9] API improvements --- src/jzczhz.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/jzczhz.rs b/src/jzczhz.rs index a38175e..dd7e848 100644 --- a/src/jzczhz.rs +++ b/src/jzczhz.rs @@ -5,7 +5,6 @@ * // license that can be found in the LICENSE file. */ use crate::{EuclideanDistance, Jzazbz, Rgb, TaxicabDistance, TransferFunction, Xyz}; -use erydanos::{Cosine, Sine, eatan2f, ehypot3f, ehypotf}; use num_traits::Pow; use std::ops::{ Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub, SubAssign, @@ -58,24 +57,24 @@ impl Jzczhz { /// Converts Jzazbz to polar coordinates Jzczhz #[inline] pub fn from_jzazbz(jzazbz: Jzazbz) -> Jzczhz { - let cz = ehypotf(jzazbz.az, jzazbz.bz); - let hz = eatan2f(jzazbz.bz, jzazbz.az); + let cz = f32::hypot(jzazbz.az, jzazbz.bz); + let hz = f32::atan2(jzazbz.bz, jzazbz.az); Jzczhz::new(jzazbz.jz, cz, hz) } /// Converts Jzczhz into Jzazbz #[inline] pub fn to_jzazbz(&self) -> Jzazbz { - let az = self.cz * self.hz.ecos(); - let bz = self.cz * self.hz.esin(); + let az = self.cz * self.hz.cos(); + let bz = self.cz * self.hz.sin(); Jzazbz::new(self.jz, az, bz) } /// Converts Jzczhz into Jzazbz #[inline] pub fn to_jzazbz_with_luminance(&self, display_luminance: f32) -> Jzazbz { - let az = self.cz * self.hz.ecos(); - let bz = self.cz * self.hz.esin(); + let az = self.cz * self.hz.cos(); + let bz = self.cz * self.hz.sin(); Jzazbz::new_with_luminance(self.jz, az, bz, display_luminance) } @@ -142,8 +141,8 @@ impl Jzczhz { let djz = self.jz - other.jz; let dcz = self.cz - other.cz; let dhz = self.hz - other.hz; - let dh = 2f32 * (self.cz * other.cz).sqrt() * (dhz * 0.5f32).esin(); - ehypot3f(djz, dcz, dh) + let dh = 2f32 * (self.cz * other.cz).sqrt() * (dhz * 0.5f32).sin(); + (djz * djz + dcz * dcz + dh * dh).sqrt() } } From 81f622a8761667008ad057fd653a18af98b7eb99 Mon Sep 17 00:00:00 2001 From: Radzivon Bartoshyk Date: Sat, 11 Apr 2026 18:51:47 +0100 Subject: [PATCH 8/9] math fixes --- src/oklch.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/oklch.rs b/src/oklch.rs index ae98d77..1867e48 100644 --- a/src/oklch.rs +++ b/src/oklch.rs @@ -5,7 +5,6 @@ * // license that can be found in the LICENSE file. */ use crate::{EuclideanDistance, Oklab, Rgb, TaxicabDistance, TransferFunction}; -use erydanos::{Cosine, Sine, eatan2f, ehypotf}; use num_traits::Pow; use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; @@ -68,8 +67,8 @@ impl Oklch { /// Converts *Oklab* to *Oklch* #[inline] pub fn from_oklab(oklab: Oklab) -> Oklch { - let chroma = ehypotf(oklab.b, oklab.a); - let hue = eatan2f(oklab.b, oklab.a); + let chroma = f32::hypot(oklab.b, oklab.a); + let hue = f32::atan2(oklab.b, oklab.a); Oklch::new(oklab.l, chroma, hue) } @@ -77,8 +76,8 @@ impl Oklch { #[inline] pub fn to_oklab(&self) -> Oklab { let l = self.l; - let a = self.c * self.h.ecos(); - let b = self.c * self.h.esin(); + let a = self.c * self.h.cos(); + let b = self.c * self.h.sin(); Oklab::new(l, a, b) } } From ec48beb3149fb67c5adfdbec2805cef2e0fbeb36 Mon Sep 17 00:00:00 2001 From: Radzivon Bartoshyk Date: Sat, 11 Apr 2026 18:52:27 +0100 Subject: [PATCH 9/9] math fixes --- .github/workflows/mirroring.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/mirroring.yaml b/.github/workflows/mirroring.yaml index 231ed1b..ac69064 100644 --- a/.github/workflows/mirroring.yaml +++ b/.github/workflows/mirroring.yaml @@ -19,7 +19,7 @@ jobs: run: | git remote add gitlab https://oauth2:${{ secrets.GITLAB_TOKEN }}@gitlab.com/awxkee/colorutils.git git remote add codeberg https://awxkee:${{ secrets.CODEBERG_TOKEN }}@codeberg.org/awxkee/colorutils.git - git push gitlab --all --force - git push gitlab --tags --force - git push codeberg --all --force - git push codeberg --tags --force \ No newline at end of file + git push gitlab --all + git push gitlab --tags + git push codeberg --all + git push codeberg --tags \ No newline at end of file