| // Copyright 2025 the Vello Authors | |
| // SPDX-License-Identifier: Apache-2.0 OR MIT | |
| //! Utility functions. | |
| use crate::geometry::RectU16; | |
| use crate::math::{FloatExt, snap_up}; | |
| use crate::strip::{Strip, visit_strip_fill_segments}; | |
| use crate::tile::Tile; | |
| use alloc::vec::Vec; | |
| use core::ops::{Index, IndexMut}; | |
| use fearless_simd::{f32x16, prelude::*, u8x16, u32x16}; | |
| #[cfg(not(feature = "std"))] | |
| use peniko::kurbo::common::FloatFuncs as _; | |
| use peniko::kurbo::{Affine, Rect}; | |
| /// Convert f32x16 to u8x16. | |
| /// | |
| /// **Important note: The values need to be between 0.0 and 1.0, otherwise you might | |
| /// get inconsistent results across different platforms.** | |
| // We can't guarantee correctness for values < 0.0 due to a restriction in fearless_simd: | |
| // https://github.com/linebender/fearless_simd/blob/3f4489389940b7c3c6ee1847a2d007a22494eeff/fearless_simd/src/generated/simd_types.rs#L1623 | |
| #[inline(always)] | |
| pub fn f32_to_u8<S: Simd>(val: f32x16<S>) -> u8x16<S> { | |
| let simd = val.simd; | |
| let converted = val.to_int::<u32x16<S>>().to_bytes(); | |
| let (x8_1, x8_2) = simd.split_u8x64(converted); | |
| let (p1, p2) = simd.split_u8x32(x8_1); | |
| let (p3, p4) = simd.split_u8x32(x8_2); | |
| #[cfg(target_endian = "little")] | |
| let result = { | |
| let uzp1 = simd.unzip_low_u8x16(p1, p2); | |
| let uzp2 = simd.unzip_low_u8x16(p3, p4); | |
| simd.unzip_low_u8x16(uzp1, uzp2) | |
| }; | |
| #[cfg(target_endian = "big")] | |
| let result = { | |
| let uzp1 = simd.unzip_high_u8x16(p1, p2); | |
| let uzp2 = simd.unzip_high_u8x16(p3, p4); | |
| simd.unzip_high_u8x16(uzp1, uzp2) | |
| }; | |
| result | |
| } | |
| /// A trait for implementing a fast approximal division by 255 for integers. | |
| pub trait Div255Ext: private::Sealed { | |
| /// Divide by 255. | |
| fn div_255(self) -> Self; | |
| } | |
| mod private { | |
| use fearless_simd::{Simd, u16x16, u16x32}; | |
| #[expect(unnameable_types, reason = "Sealed trait pattern.")] | |
| pub trait Sealed {} | |
| impl<S: Simd> Sealed for u16x16<S> {} | |
| impl<S: Simd> Sealed for u16x32<S> {} | |
| } | |
| /// Widen a SIMD vector and combine the two widened halves into one vector. | |
| #[inline(always)] | |
| pub fn widen<S, V>(value: V) -> <V::Widened as SimdCombine<S>>::Combined | |
| where | |
| S: Simd, | |
| V: SimdWiden<S>, | |
| V::Widened: SimdCombine<S>, | |
| { | |
| let (low, high) = value.widen(); | |
| low.combine(high) | |
| } | |
| /// Split a SIMD vector and narrow its two halves into one vector. | |
| #[inline(always)] | |
| pub fn narrow<S, V>(value: V) -> <V::Split as SimdNarrow<S>>::Narrowed | |
| where | |
| S: Simd, | |
| V: SimdSplit<S>, | |
| V::Split: SimdNarrow<S>, | |
| { | |
| let (low, high) = value.split(); | |
| low.narrow(high) | |
| } | |
| /// Split a SIMD vector and narrow its two halves with saturation. | |
| #[inline(always)] | |
| pub fn saturating_narrow<S, V>(value: V) -> <V::Split as SimdNarrow<S>>::Narrowed | |
| where | |
| S: Simd, | |
| V: SimdSplit<S>, | |
| V::Split: SimdNarrow<S>, | |
| { | |
| let (low, high) = value.split(); | |
| low.saturating_narrow(high) | |
| } | |
| impl<T> Div255Ext for T | |
| where | |
| T: private::Sealed + core::ops::Add<u16, Output = T> + core::ops::Shr<u32, Output = T>, | |
| { | |
| #[inline(always)] | |
| fn div_255(self) -> Self { | |
| (self + 255_u16) >> 8_u32 | |
| } | |
| } | |
| /// Perform a normalized multiplication for a SIMD vector of `u8` values. | |
| #[inline(always)] | |
| pub fn normalized_mul_u8<S, V>(a: V, b: V) -> <V::Widened as SimdCombine<S>>::Combined | |
| where | |
| S: Simd, | |
| V: SimdWiden<S>, | |
| V::Widened: SimdCombine<S>, | |
| <V::Widened as SimdCombine<S>>::Combined: Div255Ext, | |
| { | |
| let a = widen(a); | |
| let b = widen(b); | |
| (a * b).div_255() | |
| } | |
| /// Check if an affine transform is a pure integer translation. | |
| /// | |
| /// Returns true if the transform only contains integer translation (no rotation, | |
| /// skew, or scaling), meaning rectangles will remain pixel-aligned after transformation. | |
| #[inline] | |
| pub fn is_integer_translation(transform: &Affine) -> bool { | |
| let [a, b, c, d, e, f] = transform.as_coeffs(); | |
| (a - 1.0).is_nearly_zero() | |
| && b.is_nearly_zero() | |
| && c.is_nearly_zero() | |
| && (d - 1.0).is_nearly_zero() | |
| && (e - e.round()).is_nearly_zero() | |
| && (f - f.round()).is_nearly_zero() | |
| } | |
| /// Check if an affine transform has no skewing (i.e. preserves axis alignment). | |
| #[inline] | |
| pub fn is_axis_aligned(transform: &Affine) -> bool { | |
| let [_, b, c, ..] = transform.as_coeffs(); | |
| b.is_nearly_zero() && c.is_nearly_zero() | |
| } | |
| /// Transform a rectangle if the rectangle fast path supports the given settings. | |
| #[inline] | |
| pub fn into_fast_path_rect( | |
| rect: Rect, | |
| transform: &Affine, | |
| aliasing_threshold: Option<u8>, | |
| ) -> Option<Rect> { | |
| (is_axis_aligned(transform) && aliasing_threshold.is_none()) | |
| .then(|| transform.transform_rect_bbox(rect)) | |
| } | |
| /// Extract scale factors from an affine transform using singular value decomposition. | |
| /// | |
| /// Returns a tuple of (`scale_x`, `scale_y`) representing the scale along each axis. | |
| /// This uses the same algorithm as kurbo's internal `svd()` method. | |
| /// | |
| /// # Arguments | |
| /// * `transform` - The affine transformation to extract scales from. | |
| /// | |
| /// # Returns | |
| /// A tuple `(scale_x, scale_y)` with minimum values clamped to 1e-6 to avoid division by zero. | |
| /// | |
| /// # Note | |
| /// TODO: Consider making `Affine::svd()` public in kurbo to avoid duplicating this code. | |
| /// This implementation mirrors kurbo's internal SVD calculation for extracting scale factors | |
| /// from arbitrary affine transformations. | |
| #[inline] | |
| pub fn extract_scales(transform: &Affine) -> (f32, f32) { | |
| let [a, b, c, d, _, _] = transform.as_coeffs(); | |
| let a = a as f32; | |
| let b = b as f32; | |
| let c = c as f32; | |
| let d = d as f32; | |
| // Compute singular values using the same formula as kurbo's svd() | |
| let a2 = a * a; | |
| let b2 = b * b; | |
| let c2 = c * c; | |
| let d2 = d * d; | |
| let s1 = a2 + b2 + c2 + d2; | |
| let s2 = ((a2 - b2 + c2 - d2).powi(2) + 4.0 * (a * b + c * d).powi(2)).sqrt(); | |
| let scale_x = (0.5 * (s1 + s2)).sqrt(); | |
| let scale_y = (0.5 * (s1 - s2)).sqrt(); | |
| (scale_x.max(1e-6), scale_y.max(1e-6)) | |
| } | |
| /// Extension methods for rectangles. | |
| pub trait RectExt { | |
| /// Snap the rect to whole tile coordinates. | |
| fn snap_to_tile_coordinates(self) -> Self; | |
| } | |
| impl RectExt for Rect { | |
| #[inline] | |
| fn snap_to_tile_coordinates(self) -> Self { | |
| let x0 = snap_down(self.x0, Tile::WIDTH); | |
| let y0 = snap_down(self.y0, Tile::HEIGHT); | |
| if self.is_zero_area() { | |
| return Self::new(x0, y0, x0, y0); | |
| } | |
| Self::new( | |
| x0, | |
| y0, | |
| snap_up(self.x1, Tile::WIDTH), | |
| snap_up(self.y1, Tile::HEIGHT), | |
| ) | |
| } | |
| } | |
| impl RectExt for RectU16 { | |
| #[inline] | |
| fn snap_to_tile_coordinates(self) -> Self { | |
| // This method will panic if we have a viewport of size u16::MAX and draw | |
| // at the very edge, but better than returning a wrong result. | |
| let x0 = (self.x0 / Tile::WIDTH).checked_mul(Tile::WIDTH).unwrap(); | |
| let y0 = (self.y0 / Tile::HEIGHT).checked_mul(Tile::HEIGHT).unwrap(); | |
| if self.is_empty() { | |
| return Self::new(x0, y0, x0, y0); | |
| } | |
| Self::new( | |
| x0, | |
| y0, | |
| self.x1.checked_next_multiple_of(Tile::WIDTH).unwrap(), | |
| self.y1.checked_next_multiple_of(Tile::HEIGHT).unwrap(), | |
| ) | |
| } | |
| } | |
| #[inline] | |
| fn snap_down(value: f64, step: u16) -> f64 { | |
| let step = f64::from(step); | |
| (value / step).floor() * step | |
| } | |
| /// A type that can be cleared. | |
| pub trait Clear { | |
| /// Clear the object to its default state. | |
| fn clear(&mut self); | |
| } | |
| impl<T> Clear for Vec<T> { | |
| fn clear(&mut self) { | |
| Self::clear(self); | |
| } | |
| } | |
| /// Pool for reusing allocations. | |
| #[derive(Debug)] | |
| pub struct Pool<T> { | |
| entries: Vec<T>, | |
| clear_on_submit: bool, | |
| } | |
| impl<T> Default for Pool<T> { | |
| fn default() -> Self { | |
| Self::new(true) | |
| } | |
| } | |
| impl<T> Pool<T> { | |
| /// Create a new pool. | |
| /// | |
| /// `clear_on_submit` decides whether submitted values should | |
| /// be cleared when they are submitted or whether they should retain | |
| /// their original contents. | |
| pub fn new(clear_on_submit: bool) -> Self { | |
| Self { | |
| entries: Vec::new(), | |
| clear_on_submit, | |
| } | |
| } | |
| /// Take an object from the pool or create a new one. | |
| pub fn take(&mut self) -> T | |
| where | |
| T: Default, | |
| { | |
| self.entries.pop().unwrap_or_default() | |
| } | |
| /// Return an object to the pool. | |
| pub fn submit(&mut self, mut entry: T) | |
| where | |
| T: Clear, | |
| { | |
| if self.clear_on_submit { | |
| entry.clear(); | |
| } | |
| self.entries.push(entry); | |
| } | |
| } | |
| /// Pool for reusing vector allocations. | |
| pub type VecPool<T> = Pool<Vec<T>>; | |
| /// A resizable vector that retains inner elements upon resizing. | |
| #[derive(Debug)] | |
| pub struct RetainVec<T> { | |
| inner: Vec<T>, | |
| len: usize, | |
| } | |
| impl<T: Clear> RetainVec<T> { | |
| /// Create an empty `RetainVec`. | |
| pub fn new() -> Self { | |
| Self { | |
| inner: Vec::new(), | |
| len: 0, | |
| } | |
| } | |
| /// Create a `RetainVec` with `len` initialized entries. | |
| pub fn with_len(len: usize, mut init: impl FnMut() -> T) -> Self { | |
| let mut inner = Vec::with_capacity(len); | |
| inner.resize_with(len, &mut init); | |
| Self { inner, len } | |
| } | |
| /// Return the length. | |
| pub fn len(&self) -> usize { | |
| self.len | |
| } | |
| /// Return `true` if the vector is empty. | |
| pub fn is_empty(&self) -> bool { | |
| self.len == 0 | |
| } | |
| /// Return the entries as a slice. | |
| pub fn as_slice(&self) -> &[T] { | |
| &self.inner[..self.len] | |
| } | |
| /// Return the entries as a mutable slice. | |
| pub fn as_mut_slice(&mut self) -> &mut [T] { | |
| &mut self.inner[..self.len] | |
| } | |
| /// Iterate mutably over active entries. | |
| pub fn iter_mut(&mut self) -> core::slice::IterMut<'_, T> { | |
| self.as_mut_slice().iter_mut() | |
| } | |
| /// Clear the elements in this vector. | |
| pub fn clear(&mut self) { | |
| self.len = 0; | |
| } | |
| /// Resize the vector. | |
| pub fn resize_with(&mut self, new_len: usize, mut init: impl FnMut() -> T) { | |
| let old_len = self.len; | |
| if new_len > self.inner.len() { | |
| self.inner.resize_with(new_len, &mut init); | |
| } | |
| self.len = new_len; | |
| // Make sure to actually reset the newly added values since they are not reset when shrinking | |
| // the vector. | |
| if new_len > old_len { | |
| for item in &mut self.inner[old_len..new_len] { | |
| item.clear(); | |
| } | |
| } | |
| } | |
| } | |
| impl<T: Clear> Default for RetainVec<T> { | |
| fn default() -> Self { | |
| Self::new() | |
| } | |
| } | |
| impl<T> Index<usize> for RetainVec<T> { | |
| type Output = T; | |
| fn index(&self, index: usize) -> &Self::Output { | |
| &self.inner[..self.len][index] | |
| } | |
| } | |
| impl<T> IndexMut<usize> for RetainVec<T> { | |
| fn index_mut(&mut self, index: usize) -> &mut Self::Output { | |
| &mut self.inner[..self.len][index] | |
| } | |
| } | |
| /// Calculate the bounding box of the strips. | |
| pub fn strip_bbox(strips: &[Strip]) -> Option<RectU16> { | |
| // Fill and alpha fill segments internally store their coordinates in tile units, | |
| // in order to avoid multiplications in every invocation of the closure we calculate | |
| // the bbox in tile units first and then convert back to pixel units. | |
| let mut tile_bbox = RectU16::INVERTED; | |
| visit_strip_fill_segments( | |
| strips, | |
| RectU16::new( | |
| 0, | |
| 0, | |
| u16::MAX.div_ceil(Tile::WIDTH), | |
| u16::MAX.div_ceil(Tile::HEIGHT), | |
| ), | |
| &mut tile_bbox, | |
| |bbox, segment| bbox.union(segment.fill.tile_rect()), | |
| |bbox, segment| bbox.union(segment.tile_rect()), | |
| ); | |
| // Convert to pixel units. | |
| if tile_bbox.is_empty() { | |
| None | |
| } else { | |
| Some(RectU16::new( | |
| tile_bbox.x0.checked_mul(Tile::WIDTH).unwrap(), | |
| tile_bbox.y0.checked_mul(Tile::HEIGHT).unwrap(), | |
| tile_bbox.x1.checked_mul(Tile::WIDTH).unwrap(), | |
| tile_bbox.y1.checked_mul(Tile::HEIGHT).unwrap(), | |
| )) | |
| } | |
| } | |
| pub(crate) mod unpremultiply { | |
| use fearless_simd::{prelude::*, u8x16, u16x16}; | |
| trait Div256Ext { | |
| /// Divide by 256, rounding to the nearest integer. | |
| /// | |
| /// Values must not exceed `u16::MAX - 128`. | |
| fn div_256(self) -> Self; | |
| } | |
| impl<T> Div256Ext for T | |
| where | |
| T: core::ops::Add<u16, Output = T> + core::ops::Shr<u32, Output = T>, | |
| { | |
| #[inline(always)] | |
| fn div_256(self) -> Self { | |
| (self + 128_u16) >> 8_u32 | |
| } | |
| } | |
| // Unlike premultiplication, unpremultiplication is difficult to perform | |
| // efficiently with SIMD because the divisor varies for each pixel. | |
| // Premultiplication computes `rgb * alpha / 255`, while | |
| // unpremultiplication computes `rgb * 255 / alpha`. | |
| // | |
| // Therefore, what we do instead is that we precompute the reciprocal | |
| // 255 / alpha for each possible alpha value such that the | |
| // computation simply becomes rgb * reciprocal. We do this using fixed-point | |
| // artithmetic with 8 fractional digits, hence why we need to multiply and | |
| // divide by 256. | |
| #[expect( | |
| clippy::cast_possible_truncation, | |
| reason = "all generated reciprocals fit into a u16" | |
| )] | |
| const fn reciprocals() -> [u16; 256] { | |
| let mut values = [0; 256]; | |
| // When alpha is 0, so are the RGB channels so it just stays | |
| // 0. | |
| let mut alpha = 1; | |
| while alpha < 256 { | |
| let round_factor = alpha / 2; | |
| values[alpha] = ((255 * 256 + round_factor) / alpha) as u16; | |
| alpha += 1; | |
| } | |
| values | |
| } | |
| const RECIPROCALS: [u16; 256] = reciprocals(); | |
| #[inline(always)] | |
| pub(crate) const fn reciprocal(alpha: u8) -> u16 { | |
| RECIPROCALS[alpha as usize] | |
| } | |
| #[inline(always)] | |
| pub(crate) fn scalar(component: u8, reciprocal: u16) -> u8 { | |
| u16::from(component).wrapping_mul(reciprocal).div_256() as u8 | |
| } | |
| #[inline(always)] | |
| pub(crate) fn simd<S: Simd>(_simd: S, component: u8x16<S>, reciprocal: u16x16<S>) -> u8x16<S> { | |
| let component = super::widen(component); | |
| let product = (component * reciprocal).div_256(); | |
| super::narrow(product) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use fearless_simd::{Level, SimdBase, dispatch, u8x16, u16x16}; | |
| use super::{reciprocal, scalar, simd as unpremultiply_simd}; | |
| fn assert_accurate(component: u8, alpha: u8, actual: u8) { | |
| // For 0 and 255 we want exact matches, otherwise we tolerate | |
| // a delta of at most 1 compared to f32. | |
| if component == 0 || alpha == 0 || alpha == 255 { | |
| assert_eq!(actual, component); | |
| } else if component == alpha { | |
| assert_eq!(actual, 255); | |
| } else { | |
| let expected = (f32::from(component) * 255.0 / f32::from(alpha) + 0.5) as u8; | |
| assert!( | |
| actual.abs_diff(expected) <= 1, | |
| "component {component} with alpha {alpha} produced {actual} instead of {expected}" | |
| ); | |
| } | |
| } | |
| #[test] | |
| fn scalar_exhaustive() { | |
| for alpha in 0_u8..=255 { | |
| for component in 0_u8..=alpha { | |
| assert_accurate(component, alpha, scalar(component, reciprocal(alpha))); | |
| } | |
| } | |
| } | |
| #[test] | |
| fn simd_exhaustive() { | |
| let level = Level::try_detect().unwrap_or(Level::baseline()); | |
| dispatch!(level, simd => { | |
| for alpha in 0_u8..=255 { | |
| let alpha_simd = u8x16::splat(simd, alpha); | |
| let reciprocal = | |
| u16x16::from_fn(simd, |lane| reciprocal(alpha_simd[lane])); | |
| for component_start in (0_u16..=u16::from(alpha)).step_by(16) { | |
| let component = u8x16::from_fn(simd, |lane| { | |
| (component_start + lane as u16).min(u16::from(alpha)) as u8 | |
| }); | |
| let actual = unpremultiply_simd(simd, component, reciprocal); | |
| for lane in 0..16 { | |
| let component = component[lane]; | |
| assert_accurate(component, alpha, actual[lane]); | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::RectU16; | |
| use super::{RectExt, into_fast_path_rect, strip_bbox}; | |
| use crate::strip::Strip; | |
| use crate::tile::Tile; | |
| use peniko::kurbo::{Affine, Rect}; | |
| fn sentinel(y: u16, alpha_idx: u32) -> Strip { | |
| Strip::new(u16::MAX, y, alpha_idx, false) | |
| } | |
| #[test] | |
| fn snap_to_tile_coordinates_rounds_outward() { | |
| let rect = Rect::new(-4.1, -0.1, 4.1, 8.0).snap_to_tile_coordinates(); | |
| assert_eq!(rect, Rect::new(-8.0, -4.0, 8.0, 8.0)); | |
| } | |
| #[test] | |
| fn snap_u16_to_tile_coordinates_rounds_outward() { | |
| let rect = RectU16::new(5, 3, 9, 7).snap_to_tile_coordinates(); | |
| assert_eq!(rect, RectU16::new(4, 0, 12, 8)); | |
| } | |
| #[test] | |
| fn snap_to_tile_coordinates_preserves_empty_rects() { | |
| assert_eq!( | |
| Rect::new(5.0, 3.0, 5.0, 7.0).snap_to_tile_coordinates(), | |
| Rect::new(4.0, 0.0, 4.0, 0.0) | |
| ); | |
| assert_eq!( | |
| RectU16::new(5, 3, 9, 3).snap_to_tile_coordinates(), | |
| RectU16::new(4, 0, 4, 0) | |
| ); | |
| } | |
| #[test] | |
| fn fast_path_rect_is_transformed_when_supported() { | |
| let rect = Rect::new(1.0, 2.0, 3.0, 4.0); | |
| let transform = Affine::translate((5.0, 7.0)) * Affine::scale_non_uniform(2.0, -3.0); | |
| assert_eq!( | |
| into_fast_path_rect(rect, &transform, None), | |
| Some(Rect::new(7.0, -5.0, 11.0, 1.0)) | |
| ); | |
| } | |
| #[test] | |
| fn fast_path_rect_rejects_unsupported_settings() { | |
| let rect = Rect::new(1.0, 2.0, 3.0, 4.0); | |
| assert_eq!(into_fast_path_rect(rect, &Affine::rotate(0.5), None), None); | |
| assert_eq!( | |
| into_fast_path_rect(rect, &Affine::IDENTITY, Some(128)), | |
| None | |
| ); | |
| } | |
| #[test] | |
| fn empty_strip_bbox() { | |
| let strips = [Strip::sentinel(0, 0)]; | |
| assert_eq!(strip_bbox(&strips), None); | |
| } | |
| #[test] | |
| fn single_strip_bbox() { | |
| let strips = [ | |
| Strip::new(8, 4, 0, false), | |
| sentinel(4, u32::from(Tile::HEIGHT) * 4), | |
| ]; | |
| assert_eq!(strip_bbox(&strips), Some(RectU16::new(8, 4, 12, 8))); | |
| } | |
| #[test] | |
| fn strip_with_fill_bbox() { | |
| let strips = [ | |
| Strip::new(4, 0, 0, false), | |
| Strip::new(20, 0, u32::from(Tile::HEIGHT) * 4, true), | |
| sentinel(0, u32::from(Tile::HEIGHT) * 8), | |
| ]; | |
| assert_eq!(strip_bbox(&strips), Some(RectU16::new(4, 0, 24, 4))); | |
| } | |
| #[test] | |
| fn strip_with_row_end_fill_gap_bbox_is_clamped_to_viewport() { | |
| let strips = [ | |
| Strip::new(4, 0, 0, false), | |
| Strip::new(32, 0, u32::from(Tile::HEIGHT) * 4, true), | |
| sentinel(0, u32::from(Tile::HEIGHT) * 4), | |
| ]; | |
| assert_eq!(strip_bbox(&strips), Some(RectU16::new(4, 0, 32, 4))); | |
| } | |
| #[test] | |
| fn strips_with_multiple_rows_bbox() { | |
| let strips = [ | |
| Strip::new(12, 0, 0, false), | |
| Strip::new(4, 8, u32::from(Tile::HEIGHT) * 4, false), | |
| sentinel(8, u32::from(Tile::HEIGHT) * 8), | |
| ]; | |
| assert_eq!(strip_bbox(&strips), Some(RectU16::new(4, 0, 16, 12))); | |
| } | |
| } |