Skip to content

Commit

Permalink
CacheKey type for maps which use colours as keys (#92)
Browse files Browse the repository at this point in the history
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<T>`.
These traits are implemented for f32, and the colour types in this crate
(except the 8 bit types)
  • Loading branch information
DJMcNab authored Dec 13, 2024
1 parent 6c4419c commit f24518d
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 25 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down
210 changes: 210 additions & 0 deletions color/src/cache_key.rs
Original file line number Diff line number Diff line change
@@ -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<T>`] 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<T>(pub T);

impl<T> CacheKey<T> {
/// 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<T: BitEq> Eq for CacheKey<T> {}
impl<T: BitEq> PartialEq for CacheKey<T> {
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<T: BitHash> Hash for CacheKey<T> {
fn hash<H: Hasher>(&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<H: Hasher>(&self, state: &mut H);
// Intentionally no hash_slice for simplicity.
}

impl BitHash for f32 {
fn bit_hash<H: Hasher>(&self, state: &mut H) {
self.to_bits().hash(state);
}
}
impl<T: BitHash, const N: usize> BitHash for [T; N] {
fn bit_hash<H: Hasher>(&self, state: &mut H) {
self[..].bit_hash(state);
}
}

impl<T: BitHash> BitHash for [T] {
fn bit_hash<H: Hasher>(&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<T>]`, then
// using `Hash::hash` on the resulting slice.
state.write_usize(self.len());
for piece in self {
piece.bit_hash(state);
}
}
}

impl<T: BitHash> BitHash for &T {
fn bit_hash<H: Hasher>(&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<T: BitEq, const N: usize> 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<T: BitEq> 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<T: BitEq> 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<T: Hash> BitHash for T {...}
// impl<T: PartialEq + Eq> 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<CacheKey<f32>, 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<CacheKey<DynamicColor>, 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);
}
}
41 changes: 40 additions & 1 deletion color/src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -726,6 +729,18 @@ impl<CS: ColorSpace> core::ops::Sub for OpaqueColor<CS> {
}
}

impl<CS> BitEq for OpaqueColor<CS> {
fn bit_eq(&self, other: &Self) -> bool {
self.components.bit_eq(&other.components)
}
}

impl<CS> BitHash for OpaqueColor<CS> {
fn bit_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.components.bit_hash(state);
}
}

/// Multiply components by a scalar.
impl<CS: ColorSpace> core::ops::Mul<f32> for AlphaColor<CS> {
type Output = Self;
Expand Down Expand Up @@ -776,6 +791,18 @@ impl<CS: ColorSpace> core::ops::Sub for AlphaColor<CS> {
}
}

impl<CS> BitEq for AlphaColor<CS> {
fn bit_eq(&self, other: &Self) -> bool {
self.components.bit_eq(&other.components)
}
}

impl<CS> BitHash for AlphaColor<CS> {
fn bit_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.components.bit_hash(state);
}
}

/// Multiply components by a scalar.
///
/// For rectangular color spaces, this is equivalent to multiplying
Expand Down Expand Up @@ -830,6 +857,18 @@ impl<CS: ColorSpace> core::ops::Sub for PremulColor<CS> {
}
}

impl<CS> BitEq for PremulColor<CS> {
fn bit_eq(&self, other: &Self) -> bool {
self.components.bit_eq(&other.components)
}
}

impl<CS> BitHash for PremulColor<CS> {
fn bit_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.components.bit_hash(state);
}
}

#[cfg(test)]
mod tests {
use super::{fixup_hue, AlphaColor, HueDirection, PremulColor, PremulRgba8, Rgba8, Srgb};
Expand Down
36 changes: 19 additions & 17 deletions color/src/dynamic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<H: Hasher>(&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<H: Hasher>(&self, state: &mut H) {
self.cs.hash(state);
self.flags.hash(state);
self.components.bit_hash(state);
}
}

Expand Down
Loading

0 comments on commit f24518d

Please sign in to comment.