From 0d0b9e5cee4d5719a9710a6ead480f23ab4faf1e Mon Sep 17 00:00:00 2001
From: Erik Hedvall <erikwhedvall@gmail.com>
Date: Sun, 9 Apr 2023 20:24:55 +0200
Subject: [PATCH 1/2] Allow RGB/XYZ conversion matrices to be pre-defined

---
 palette/src/encoding/srgb.rs | 44 ++++++++++++++++++++++++++++++++++--
 palette/src/matrix.rs        | 19 ++++++++++++++++
 palette/src/rgb.rs           | 22 +++++++++++++++++-
 palette/src/rgb/rgb.rs       | 17 ++++++++++----
 palette/src/xyz.rs           | 15 ++++++++----
 5 files changed, 106 insertions(+), 11 deletions(-)

diff --git a/palette/src/encoding/srgb.rs b/palette/src/encoding/srgb.rs
index 474a59819..bb90a9e19 100644
--- a/palette/src/encoding/srgb.rs
+++ b/palette/src/encoding/srgb.rs
@@ -7,7 +7,7 @@ use crate::{
     num::{Arithmetics, MulAdd, MulSub, PartialCmp, Powf, Real},
     rgb::{Primaries, RgbSpace, RgbStandard},
     white_point::{Any, D65},
-    Yxy,
+    Mat3, Yxy,
 };
 
 use lookup_tables::*;
@@ -59,6 +59,28 @@ impl<T: Real> Primaries<T> for Srgb {
 impl RgbSpace for Srgb {
     type Primaries = Srgb;
     type WhitePoint = D65;
+
+    #[rustfmt::skip]
+    #[inline(always)]
+    fn rgb_to_xyz_matrix() -> Option<Mat3<f64>> {
+        // Matrix from http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
+        Some([
+            0.4124564, 0.3575761, 0.1804375,
+            0.2126729, 0.7151522, 0.0721750,
+            0.0193339, 0.1191920, 0.9503041,
+        ])
+    }
+
+    #[rustfmt::skip]
+    #[inline(always)]
+    fn xyz_to_rgb_matrix() -> Option<Mat3<f64>> {
+        // Matrix from http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
+        Some([
+             3.2404542, -1.5371385, -0.4985314,
+            -0.9692660,  1.8760108,  0.0415560,
+             0.0556434, -0.2040259,  1.0572252,
+        ])
+    }
 }
 
 impl RgbStandard for Srgb {
@@ -130,7 +152,25 @@ impl FromLinear<f64, u8> for Srgb {
 
 #[cfg(test)]
 mod test {
-    use crate::encoding::{FromLinear, IntoLinear, Srgb};
+    use crate::{
+        encoding::{FromLinear, IntoLinear, Srgb},
+        matrix::{matrix_inverse, rgb_to_xyz_matrix},
+        rgb::RgbSpace,
+    };
+
+    #[test]
+    fn rgb_to_xyz() {
+        let dynamic = rgb_to_xyz_matrix::<Srgb, f64>();
+        let constant = Srgb::rgb_to_xyz_matrix().unwrap();
+        assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001);
+    }
+
+    #[test]
+    fn xyz_to_rgb() {
+        let dynamic = matrix_inverse(rgb_to_xyz_matrix::<Srgb, f64>());
+        let constant = Srgb::xyz_to_rgb_matrix().unwrap();
+        assert_relative_eq!(dynamic[..], constant[..], epsilon = 0.0000001);
+    }
 
     #[test]
     fn u8_to_f32_to_u8() {
diff --git a/palette/src/matrix.rs b/palette/src/matrix.rs
index da0eaefe9..ea33be3c4 100644
--- a/palette/src/matrix.rs
+++ b/palette/src/matrix.rs
@@ -152,6 +152,25 @@ where
     ]
 }
 
+/// Maps a matrix from one item type to another.
+///
+/// This turned out to be easier for the compiler to optimize than `matrix.map(f)`.
+#[inline(always)]
+pub fn matrix_map<T, U>(matrix: Mat3<T>, mut f: impl FnMut(T) -> U) -> Mat3<U> {
+    let [m1, m2, m3, m4, m5, m6, m7, m8, m9] = matrix;
+    [
+        f(m1),
+        f(m2),
+        f(m3),
+        f(m4),
+        f(m5),
+        f(m6),
+        f(m7),
+        f(m8),
+        f(m9),
+    ]
+}
+
 /// Generates the Srgb to Xyz transformation matrix for a given white point.
 #[inline]
 pub fn rgb_to_xyz_matrix<S, T>() -> Mat3<T>
diff --git a/palette/src/rgb.rs b/palette/src/rgb.rs
index bac11b978..a90296dc9 100644
--- a/palette/src/rgb.rs
+++ b/palette/src/rgb.rs
@@ -64,7 +64,7 @@ use crate::{
     encoding::{self, FromLinear, Gamma, IntoLinear, Linear},
     stimulus::{FromStimulus, Stimulus},
     white_point::Any,
-    Yxy,
+    Mat3, Yxy,
 };
 
 pub use self::rgb::{FromHexError, Rgb, Rgba};
@@ -121,6 +121,26 @@ pub trait RgbSpace {
 
     /// The white point of the RGB color space.
     type WhitePoint;
+
+    /// Get a pre-defined matrix for converting an RGB value with this standard
+    /// into an XYZ value.
+    ///
+    /// Returning `None` (as in the default implementation) means that the
+    /// matrix will be computed dynamically, which is significantly slower.
+    #[inline(always)]
+    fn rgb_to_xyz_matrix() -> Option<Mat3<f64>> {
+        None
+    }
+
+    /// Get a pre-defined matrix for converting an XYZ value into an RGB value
+    /// with this standard.
+    ///
+    /// Returning `None` (as in the default implementation) means that the
+    /// matrix will be computed dynamically, which is significantly slower.
+    #[inline(always)]
+    fn xyz_to_rgb_matrix() -> Option<Mat3<f64>> {
+        None
+    }
 }
 
 impl<P, W> RgbSpace for (P, W) {
diff --git a/palette/src/rgb/rgb.rs b/palette/src/rgb/rgb.rs
index 415a1aefa..8566d9cfa 100644
--- a/palette/src/rgb/rgb.rs
+++ b/palette/src/rgb/rgb.rs
@@ -29,7 +29,7 @@ use crate::{
     convert::{FromColorUnclamped, IntoColorUnclamped},
     encoding::{FromLinear, IntoLinear, Linear, Srgb},
     luma::LumaStandard,
-    matrix::{matrix_inverse, multiply_xyz_to_rgb, rgb_to_xyz_matrix},
+    matrix::{matrix_inverse, matrix_map, multiply_xyz_to_rgb, rgb_to_xyz_matrix},
     num::{
         self, Abs, Arithmetics, FromScalar, FromScalarArray, IntoScalarArray, IsValidDivisor,
         MinMax, One, PartialCmp, Real, Recip, Round, Trigonometry, Zero,
@@ -251,6 +251,7 @@ impl<S: RgbStandard, T> Rgb<S, T> {
     ///
     /// See the transfer function types in the [`encoding`](crate::encoding)
     /// module for details and performance characteristics.
+    #[inline(always)]
     pub fn into_linear<U>(self) -> Rgb<Linear<S::Space>, U>
     where
         S::TransferFn: IntoLinear<U, T>,
@@ -276,6 +277,7 @@ impl<S: RgbStandard, T> Rgb<S, T> {
     ///
     /// See the transfer function types in the [`encoding`](crate::encoding)
     /// module for details and performance characteristics.
+    #[inline(always)]
     pub fn from_linear<U>(color: Rgb<Linear<S::Space>, U>) -> Self
     where
         S::TransferFn: FromLinear<U, T>,
@@ -589,12 +591,19 @@ where
     <S::Space as RgbSpace>::Primaries: Primaries<T::Scalar>,
     <S::Space as RgbSpace>::WhitePoint: WhitePoint<T::Scalar>,
     T: Arithmetics + FromScalar,
-    T::Scalar:
-        Recip + IsValidDivisor<Mask = bool> + Arithmetics + Clone + FromScalar<Scalar = T::Scalar>,
+    T::Scalar: Real
+        + Recip
+        + IsValidDivisor<Mask = bool>
+        + Arithmetics
+        + Clone
+        + FromScalar<Scalar = T::Scalar>,
     Yxy<Any, T::Scalar>: IntoColorUnclamped<Xyz<Any, T::Scalar>>,
 {
     fn from_color_unclamped(color: Xyz<<S::Space as RgbSpace>::WhitePoint, T>) -> Self {
-        let transform_matrix = matrix_inverse(rgb_to_xyz_matrix::<S::Space, T::Scalar>());
+        let transform_matrix = S::Space::xyz_to_rgb_matrix().map_or_else(
+            || matrix_inverse(rgb_to_xyz_matrix::<S::Space, T::Scalar>()),
+            |matrix| matrix_map(matrix, T::Scalar::from_f64),
+        );
         Self::from_linear(multiply_xyz_to_rgb(transform_matrix, color))
     }
 }
diff --git a/palette/src/xyz.rs b/palette/src/xyz.rs
index 9c029dff3..b30f59095 100644
--- a/palette/src/xyz.rs
+++ b/palette/src/xyz.rs
@@ -21,7 +21,7 @@ use crate::{
     convert::{FromColorUnclamped, IntoColorUnclamped},
     encoding::IntoLinear,
     luma::LumaStandard,
-    matrix::{multiply_rgb_to_xyz, multiply_xyz, rgb_to_xyz_matrix},
+    matrix::{matrix_map, multiply_rgb_to_xyz, multiply_xyz, rgb_to_xyz_matrix},
     num::{
         self, Arithmetics, FromScalar, FromScalarArray, IntoScalarArray, IsValidDivisor, MinMax,
         One, PartialCmp, Powi, Real, Recip, Zero,
@@ -209,8 +209,12 @@ impl<Wp, T> FromColorUnclamped<Xyz<Wp, T>> for Xyz<Wp, T> {
 impl<Wp, T, S> FromColorUnclamped<Rgb<S, T>> for Xyz<Wp, T>
 where
     T: Arithmetics + FromScalar,
-    T::Scalar:
-        Recip + IsValidDivisor<Mask = bool> + Arithmetics + FromScalar<Scalar = T::Scalar> + Clone,
+    T::Scalar: Real
+        + Recip
+        + IsValidDivisor<Mask = bool>
+        + Arithmetics
+        + FromScalar<Scalar = T::Scalar>
+        + Clone,
     Wp: WhitePoint<T::Scalar>,
     S: RgbStandard,
     S::TransferFn: IntoLinear<T, T>,
@@ -219,7 +223,10 @@ where
     Yxy<Any, T::Scalar>: IntoColorUnclamped<Xyz<Any, T::Scalar>>,
 {
     fn from_color_unclamped(color: Rgb<S, T>) -> Self {
-        let transform_matrix = rgb_to_xyz_matrix::<S::Space, T::Scalar>();
+        let transform_matrix = S::Space::rgb_to_xyz_matrix().map_or_else(
+            || rgb_to_xyz_matrix::<S::Space, T::Scalar>(),
+            |matrix| matrix_map(matrix, T::Scalar::from_f64),
+        );
         multiply_rgb_to_xyz(transform_matrix, color.into_linear())
     }
 }

From df9902f7bebac4c2810e72cfc89fd5f3dd93ed1a Mon Sep 17 00:00:00 2001
From: Erik Hedvall <erikwhedvall@gmail.com>
Date: Sun, 9 Apr 2023 21:01:49 +0200
Subject: [PATCH 2/2] Change criterion-compare-action version

---
 .github/workflows/benchmark.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index 759aa98ee..ccea6cc5d 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -13,7 +13,7 @@ jobs:
           toolchain: stable
           override: true
           profile: minimal
-      - uses: boa-dev/criterion-compare-action@v3.2.0
+      - uses: boa-dev/criterion-compare-action@v3
         with:
           token: ${{ secrets.GITHUB_TOKEN }}
           branchName: ${{ github.base_ref }}