From f24518d7b6bf2b3c7ff65a01fcb07e875b8f9b2f Mon Sep 17 00:00:00 2001 From: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:18:59 +0000 Subject: [PATCH] `CacheKey` type for maps which use colours as keys (#92) This will be useful for Vello. This is done through two new traits, `BitEq` and `BitHash`. These are used in the `Eq` and `Hash` implementations of `CacheKey`. These traits are implemented for f32, and the colour types in this crate (except the 8 bit types) --- CHANGELOG.md | 5 +- color/src/cache_key.rs | 210 +++++++++++++++++++++++++++++++++++++ color/src/color.rs | 41 +++++++- color/src/dynamic.rs | 36 ++++--- color/src/impl_bytemuck.rs | 14 ++- color/src/lib.rs | 9 +- 6 files changed, 290 insertions(+), 25 deletions(-) create mode 100644 color/src/cache_key.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c5d14df..1c39b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,11 +24,12 @@ This release has an [MSRV][] of 1.82. * `AlphaColor::with_alpha` allows setting the alpha channel. ([#67][] by [@waywardmonkeys][]) * Support for the `ACEScg` color space. ([#54][] by [@MightyBurger][]) * `DynamicColor` gets `with_alpha` and `multiply_alpha`. ([#71][] by [@waywardmonkeys][]) -* `DynamicColor` now impls `Hash` and `PartialEq`. ([#75][] by [@waywardmonkeys][]) +* `DynamicColor` now impls `PartialEq`. ([#75][] by [@waywardmonkeys][]) * `AlphaColor`, `OpaqueColor`, and `PremulColor` now impl `PartialEq`. ([#76][], [#86][] by [@waywardmonkeys][]) * `HueDirection` now impls `PartialEq`. ([#79][] by [@waywardmonkeys][]) * `ColorSpaceTag` and `HueDirection` now have bytemuck support. ([#81][] by [@waywardmonkeys][]) * A `DynamicColor` parsed from a named color or named color space function now serializes back to that name, as per the CSS Color Level 4 spec ([#39][] by [@tomcur][]). +* `CacheKey` to allow using colors as keys for resource caching. ([#92][] by [@DJMcNab][]) ### Changed @@ -44,6 +45,7 @@ This release has an [MSRV][] of 1.82. This is the initial release. +[@DJMcNab]: https://github.com/DJMcNab [@MightyBurger]: https://github.com/MightyBurger [@raphlinus]: https://github.com/raphlinus [@tomcur]: https://github.com/tomcur @@ -66,6 +68,7 @@ This is the initial release. [#80]: https://github.com/linebender/color/pull/80 [#81]: https://github.com/linebender/color/pull/81 [#86]: https://github.com/linebender/color/pull/86 +[#92]: https://github.com/linebender/color/pull/92 [Unreleased]: https://github.com/linebender/color/compare/v0.1.0...HEAD [0.1.0]: https://github.com/linebender/color/releases/tag/v0.1.0 diff --git a/color/src/cache_key.rs b/color/src/cache_key.rs new file mode 100644 index 0000000..e90fa5c --- /dev/null +++ b/color/src/cache_key.rs @@ -0,0 +1,210 @@ +// Copyright 2024 the Color Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Hashing and other caching utilities for Color types. +//! +//! In this crate, colors are implemented using `f32`. +//! This means that color types aren't `Hash` and `Eq` for good reasons: +//! +//! - Equality on these types is not reflexive (consider [NaN](f32::NAN)). +//! - Certain values have two representations (`-0` and `+0` are both zero). +//! +//! However, it is still useful to create caches which key off these values. +//! These are caches which don't have any semantic meaning, but instead +//! are used to avoid redundant calculations or storage. +//! +//! Color supports creating these caches by using [`CacheKey`] as the key in +//! your cache. +//! `T` is the key type (i.e. a color) which you want to use as the key. +//! This `T` must implement both [`BitHash`] and [`BitEq`], which are +//! versions of the standard `Hash` and `Eq` traits which support implementations +//! for floating point numbers which might be unexpected outside of a caching context. + +use core::hash::{Hash, Hasher}; + +/// A key usable in a hashmap to compare the bit representation +/// types containing colors. +/// +/// See the [module level docs](self) for more information. +#[derive(Debug, Copy, Clone)] +#[repr(transparent)] +pub struct CacheKey(pub T); + +impl CacheKey { + /// Create a new `CacheKey`. + /// + /// All fields are public, so the struct constructor can also be used. + pub fn new(value: T) -> Self { + Self(value) + } + + /// Get the inner value. + pub fn into_inner(self) -> T { + self.0 + } +} + +// This module exists for these implementations: + +// `BitEq` is an equivalence relation, just maybe not the one you'd expect. +impl Eq for CacheKey {} +impl PartialEq for CacheKey { + fn eq(&self, other: &Self) -> bool { + self.0.bit_eq(&other.0) + } +} +// If we implement Eq, BitEq's implementation matches that of the hash. +impl Hash for CacheKey { + fn hash(&self, state: &mut H) { + self.0.bit_hash(state); + } +} + +/// A hash implementation for types which normally wouldn't have one, +/// implemented using a hash of the bitwise equivalent types when needed. +/// +/// If a type is `BitHash` and `BitEq`, then it is important that the following property holds: +/// +/// ```text +/// k1 biteq k2 -> bithash(k1) == bithash(k2) +/// ``` +/// +/// See the docs on [`Hash`] for more information. +/// +/// Useful for creating caches based on exact values. +/// See the [module level docs](self) for more information. +pub trait BitHash { + /// Feeds this value into the given [`Hasher`]. + fn bit_hash(&self, state: &mut H); + // Intentionally no hash_slice for simplicity. +} + +impl BitHash for f32 { + fn bit_hash(&self, state: &mut H) { + self.to_bits().hash(state); + } +} +impl BitHash for [T; N] { + fn bit_hash(&self, state: &mut H) { + self[..].bit_hash(state); + } +} + +impl BitHash for [T] { + fn bit_hash(&self, state: &mut H) { + // In theory, we should use `write_length_prefix`, which is unstable: + // https://github.com/rust-lang/rust/issues/96762 + // We could do that by (unsafely) casting to `[CacheKey]`, then + // using `Hash::hash` on the resulting slice. + state.write_usize(self.len()); + for piece in self { + piece.bit_hash(state); + } + } +} + +impl BitHash for &T { + fn bit_hash(&self, state: &mut H) { + T::bit_hash(*self, state); + } +} + +// Don't BitHash tuples, not that important + +/// An equivalence relation for types which normally wouldn't have +/// one, implemented using a bitwise comparison for floating point +/// values. +/// +/// See the docs on [`Eq`] for more information. +/// +/// Useful for creating caches based on exact values. +/// See the [module level docs](self) for more information. +pub trait BitEq { + /// Returns true if `self` is equal to `other`. + /// + /// This need not use the semantically natural comparison operation + /// for the type; indeed floating point types should implement this + /// by comparing bit values. + fn bit_eq(&self, other: &Self) -> bool; + // Intentionally no bit_ne as would be added complexity for little gain +} + +impl BitEq for f32 { + fn bit_eq(&self, other: &Self) -> bool { + self.to_bits() == other.to_bits() + } +} + +impl BitEq for [T; N] { + fn bit_eq(&self, other: &Self) -> bool { + for i in 0..N { + if !self[i].bit_eq(&other[i]) { + return false; + } + } + true + } +} + +impl BitEq for [T] { + fn bit_eq(&self, other: &Self) -> bool { + if self.len() != other.len() { + return false; + } + for (a, b) in self.iter().zip(other) { + if !a.bit_eq(b) { + return false; + } + } + true + } +} + +impl BitEq for &T { + fn bit_eq(&self, other: &Self) -> bool { + T::bit_eq(*self, *other) + } +} + +// Don't BitEq tuples, not that important + +// Ideally we'd also have these implementations, but they cause conflicts +// (in case std ever went mad and implemented Eq for f32, for example). +// impl BitHash for T {...} +// impl BitEq for T {...} + +#[cfg(test)] +mod tests { + use super::CacheKey; + use crate::{parse_color, DynamicColor}; + + use std::collections::HashMap; + + #[test] + fn bit_eq_hashmap() { + let mut map: HashMap, i32> = HashMap::new(); + // The implementation for f32 is the base case. + assert!(map.insert(CacheKey(0.0), 0).is_none()); + assert!(map.insert(CacheKey(-0.0), -1).is_none()); + assert!(map.insert(CacheKey(1.0), 1).is_none()); + assert!(map.insert(CacheKey(0.5), 5).is_none()); + + assert_eq!(map.get(&CacheKey(1.0)).unwrap(), &1); + assert_eq!(map.get(&CacheKey(0.0)).unwrap(), &0); + assert_eq!(map.remove(&CacheKey(-0.0)).unwrap(), -1); + assert!(!map.contains_key(&CacheKey(-0.0))); + assert_eq!(map.get(&CacheKey(0.5)).unwrap(), &5); + } + #[test] + fn bit_eq_color_hashmap() { + let mut map: HashMap, i32> = HashMap::new(); + + let red = parse_color("red").unwrap(); + let red2 = parse_color("red").unwrap(); + let other = parse_color("oklab(0.4 0.2 0.6)").unwrap(); + assert!(map.insert(CacheKey(red), 10).is_none()); + assert_eq!(map.insert(CacheKey(red2), 5).unwrap(), 10); + assert!(map.insert(CacheKey(other), 15).is_none()); + assert_eq!(map.get(&CacheKey(other)).unwrap(), &15); + } +} diff --git a/color/src/color.rs b/color/src/color.rs index 83ee3cb..a802039 100644 --- a/color/src/color.rs +++ b/color/src/color.rs @@ -6,7 +6,10 @@ use core::any::TypeId; use core::marker::PhantomData; -use crate::{ColorSpace, ColorSpaceLayout, ColorSpaceTag, Oklab, Oklch, PremulRgba8, Rgba8, Srgb}; +use crate::{ + cache_key::{BitEq, BitHash}, + ColorSpace, ColorSpaceLayout, ColorSpaceTag, Oklab, Oklch, PremulRgba8, Rgba8, Srgb, +}; #[cfg(all(not(feature = "std"), not(test)))] use crate::floatfuncs::FloatFuncs; @@ -726,6 +729,18 @@ impl core::ops::Sub for OpaqueColor { } } +impl BitEq for OpaqueColor { + fn bit_eq(&self, other: &Self) -> bool { + self.components.bit_eq(&other.components) + } +} + +impl BitHash for OpaqueColor { + fn bit_hash(&self, state: &mut H) { + self.components.bit_hash(state); + } +} + /// Multiply components by a scalar. impl core::ops::Mul for AlphaColor { type Output = Self; @@ -776,6 +791,18 @@ impl core::ops::Sub for AlphaColor { } } +impl BitEq for AlphaColor { + fn bit_eq(&self, other: &Self) -> bool { + self.components.bit_eq(&other.components) + } +} + +impl BitHash for AlphaColor { + fn bit_hash(&self, state: &mut H) { + self.components.bit_hash(state); + } +} + /// Multiply components by a scalar. /// /// For rectangular color spaces, this is equivalent to multiplying @@ -830,6 +857,18 @@ impl core::ops::Sub for PremulColor { } } +impl BitEq for PremulColor { + fn bit_eq(&self, other: &Self) -> bool { + self.components.bit_eq(&other.components) + } +} + +impl BitHash for PremulColor { + fn bit_hash(&self, state: &mut H) { + self.components.bit_hash(state); + } +} + #[cfg(test)] mod tests { use super::{fixup_hue, AlphaColor, HueDirection, PremulColor, PremulRgba8, Rgba8, Srgb}; diff --git a/color/src/dynamic.rs b/color/src/dynamic.rs index 920c205..c76cdc1 100644 --- a/color/src/dynamic.rs +++ b/color/src/dynamic.rs @@ -4,6 +4,7 @@ //! CSS colors and syntax. use crate::{ + cache_key::{BitEq, BitHash}, color::{add_alpha, fixup_hues_for_interpolate, split_alpha}, AlphaColor, ColorSpace, ColorSpaceLayout, ColorSpaceTag, Flags, HueDirection, LinearSrgb, Missing, @@ -376,28 +377,29 @@ impl DynamicColor { } } -impl Hash for DynamicColor { - /// The hash is computed from the bit representation of the component values. - /// That makes it suitable for use as a cache key or memoization, but does not - /// match behavior for Rust float types. - fn hash(&self, state: &mut H) { - self.cs.hash(state); - self.flags.hash(state); - for c in self.components { - c.to_bits().hash(state); - } +impl PartialEq for DynamicColor { + /// Equality is not perceptual, but requires the component values to be equal. + /// + /// See also [`CacheKey`](crate::cache_key::CacheKey). + fn eq(&self, other: &Self) -> bool { + // Same as the derive implementation, but we want a doc comment. + self.cs == other.cs && self.flags == other.flags && self.components == other.components } } -impl PartialEq for DynamicColor { - /// Equality is determined based on the bit representation. - fn eq(&self, other: &Self) -> bool { +impl BitEq for DynamicColor { + fn bit_eq(&self, other: &Self) -> bool { self.cs == other.cs && self.flags == other.flags - && self.components[0].to_bits() == other.components[0].to_bits() - && self.components[1].to_bits() == other.components[1].to_bits() - && self.components[2].to_bits() == other.components[2].to_bits() - && self.components[3].to_bits() == other.components[3].to_bits() + && self.components.bit_eq(&other.components) + } +} + +impl BitHash for DynamicColor { + fn bit_hash(&self, state: &mut H) { + self.cs.hash(state); + self.flags.hash(state); + self.components.bit_hash(state); } } diff --git a/color/src/impl_bytemuck.rs b/color/src/impl_bytemuck.rs index f4747e9..0c68663 100644 --- a/color/src/impl_bytemuck.rs +++ b/color/src/impl_bytemuck.rs @@ -4,8 +4,8 @@ #![allow(unsafe_code, reason = "unsafe is required for bytemuck unsafe impls")] use crate::{ - AlphaColor, ColorSpace, ColorSpaceTag, HueDirection, OpaqueColor, PremulColor, PremulRgba8, - Rgba8, + cache_key::CacheKey, AlphaColor, ColorSpace, ColorSpaceTag, HueDirection, OpaqueColor, + PremulColor, PremulRgba8, Rgba8, }; // Safety: The struct is `repr(transparent)` and the data member is bytemuck::Pod. @@ -97,10 +97,14 @@ unsafe impl bytemuck::Contiguous for HueDirection { const MAX_VALUE: u8 = Self::Decreasing as u8; } +// Safety: The struct is `repr(transparent)`. +unsafe impl bytemuck::TransparentWrapper for CacheKey {} + #[cfg(test)] mod tests { use crate::{ - AlphaColor, ColorSpaceTag, HueDirection, OpaqueColor, PremulColor, PremulRgba8, Rgba8, Srgb, + cache_key::CacheKey, AlphaColor, ColorSpaceTag, HueDirection, OpaqueColor, PremulColor, + PremulRgba8, Rgba8, Srgb, }; use bytemuck::{checked::try_from_bytes, Contiguous, TransparentWrapper, Zeroable}; use core::{marker::PhantomData, ptr}; @@ -215,6 +219,10 @@ mod tests { let pc = PremulColor::::new([1., 2., 3., 0.]); let pi: [f32; 4] = PremulColor::::peel(pc); assert_eq!(pi, [1., 2., 3., 0.]); + + let ck = CacheKey::::new(1.); + let ci: f32 = CacheKey::::peel(ck); + assert_eq!(ci, 1.); } #[test] diff --git a/color/src/lib.rs b/color/src/lib.rs index 1f22cbc..0ad3955 100644 --- a/color/src/lib.rs +++ b/color/src/lib.rs @@ -84,19 +84,22 @@ #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(all(not(feature = "std"), not(test)), no_std)] +pub mod cache_key; mod color; mod colorspace; +mod dynamic; mod flags; mod gradient; -// Note: this may become feature-gated; we'll decide this soon -mod dynamic; pub mod palette; -mod parse; mod rgba8; mod serialize; mod tag; mod x11_colors; +// Note: this may become feature-gated; we'll decide this soon +// (This line is isolated so that the comment binds to it with import ordering) +mod parse; + #[cfg(feature = "bytemuck")] mod impl_bytemuck;