diff --git a/src/cli/damage_tracking.rs b/src/cli/damage_tracking.rs index 726a0532..9532ff1b 100644 --- a/src/cli/damage_tracking.rs +++ b/src/cli/damage_tracking.rs @@ -1,7 +1,7 @@ use { crate::{ cli::{GlobalArgs, color::parse_color, duration::parse_duration}, - theme::TransferFunction, + cmm::cmm_transfer_function::TransferFunction, tools::tool_client::{ToolClient, with_tool_client}, wire::jay_damage_tracking::{SetVisualizerColor, SetVisualizerDecay, SetVisualizerEnabled}, }, diff --git a/src/cmm.rs b/src/cmm.rs new file mode 100644 index 00000000..dac260c1 --- /dev/null +++ b/src/cmm.rs @@ -0,0 +1,8 @@ +pub mod cmm_description; +pub mod cmm_luminance; +pub mod cmm_manager; +pub mod cmm_primaries; +#[cfg(test)] +mod cmm_tests; +pub mod cmm_transfer_function; +pub mod cmm_transform; diff --git a/src/cmm/cmm_description.rs b/src/cmm/cmm_description.rs new file mode 100644 index 00000000..a1c1a499 --- /dev/null +++ b/src/cmm/cmm_description.rs @@ -0,0 +1,81 @@ +use { + crate::{ + cmm::{ + cmm_luminance::{Luminance, white_balance}, + cmm_manager::Shared, + cmm_primaries::{NamedPrimaries, Primaries}, + cmm_transfer_function::TransferFunction, + cmm_transform::{ColorMatrix, Local, Xyz, bradford_adjustment}, + }, + utils::free_list::FreeList, + }, + std::rc::Rc, +}; + +linear_ids!(LinearColorDescriptionIds, LinearColorDescriptionId, u64); + +pub type ColorDescriptionIds = FreeList; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct ColorDescriptionId(u32); + +impl From for ColorDescriptionId { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for u32 { + fn from(value: ColorDescriptionId) -> Self { + value.0 + } +} + +#[derive(Debug)] +pub struct LinearColorDescription { + pub id: LinearColorDescriptionId, + pub primaries: Primaries, + pub xyz_from_local: ColorMatrix, + pub local_from_xyz: ColorMatrix, + pub luminance: Luminance, + pub(super) shared: Rc, +} + +#[derive(Debug)] +pub struct ColorDescription { + pub id: ColorDescriptionId, + #[expect(dead_code)] + pub linear: Rc, + #[expect(dead_code)] + pub named_primaries: Option, + #[expect(dead_code)] + pub transfer_function: TransferFunction, + pub(super) shared: Rc, +} + +impl LinearColorDescription { + #[expect(dead_code)] + pub fn color_transform(&self, target: &Self) -> ColorMatrix { + let mut mat = target.local_from_xyz; + if self.luminance != target.luminance { + mat *= white_balance(&self.luminance, &target.luminance, target.primaries.wp); + } + if self.primaries.wp != target.primaries.wp { + mat *= bradford_adjustment(self.primaries.wp, target.primaries.wp); + } + mat * self.xyz_from_local + } +} + +impl Drop for LinearColorDescription { + fn drop(&mut self) { + self.shared.dead_linear.fetch_add(1); + } +} + +impl Drop for ColorDescription { + fn drop(&mut self) { + self.shared.dead_complete.fetch_add(1); + self.shared.complete_ids.release(self.id); + } +} diff --git a/src/cmm/cmm_luminance.rs b/src/cmm/cmm_luminance.rs new file mode 100644 index 00000000..6f5100af --- /dev/null +++ b/src/cmm/cmm_luminance.rs @@ -0,0 +1,72 @@ +use crate::{ + cmm::cmm_transform::{ColorMatrix, Xyz}, + utils::ordered_float::F64, +}; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct Luminance { + pub min: F64, + pub max: F64, + pub white: F64, +} + +impl Luminance { + pub const SRGB: Self = Self { + min: F64(0.2), + max: F64(80.0), + white: F64(80.0), + }; + + #[expect(dead_code)] + pub const BT1886: Self = Self { + min: F64(0.01), + max: F64(100.0), + white: F64(100.0), + }; + + pub const ST2084_PQ: Self = Self { + min: F64(0.0), + max: F64(10000.0), + white: F64(203.0), + }; + + #[expect(dead_code)] + pub const HLG: Self = Self { + min: F64(0.005), + max: F64(1000.0), + white: F64(203.0), + }; + + pub const WINDOWS_SCRGB: Self = Self { + min: Self::ST2084_PQ.min, + max: Self::ST2084_PQ.max, + // This causes the white balance formula (with target ST2084_PQ) to simplify to + // `Y * 80 / 10000`, meaning that sRGB pure white maps to a luminance of + // 80 cd/m^2. + white: F64(Self::ST2084_PQ.white.0 / 80.0 * Self::ST2084_PQ.max.0), + }; +} + +impl Default for Luminance { + fn default() -> Self { + Self::SRGB + } +} + +#[expect(non_snake_case)] +pub fn white_balance(from: &Luminance, to: &Luminance, w_to: (F64, F64)) -> ColorMatrix { + let a = ((from.max - from.min) / (to.max - to.min) * (to.white - from.min) + / (from.white - from.min)) + .0; + let d = ((from.min - to.min) / (to.max - to.min)).0.max(0.0); + let s = a - d; + let (F64(x_to), F64(y_to)) = w_to; + let X_to = x_to / y_to; + let Y_to = 1.0; + let Z_to = (1.0 - x_to - y_to) / y_to; + ColorMatrix::new([ + [s, 0.0, 0.0, d * X_to], + [0.0, s, 0.0, d * Y_to], + [0.0, 0.0, s, d * Z_to], + ]) +} diff --git a/src/cmm/cmm_manager.rs b/src/cmm/cmm_manager.rs new file mode 100644 index 00000000..23505765 --- /dev/null +++ b/src/cmm/cmm_manager.rs @@ -0,0 +1,204 @@ +use { + crate::{ + cmm::{ + cmm_description::{ + ColorDescription, ColorDescriptionIds, LinearColorDescription, + LinearColorDescriptionId, LinearColorDescriptionIds, + }, + cmm_luminance::Luminance, + cmm_primaries::{NamedPrimaries, Primaries}, + cmm_transfer_function::TransferFunction, + }, + utils::{copyhashmap::CopyHashMap, numcell::NumCell}, + }, + std::rc::{Rc, Weak}, +}; + +pub struct ColorManager { + linear_ids: LinearColorDescriptionIds, + linear_descriptions: CopyHashMap>, + complete_descriptions: CopyHashMap>, + shared: Rc, + srgb_srgb: Rc, + srgb_linear: Rc, + windows_scrgb: Rc, +} + +#[derive(Debug, Default)] +pub(super) struct Shared { + pub(super) dead_linear: NumCell, + pub(super) dead_complete: NumCell, + pub(super) complete_ids: ColorDescriptionIds, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +struct LinearDescriptionKey { + primaries: Primaries, + luminance: Luminance, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +struct CompleteDescriptionKey { + linear: LinearColorDescriptionId, + named_primaries: Option, + transfer_function: TransferFunction, +} + +impl ColorManager { + pub fn new() -> Rc { + let linear_ids = LinearColorDescriptionIds::default(); + let linear_descriptions = CopyHashMap::default(); + let complete_descriptions = CopyHashMap::default(); + let shared = Rc::new(Shared::default()); + let _ = shared.complete_ids.acquire(); + let srgb_srgb = get_description( + &shared, + &linear_descriptions, + &complete_descriptions, + &linear_ids, + Some(NamedPrimaries::Srgb), + Primaries::SRGB, + Luminance::SRGB, + TransferFunction::Srgb, + ); + let srgb_linear = get_description( + &shared, + &linear_descriptions, + &complete_descriptions, + &linear_ids, + Some(NamedPrimaries::Srgb), + Primaries::SRGB, + Luminance::SRGB, + TransferFunction::Linear, + ); + let windows_scrgb = get_description( + &shared, + &linear_descriptions, + &complete_descriptions, + &linear_ids, + Some(NamedPrimaries::Srgb), + Primaries::SRGB, + Luminance::WINDOWS_SCRGB, + TransferFunction::Linear, + ); + Rc::new(Self { + linear_ids, + linear_descriptions, + complete_descriptions, + shared, + srgb_srgb, + srgb_linear, + windows_scrgb, + }) + } + + #[expect(dead_code)] + pub fn srgb_srgb(&self) -> &Rc { + &self.srgb_srgb + } + + #[expect(dead_code)] + pub fn srgb_linear(&self) -> &Rc { + &self.srgb_linear + } + + #[expect(dead_code)] + pub fn windows_scrgb(&self) -> &Rc { + &self.windows_scrgb + } + + #[expect(dead_code)] + pub fn get_description( + self: &Rc, + named_primaries: Option, + primaries: Primaries, + luminance: Luminance, + transfer_function: TransferFunction, + ) -> Rc { + get_description( + &self.shared, + &self.linear_descriptions, + &self.complete_descriptions, + &self.linear_ids, + named_primaries, + primaries, + luminance, + transfer_function, + ) + } +} + +fn get_description( + shared: &Rc, + linear_descriptions: &CopyHashMap>, + complete_descriptions: &CopyHashMap>, + linear_ids: &LinearColorDescriptionIds, + named_primaries: Option, + primaries: Primaries, + luminance: Luminance, + transfer_function: TransferFunction, +) -> Rc { + macro_rules! gc { + ($d:ident, $i:expr) => { + if $d.len() > 16 && $i.get() * 2 > $d.len() { + $d.lock().retain(|_, d| d.strong_count() > 0); + $i.set(0); + } + }; + } + gc!(linear_descriptions, &shared.dead_linear); + gc!(complete_descriptions, &shared.dead_complete); + let key = LinearDescriptionKey { + primaries, + luminance, + }; + if let Some(d) = linear_descriptions.get(&key) { + if let Some(d) = d.upgrade() { + let key = CompleteDescriptionKey { + linear: d.id, + named_primaries, + transfer_function, + }; + if let Some(d) = complete_descriptions.get(&key) { + if let Some(d) = d.upgrade() { + return d; + } + shared.dead_complete.fetch_sub(1); + } + let d = Rc::new(ColorDescription { + id: shared.complete_ids.acquire(), + linear: d, + named_primaries, + transfer_function, + shared: shared.clone(), + }); + complete_descriptions.set(key, Rc::downgrade(&d)); + return d; + } + shared.dead_linear.fetch_sub(1); + } + let (xyz_from_local, local_from_xyz) = primaries.matrices(); + let d = Rc::new(LinearColorDescription { + id: linear_ids.next(), + primaries, + xyz_from_local, + local_from_xyz, + luminance, + shared: shared.clone(), + }); + linear_descriptions.set(key, Rc::downgrade(&d)); + let key = CompleteDescriptionKey { + linear: d.id, + named_primaries, + transfer_function, + }; + let d = Rc::new(ColorDescription { + id: shared.complete_ids.acquire(), + linear: d, + named_primaries, + transfer_function, + shared: shared.clone(), + }); + complete_descriptions.set(key, Rc::downgrade(&d)); + d +} diff --git a/src/cmm/cmm_primaries.rs b/src/cmm/cmm_primaries.rs new file mode 100644 index 00000000..39f0be05 --- /dev/null +++ b/src/cmm/cmm_primaries.rs @@ -0,0 +1,121 @@ +use {crate::utils::ordered_float::F64, std::hash::Hash}; + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub enum NamedPrimaries { + Srgb, + #[expect(dead_code)] + PalM, + #[expect(dead_code)] + Pal, + #[expect(dead_code)] + Ntsc, + #[expect(dead_code)] + GenericFilm, + #[expect(dead_code)] + Bt2020, + #[expect(dead_code)] + Cie1931Xyz, + #[expect(dead_code)] + DciP3, + #[expect(dead_code)] + DisplayP3, + #[expect(dead_code)] + AdobeRgb, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct Primaries { + pub r: (F64, F64), + pub g: (F64, F64), + pub b: (F64, F64), + pub wp: (F64, F64), +} + +impl Primaries { + pub const SRGB: Self = Self { + r: (F64(0.64), F64(0.33)), + g: (F64(0.3), F64(0.6)), + b: (F64(0.15), F64(0.06)), + wp: (F64(0.3127), F64(0.3290)), + }; + + pub const PAL_M: Self = Self { + r: (F64(0.67), F64(0.33)), + g: (F64(0.21), F64(0.71)), + b: (F64(0.14), F64(0.08)), + wp: (F64(0.310), F64(0.316)), + }; + + pub const PAL: Self = Self { + r: (F64(0.64), F64(0.33)), + g: (F64(0.29), F64(0.60)), + b: (F64(0.15), F64(0.06)), + wp: (F64(0.3127), F64(0.3290)), + }; + + pub const NTSC: Self = Self { + r: (F64(0.630), F64(0.340)), + g: (F64(0.310), F64(0.595)), + b: (F64(0.155), F64(0.070)), + wp: (F64(0.3127), F64(0.3290)), + }; + + pub const GENERIC_FILM: Self = Self { + r: (F64(0.681), F64(0.319)), + g: (F64(0.243), F64(0.692)), + b: (F64(0.145), F64(0.049)), + wp: (F64(0.310), F64(0.316)), + }; + + pub const BT2020: Self = Self { + r: (F64(0.708), F64(0.292)), + g: (F64(0.170), F64(0.797)), + b: (F64(0.131), F64(0.046)), + wp: (F64(0.3127), F64(0.3290)), + }; + + pub const CIE1931_XYZ: Self = Self { + r: (F64(1.0), F64(0.0)), + g: (F64(0.0), F64(1.0)), + b: (F64(0.0), F64(0.0)), + wp: (F64(1.0 / 3.0), F64(1.0 / 3.0)), + }; + + pub const DCI_P3: Self = Self { + r: (F64(0.680), F64(0.320)), + g: (F64(0.265), F64(0.690)), + b: (F64(0.150), F64(0.060)), + wp: (F64(0.314), F64(0.351)), + }; + + pub const DISPLAY_P3: Self = Self { + r: (F64(0.680), F64(0.320)), + g: (F64(0.265), F64(0.690)), + b: (F64(0.150), F64(0.060)), + wp: (F64(0.3127), F64(0.3290)), + }; + + pub const ADOBE_RGB: Self = Self { + r: (F64(0.64), F64(0.33)), + g: (F64(0.21), F64(0.71)), + b: (F64(0.15), F64(0.06)), + wp: (F64(0.3127), F64(0.3290)), + }; +} +impl NamedPrimaries { + #[expect(dead_code)] + pub const fn primaries(self) -> Primaries { + match self { + NamedPrimaries::Srgb => Primaries::SRGB, + NamedPrimaries::PalM => Primaries::PAL_M, + NamedPrimaries::Pal => Primaries::PAL, + NamedPrimaries::Ntsc => Primaries::NTSC, + NamedPrimaries::GenericFilm => Primaries::GENERIC_FILM, + NamedPrimaries::Bt2020 => Primaries::BT2020, + NamedPrimaries::Cie1931Xyz => Primaries::CIE1931_XYZ, + NamedPrimaries::DciP3 => Primaries::DCI_P3, + NamedPrimaries::DisplayP3 => Primaries::DISPLAY_P3, + NamedPrimaries::AdobeRgb => Primaries::ADOBE_RGB, + } + } +} diff --git a/src/cmm/cmm_tests.rs b/src/cmm/cmm_tests.rs new file mode 100644 index 00000000..52b98d72 --- /dev/null +++ b/src/cmm/cmm_tests.rs @@ -0,0 +1,201 @@ +mod matrices { + use crate::{cmm::cmm_primaries::Primaries, utils::ordered_float::F64}; + + fn check(primaries: Primaries, expected: [[f64; 4]; 3]) { + let (ltg, gtl) = primaries.matrices(); + println!("{:#?}", ltg); + assert!((ltg.0[0][0].0 - expected[0][0]).abs() < 0.001); + assert!((ltg.0[0][1].0 - expected[0][1]).abs() < 0.001); + assert!((ltg.0[0][2].0 - expected[0][2]).abs() < 0.001); + assert!((ltg.0[0][3].0 - expected[0][3]).abs() < 0.001); + assert!((ltg.0[1][0].0 - expected[1][0]).abs() < 0.001); + assert!((ltg.0[1][1].0 - expected[1][1]).abs() < 0.001); + assert!((ltg.0[1][2].0 - expected[1][2]).abs() < 0.001); + assert!((ltg.0[1][3].0 - expected[1][3]).abs() < 0.001); + assert!((ltg.0[2][0].0 - expected[2][0]).abs() < 0.001); + assert!((ltg.0[2][1].0 - expected[2][1]).abs() < 0.001); + assert!((ltg.0[2][2].0 - expected[2][2]).abs() < 0.001); + assert!((ltg.0[2][3].0 - expected[2][3]).abs() < 0.001); + let roundtrip = gtl * ltg; + assert!((roundtrip.0[0][0].0 - 1.0).abs() < 0.001); + assert!((roundtrip.0[0][1].0 - 0.0).abs() < 0.001); + assert!((roundtrip.0[0][2].0 - 0.0).abs() < 0.001); + assert!((roundtrip.0[0][3].0 - 0.0).abs() < 0.001); + assert!((roundtrip.0[1][0].0 - 0.0).abs() < 0.001); + assert!((roundtrip.0[1][1].0 - 1.0).abs() < 0.001); + assert!((roundtrip.0[1][2].0 - 0.0).abs() < 0.001); + assert!((roundtrip.0[1][3].0 - 0.0).abs() < 0.001); + assert!((roundtrip.0[2][0].0 - 0.0).abs() < 0.001); + assert!((roundtrip.0[2][1].0 - 0.0).abs() < 0.001); + assert!((roundtrip.0[2][2].0 - 1.0).abs() < 0.001); + assert!((roundtrip.0[2][3].0 - 0.0).abs() < 0.001); + } + + #[test] + fn srgb() { + check( + Primaries::SRGB, + [ + [0.4124564, 0.3575761, 0.1804375, 0.0], + [0.2126729, 0.7151522, 0.0721750, 0.0], + [0.0193339, 0.1191920, 0.9503041, 0.0], + ], + ); + } + + #[test] + fn cie1931_xyz() { + check( + Primaries::CIE1931_XYZ, + [ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + ], + ); + } + + #[test] + fn adobe_rgb() { + check( + Primaries::ADOBE_RGB, + [ + [0.5767309, 0.1855540, 0.1881852, 0.0], + [0.2973769, 0.6273491, 0.0752741, 0.0], + [0.0270343, 0.0706872, 0.9911085, 0.0], + ], + ); + } + + #[test] + fn apple_rgb() { + check( + Primaries { + r: (F64(0.625), F64(0.34)), + g: (F64(0.28), F64(0.595)), + b: (F64(0.155), F64(0.07)), + wp: (F64(0.31271), F64(0.32902)), + }, + [ + [0.4497288, 0.3162486, 0.1844926, 0.0], + [0.2446525, 0.6720283, 0.0833192, 0.0], + [0.0251848, 0.1411824, 0.9224628, 0.0], + ], + ); + } + + #[test] + fn bt2020() { + check( + Primaries::BT2020, + [ + [0.636958, 0.144617, 0.168881, 0.0], + [0.262700, 0.677998, 0.059302, 0.0], + [0.000000, 0.028073, 1.060985, 0.0], + ], + ); + } + + #[test] + fn pal() { + check( + Primaries::PAL, + [ + [0.4306190, 0.3415419, 0.1783091, 0.0], + [0.2220379, 0.7066384, 0.0713236, 0.0], + [0.0201853, 0.1295504, 0.9390944, 0.0], + ], + ); + } + + #[test] + fn dci_p3() { + check( + Primaries::DCI_P3, + [ + [0.445170, 0.277134, 0.172283, 0.0], + [0.209492, 0.721595, 0.068913, 0.0], + [-0.000000, 0.047061, 0.907355, 0.0], + ], + ); + } + + #[test] + fn display_p3() { + check( + Primaries::DISPLAY_P3, + [ + [0.486571, 0.265668, 0.198217, 0.0], + [0.228975, 0.691739, 0.079287, 0.0], + [-0.000000, 0.045113, 1.043944, 0.0], + ], + ); + } +} + +mod transforms { + use crate::cmm::{ + cmm_luminance::Luminance, cmm_manager::ColorManager, cmm_primaries::Primaries, + cmm_transfer_function::TransferFunction, + }; + + fn check(p1: Primaries, p2: Primaries, expected: [[f64; 4]; 3]) { + let manager = ColorManager::new(); + let d = |p| manager.get_description(None, p, Luminance::SRGB, TransferFunction::Linear); + let d1 = d(p1); + let d2 = d(p2); + let m = d1.linear.color_transform(&d2.linear); + println!("{:#?}", m); + assert!((m.0[0][0].0 - expected[0][0]).abs() < 0.001); + assert!((m.0[0][1].0 - expected[0][1]).abs() < 0.001); + assert!((m.0[0][2].0 - expected[0][2]).abs() < 0.001); + assert!((m.0[0][3].0 - expected[0][3]).abs() < 0.001); + assert!((m.0[1][0].0 - expected[1][0]).abs() < 0.001); + assert!((m.0[1][1].0 - expected[1][1]).abs() < 0.001); + assert!((m.0[1][2].0 - expected[1][2]).abs() < 0.001); + assert!((m.0[1][3].0 - expected[1][3]).abs() < 0.001); + assert!((m.0[2][0].0 - expected[2][0]).abs() < 0.001); + assert!((m.0[2][1].0 - expected[2][1]).abs() < 0.001); + assert!((m.0[2][2].0 - expected[2][2]).abs() < 0.001); + assert!((m.0[2][3].0 - expected[2][3]).abs() < 0.001); + } + + #[test] + fn srgb_to_bt2020() { + check( + Primaries::SRGB, + Primaries::BT2020, + [ + [0.627404, 0.329283, 0.043313, 0.0], + [0.069097, 0.919540, 0.011362, 0.0], + [0.016391, 0.088013, 0.895595, 0.0], + ], + ) + } + + #[test] + fn bt2020_to_srgb() { + check( + Primaries::BT2020, + Primaries::SRGB, + [ + [1.660491, -0.587641, -0.072850, 0.0], + [-0.124550, 1.132900, -0.008349, 0.0], + [-0.018151, -0.100579, 1.118730, 0.0], + ], + ) + } + + #[test] + fn srgb_to_dci_p3() { + check( + Primaries::SRGB, + Primaries::DCI_P3, + [ + [0.868580, 0.128919, 0.002501, 0.0], + [0.034540, 0.961811, 0.003648, 0.0], + [0.016771, 0.071040, 0.912189, 0.0], + ], + ) + } +} diff --git a/src/cmm/cmm_transfer_function.rs b/src/cmm/cmm_transfer_function.rs new file mode 100644 index 00000000..22fc9920 --- /dev/null +++ b/src/cmm/cmm_transfer_function.rs @@ -0,0 +1,5 @@ +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum TransferFunction { + Srgb, + Linear, +} diff --git a/src/cmm/cmm_transform.rs b/src/cmm/cmm_transform.rs new file mode 100644 index 00000000..c175caef --- /dev/null +++ b/src/cmm/cmm_transform.rs @@ -0,0 +1,258 @@ +use { + crate::{ + cmm::{cmm_primaries::Primaries, cmm_transfer_function::TransferFunction}, + theme::Color, + utils::{debug_fn::debug_fn, ordered_float::F64}, + }, + std::{ + fmt::{Debug, Formatter}, + hash::{Hash, Hasher}, + marker::PhantomData, + ops::{Mul, MulAssign}, + }, +}; + +pub struct ColorMatrix(pub [[F64; 4]; 3], PhantomData<(To, From)>); + +#[derive(Copy, Clone)] +pub struct Local; +#[derive(Copy, Clone)] +pub struct Xyz; +#[derive(Copy, Clone)] +pub struct Bradford; + +impl Copy for ColorMatrix {} + +impl Clone for ColorMatrix { + fn clone(&self) -> Self { + *self + } +} + +impl PartialEq for ColorMatrix { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for ColorMatrix {} + +impl Hash for ColorMatrix { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl Debug for ColorMatrix { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("ColorMatrix") + .field(&format_matrix(&self.0)) + .finish() + } +} + +fn format_matrix<'a>(m: &'a [[F64; 4]; 3]) -> impl Debug + use<'a> { + debug_fn(move |f| { + let iter = m + .iter() + .copied() + .chain(Some([F64(0.0), F64(0.0), F64(0.0), F64(1.0)])) + .enumerate(); + if f.alternate() { + for (idx, row) in iter { + if idx > 0 { + f.write_str("\n")?; + } + write!( + f, + "{:7.4} {:7.4} {:7.4} {:7.4}", + row[0], row[1], row[2], row[3] + )?; + } + } else { + f.write_str("[")?; + for (idx, row) in iter { + if idx > 0 { + f.write_str(", ")?; + } + write!( + f, + "[{:.4}, {:.4}, {:.4}, {:.4}]", + row[0], row[1], row[2], row[3] + )?; + } + f.write_str("]")?; + } + Ok(()) + }) +} + +impl Mul> for ColorMatrix { + type Output = ColorMatrix; + + fn mul(self, rhs: ColorMatrix) -> Self::Output { + let a = &self.0; + let b = &rhs.0; + macro_rules! mul { + ($ar:expr, $bc:expr) => { + a[$ar][0] * b[0][$bc] + a[$ar][1] * b[1][$bc] + a[$ar][2] * b[2][$bc] + }; + } + let m = [ + [mul!(0, 0), mul!(0, 1), mul!(0, 2), mul!(0, 3) + a[0][3]], + [mul!(1, 0), mul!(1, 1), mul!(1, 2), mul!(1, 3) + a[1][3]], + [mul!(2, 0), mul!(2, 1), mul!(2, 2), mul!(2, 3) + a[2][3]], + ]; + ColorMatrix(m, PhantomData) + } +} + +impl MulAssign> for ColorMatrix { + fn mul_assign(&mut self, rhs: ColorMatrix) { + *self = *self * rhs; + } +} + +impl Mul<[f64; 3]> for ColorMatrix { + type Output = [f64; 3]; + + fn mul(self, rhs: [f64; 3]) -> Self::Output { + let a = &self.0; + macro_rules! mul { + ($ar:expr) => { + a[$ar][0].0 * rhs[0] + a[$ar][1].0 * rhs[1] + a[$ar][2].0 * rhs[2] + }; + } + [mul!(0), mul!(1), mul!(2)] + } +} + +impl Mul for ColorMatrix { + type Output = Color; + + fn mul(self, rhs: Color) -> Self::Output { + let mut rgba = rhs.to_array(TransferFunction::Linear); + let a = rgba[3]; + if a < 1.0 && a > 0.0 { + for c in &mut rgba[..3] { + *c /= a; + } + } + let [r, g, b] = self * [rgba[0] as f64, rgba[1] as f64, rgba[2] as f64]; + let mut color = Color::new(TransferFunction::Linear, r as f32, g as f32, b as f32); + if a < 1.0 { + color = color * a; + } + color + } +} + +impl ColorMatrix { + pub const fn new(m: [[f64; 4]; 3]) -> Self { + let m = [ + [F64(m[0][0]), F64(m[0][1]), F64(m[0][2]), F64(m[0][3])], + [F64(m[1][0]), F64(m[1][1]), F64(m[1][2]), F64(m[1][3])], + [F64(m[2][0]), F64(m[2][1]), F64(m[2][2]), F64(m[2][3])], + ]; + Self(m, PhantomData) + } + + #[expect(dead_code)] + pub const fn to_f32(&self) -> [[f32; 4]; 4] { + let m = &self.0; + macro_rules! map { + ($r:expr, $c:expr) => { + m[$r][$c].0 as f32 + }; + } + [ + [map!(0, 0), map!(0, 1), map!(0, 2), map!(0, 3)], + [map!(1, 0), map!(1, 1), map!(1, 2), map!(1, 3)], + [map!(2, 0), map!(2, 1), map!(2, 2), map!(2, 3)], + [0.0, 0.0, 0.0, 1.0], + ] + } +} + +impl ColorMatrix { + const BFD: Self = Self::new([ + [0.8951, 0.2664, -0.1614, 0.0], + [-0.7502, 1.7135, 0.0367, 0.0], + [0.0389, -0.0685, 1.0296, 0.0], + ]); +} + +impl ColorMatrix { + const BFD_INV: Self = Self::new([ + [0.9870, -0.1471, 0.1600, 0.0], + [0.4323, 0.5184, 0.0493, 0.0], + [-0.0085, 0.04, 0.9685, 0.0], + ]); +} + +#[expect(non_snake_case)] +pub fn bradford_adjustment(w_from: (F64, F64), w_to: (F64, F64)) -> ColorMatrix { + let (F64(x_from), F64(y_from)) = w_from; + let (F64(x_to), F64(y_to)) = w_to; + let X_from = x_from / y_from; + let Z_from = (1.0 - x_from - y_from) / y_from; + let X_to = x_to / y_to; + let Z_to = (1.0 - x_to - y_to) / y_to; + let [R_from, G_from, B_from] = ColorMatrix::BFD * [X_from, 1.0, Z_from]; + let [R_to, G_to, B_to] = ColorMatrix::BFD * [X_to, 1.0, Z_to]; + let adj = ColorMatrix::new([ + [R_to / R_from, 0.0, 0.0, 0.0], + [0.0, G_to / G_from, 0.0, 0.0], + [0.0, 0.0, B_to / B_from, 0.0], + ]); + ColorMatrix::BFD_INV * adj * ColorMatrix::BFD +} + +impl Primaries { + #[expect(non_snake_case)] + pub const fn matrices(&self) -> (ColorMatrix, ColorMatrix) { + let (F64(xw), F64(yw)) = self.wp; + let Xw = xw / yw; + let Zw = (1.0 - xw - yw) / yw; + let (F64(xr), F64(yr)) = self.r; + let (F64(xg), F64(yg)) = self.g; + let (F64(xb), F64(yb)) = self.b; + let zr = 1.0 - xr - yr; + let zg = 1.0 - xg - yg; + let zb = 1.0 - xb - yb; + let srx = yg * zb - zg * yb; + let sry = zg * xb - xg * zb; + let srz = xg * yb - yg * xb; + let sgx = zr * yb - yr * zb; + let sgz = yr * xb - xr * yb; + let sgy = xr * zb - zr * xb; + let sbx = yr * zg - zr * yg; + let sby = zr * xg - xr * zg; + let sbz = xr * yg - yr * xg; + let det = srz + sgz + sbz; + let sr = srx * Xw + sry + srz * Zw; + let sg = sgx * Xw + sgy + sgz * Zw; + let sb = sbx * Xw + sby + sbz * Zw; + let det_inv = 1.0 / det; + let sr_inv = 1.0 / sr; + let sg_inv = 1.0 / sg; + let sb_inv = 1.0 / sb; + let srp = sr * det_inv; + let sgp = sg * det_inv; + let sbp = sb * det_inv; + let XYZ_from_local = [ + [srp * xr, sgp * xg, sbp * xb, 0.0], + [srp * yr, sgp * yg, sbp * yb, 0.0], + [srp * zr, sgp * zg, sbp * zb, 0.0], + ]; + let local_from_XYZ = [ + [srx * sr_inv, sry * sr_inv, srz * sr_inv, 0.0], + [sgx * sg_inv, sgy * sg_inv, sgz * sg_inv, 0.0], + [sbx * sb_inv, sby * sb_inv, sbz * sb_inv, 0.0], + ]; + ( + ColorMatrix::new(XYZ_from_local), + ColorMatrix::new(local_from_XYZ), + ) + } +} diff --git a/src/compositor.rs b/src/compositor.rs index 06ca97d2..e9ffb99c 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -12,6 +12,7 @@ use { cli::{CliBackend, GlobalArgs, RunArgs}, client::{ClientId, Clients}, clientmem::{self, ClientMemError}, + cmm::cmm_manager::ColorManager, config::ConfigProxy, cpu_worker::{CpuWorker, CpuWorkerError}, damage::{DamageVisualizer, visualize_damage}, @@ -284,6 +285,7 @@ fn start_compositor2( data_control_device_ids: Default::default(), workspace_managers: Default::default(), color_management_enabled: Cell::new(false), + color_manager: ColorManager::new(), }); state.tracker.register(ClientId::from_raw(0)); create_dummy_output(&state); diff --git a/src/config/handler.rs b/src/config/handler.rs index fb4ee4e5..aa416023 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -5,6 +5,7 @@ use { self, ConnectorId, DrmDeviceId, InputDeviceAccelProfile, InputDeviceCapability, InputDeviceId, }, + cmm::cmm_transfer_function::TransferFunction, compositor::MAX_EXTENTS, config::ConfigProxy, format::config_formats, @@ -14,7 +15,7 @@ use { output_schedule::map_cursor_hz, scale::Scale, state::{ConnectorData, DeviceHandlerData, DrmDevData, OutputData, State}, - theme::{Color, ThemeSized, TransferFunction}, + theme::{Color, ThemeSized}, tree::{ ContainerNode, ContainerSplit, FloatNode, Node, NodeVisitorBase, OutputNode, TearingMode, VrrMode, WsMoveConfig, move_ws_to_output, diff --git a/src/gfx_apis/gl.rs b/src/gfx_apis/gl.rs index ff86fc95..ff3dc526 100644 --- a/src/gfx_apis/gl.rs +++ b/src/gfx_apis/gl.rs @@ -67,6 +67,7 @@ macro_rules! dynload { use { crate::{ + cmm::cmm_transfer_function::TransferFunction, gfx_api::{ AcquireSync, CopyTexture, FillRect, GfxApiOpt, GfxContext, GfxError, GfxTexture, ReleaseSync, SyncFile, @@ -84,7 +85,7 @@ use { GL_TRIANGLE_STRIP, GL_TRIANGLES, }, }, - theme::{Color, TransferFunction}, + theme::Color, utils::{errorfmt::ErrorFmt, rc_eq::rc_eq, vecstorage::VecStorage}, video::{ dmabuf::DMA_BUF_SYNC_READ, diff --git a/src/gfx_apis/gl/renderer/framebuffer.rs b/src/gfx_apis/gl/renderer/framebuffer.rs index 550fdd18..714cdc69 100644 --- a/src/gfx_apis/gl/renderer/framebuffer.rs +++ b/src/gfx_apis/gl/renderer/framebuffer.rs @@ -1,5 +1,6 @@ use { crate::{ + cmm::cmm_transfer_function::TransferFunction, format::Format, gfx_api::{ AcquireSync, AsyncShmGfxTextureCallback, GfxApiOpt, GfxBlendBuffer, GfxError, @@ -18,7 +19,7 @@ use { sys::{GL_ONE, GL_ONE_MINUS_SRC_ALPHA}, }, rect::Region, - theme::{Color, TransferFunction}, + theme::Color, }, std::{ cell::Cell, diff --git a/src/gfx_apis/vulkan/renderer.rs b/src/gfx_apis/vulkan/renderer.rs index 2258d07c..b46e5af7 100644 --- a/src/gfx_apis/vulkan/renderer.rs +++ b/src/gfx_apis/vulkan/renderer.rs @@ -1,6 +1,7 @@ use { crate::{ async_engine::{AsyncEngine, SpawnedFuture}, + cmm::cmm_transfer_function::TransferFunction, cpu_worker::PendingJob, format::XRGB8888, gfx_api::{ @@ -30,7 +31,7 @@ use { }, io_uring::IoUring, rect::{Rect, Region}, - theme::{Color, TransferFunction}, + theme::Color, utils::{copyhashmap::CopyHashMap, errorfmt::ErrorFmt, numcell::NumCell, stack::Stack}, video::dmabuf::{DMA_BUF_SYNC_READ, DMA_BUF_SYNC_WRITE, dma_buf_export_sync_file}, }, diff --git a/src/ifs/jay_damage_tracking.rs b/src/ifs/jay_damage_tracking.rs index 021f512c..126a2c10 100644 --- a/src/ifs/jay_damage_tracking.rs +++ b/src/ifs/jay_damage_tracking.rs @@ -1,10 +1,11 @@ use { crate::{ client::{CAP_JAY_COMPOSITOR, Client, ClientCaps, ClientError}, + cmm::cmm_transfer_function::TransferFunction, globals::{Global, GlobalName}, leaks::Tracker, object::{Object, Version}, - theme::{Color, TransferFunction}, + theme::Color, wire::{ JayCompositorId, jay_damage_tracking::{ diff --git a/src/it/test_ifs/test_single_pixel_buffer_manager.rs b/src/it/test_ifs/test_single_pixel_buffer_manager.rs index 9fc989bb..4685279c 100644 --- a/src/it/test_ifs/test_single_pixel_buffer_manager.rs +++ b/src/it/test_ifs/test_single_pixel_buffer_manager.rs @@ -1,10 +1,11 @@ use { crate::{ + cmm::cmm_transfer_function::TransferFunction, it::{ test_error::TestResult, test_ifs::test_buffer::TestBuffer, test_object::TestObject, test_transport::TestTransport, }, - theme::{Color, TransferFunction}, + theme::Color, wire::{WpSinglePixelBufferManagerV1Id, wp_single_pixel_buffer_manager_v1::*}, }, std::{cell::Cell, rc::Rc}, diff --git a/src/macros.rs b/src/macros.rs index 3579eee2..13d1db66 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -185,10 +185,10 @@ macro_rules! shared_ids { } macro_rules! linear_ids { - ($ids:ident, $id:ident) => { + ($ids:ident, $id:ident $(,)?) => { linear_ids!($ids, $id, u32); }; - ($ids:ident, $id:ident, $ty:ty) => { + ($ids:ident, $id:ident, $ty:ty $(,)?) => { pub struct $ids { next: crate::utils::numcell::NumCell<$ty>, } diff --git a/src/main.rs b/src/main.rs index 05032eed..6c9052e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,7 +35,8 @@ clippy::manual_is_ascii_check, clippy::needless_borrow, clippy::unnecessary_cast, - clippy::manual_flatten + clippy::manual_flatten, + clippy::manual_bits )] #![warn(clippy::allow_attributes, unsafe_op_in_unsafe_fn)] @@ -54,6 +55,7 @@ mod bugs; mod cli; mod client; mod clientmem; +mod cmm; mod compositor; mod config; mod cpu_worker; diff --git a/src/portal/ptl_text.rs b/src/portal/ptl_text.rs index 32ecc0ed..02ea796d 100644 --- a/src/portal/ptl_text.rs +++ b/src/portal/ptl_text.rs @@ -1,5 +1,6 @@ use { crate::{ + cmm::cmm_transfer_function::TransferFunction, format::ARGB8888, gfx_api::{GfxContext, GfxTexture}, pango::{ @@ -7,7 +8,7 @@ use { consts::{CAIRO_FORMAT_ARGB32, CAIRO_OPERATOR_SOURCE}, }, rect::Rect, - theme::{Color, TransferFunction}, + theme::Color, }, std::{ops::Neg, rc::Rc, sync::Arc}, }; diff --git a/src/renderer.rs b/src/renderer.rs index 7965c620..6948a611 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1,5 +1,6 @@ use { crate::{ + cmm::cmm_transfer_function::TransferFunction, gfx_api::{AcquireSync, GfxApiOpt, ReleaseSync, SampleRect}, ifs::wl_surface::{ SurfaceBuffer, WlSurface, @@ -11,7 +12,7 @@ use { renderer::renderer_base::RendererBase, scale::Scale, state::State, - theme::{Color, TransferFunction}, + theme::Color, tree::{ ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData, ToplevelNodeBase, WorkspaceNode, diff --git a/src/state.rs b/src/state.rs index d56b523f..cb136dc6 100644 --- a/src/state.rs +++ b/src/state.rs @@ -11,6 +11,7 @@ use { cli::RunArgs, client::{Client, ClientId, Clients, NUM_CACHED_SERIAL_RANGES, SerialRange}, clientmem::ClientMemOffset, + cmm::cmm_manager::ColorManager, compositor::LIBEI_SOCKET, config::ConfigProxy, cpu_worker::CpuWorker, @@ -234,6 +235,8 @@ pub struct State { pub data_control_device_ids: DataControlDeviceIds, pub workspace_managers: WorkspaceManagerState, pub color_management_enabled: Cell, + #[expect(dead_code)] + pub color_manager: Rc, } // impl Drop for State { diff --git a/src/text.rs b/src/text.rs index c8b2ab0c..06b49a1c 100644 --- a/src/text.rs +++ b/src/text.rs @@ -1,5 +1,6 @@ use { crate::{ + cmm::cmm_transfer_function::TransferFunction, cpu_worker::{AsyncCpuWork, CpuJob, CpuWork, CpuWorker, PendingJob}, format::ARGB8888, gfx_api::{ @@ -14,7 +15,7 @@ use { }, }, rect::{Rect, Region}, - theme::{Color, TransferFunction}, + theme::Color, utils::{ clonecell::CloneCell, double_buffered::DoubleBuffered, on_drop_event::OnDropEvent, }, diff --git a/src/theme.rs b/src/theme.rs index 72e594b7..3a19fabc 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,14 +1,8 @@ use { - crate::utils::clonecell::CloneCell, + crate::{cmm::cmm_transfer_function::TransferFunction, utils::clonecell::CloneCell}, std::{cell::Cell, cmp::Ordering, ops::Mul, sync::Arc}, }; -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum TransferFunction { - Srgb, - Linear, -} - #[derive(Copy, Clone, Debug, PartialEq)] pub struct Color { r: f32, diff --git a/src/utils.rs b/src/utils.rs index 96d65756..ba49cba1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -18,6 +18,7 @@ pub mod double_click_state; pub mod errorfmt; pub mod event_listener; pub mod fdcloser; +pub mod free_list; pub mod geometric_decay; pub mod gfx_api_ext; pub mod hash_map_ext; @@ -37,6 +38,7 @@ pub mod opaque; pub mod opaque_cell; pub mod opt; pub mod option_ext; +pub mod ordered_float; pub mod oserror; pub mod page_size; pub mod pending_serial; diff --git a/src/utils/free_list.rs b/src/utils/free_list.rs new file mode 100644 index 00000000..129e58f6 --- /dev/null +++ b/src/utils/free_list.rs @@ -0,0 +1,93 @@ +#[cfg(test)] +mod tests; + +use { + crate::utils::ptr_ext::MutPtrExt, + std::{ + array, + cell::UnsafeCell, + fmt::{Debug, Formatter}, + marker::PhantomData, + }, +}; + +type Seg = usize; +const SEG_SIZE: usize = size_of::() * 8; + +pub struct FreeList { + levels: UnsafeCell<[Vec; N]>, + _phantom: PhantomData, +} + +impl Default for FreeList { + fn default() -> Self { + Self { + levels: UnsafeCell::new(array::from_fn(|_| Vec::new())), + _phantom: Default::default(), + } + } +} + +impl Debug for FreeList { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FreeList") + .field("levels", self.get()) + .finish() + } +} + +impl FreeList { + fn get(&self) -> &mut [Vec; N] { + unsafe { self.levels.get().deref_mut() } + } + + pub fn release(&self, n: T) + where + T: Into, + { + let mut ext = n.into() as usize; + let mut int; + let levels = self.get(); + assert!(ext / SEG_SIZE < levels[0].len()); + for level in self.get() { + int = ext % SEG_SIZE; + ext /= SEG_SIZE; + unsafe { + *level.get_unchecked_mut(ext) |= 1 << int; + } + } + } + + pub fn acquire(&self) -> T + where + u32: Into, + { + let mut ext = 'last: { + let level = &mut self.get()[N - 1]; + for (idx, &seg) in level.iter().enumerate() { + if seg != 0 { + break 'last idx; + } + } + level.len() + }; + for level in self.get().iter_mut().rev() { + if ext == level.len() { + level.push(!0); + } + let seg = unsafe { level.get_unchecked(ext) }; + ext = SEG_SIZE * ext + seg.trailing_zeros() as usize; + } + let id = ext as u32; + for level in self.get().iter_mut() { + let int = ext % SEG_SIZE; + ext /= SEG_SIZE; + let seg = unsafe { level.get_unchecked_mut(ext) }; + *seg &= !(1 << int); + if *seg != 0 { + break; + } + } + id.into() + } +} diff --git a/src/utils/free_list/tests.rs b/src/utils/free_list/tests.rs new file mode 100644 index 00000000..a8c4eae5 --- /dev/null +++ b/src/utils/free_list/tests.rs @@ -0,0 +1,40 @@ +use crate::utils::free_list::FreeList; + +#[test] +fn test() { + let list = FreeList::::default(); + for i in 0..4097 { + assert_eq!(list.acquire(), i); + } + list.release(100); + assert_eq!(list.acquire(), 100); + assert_eq!(list.acquire(), 4097); + for i in 1..22 { + list.release(i); + } + for i in 1..22 { + assert_eq!(list.acquire(), i); + } + assert_eq!(list.acquire(), 4098); + for i in 64..128 { + list.release(i); + } + for i in 64..128 { + assert_eq!(list.acquire(), i); + } + assert_eq!(list.acquire(), 4099); + for i in 0..4100 { + list.release(i); + } + for i in 0..4101 { + assert_eq!(list.acquire(), i); + } +} + +#[test] +#[should_panic] +fn release_out_of_bounds() { + let list = FreeList::::default(); + list.acquire(); + list.release(500); +} diff --git a/src/utils/ordered_float.rs b/src/utils/ordered_float.rs new file mode 100644 index 00000000..4a458a61 --- /dev/null +++ b/src/utils/ordered_float.rs @@ -0,0 +1,67 @@ +use std::{ + fmt::{Debug, Display, Formatter}, + hash::{Hash, Hasher}, + ops::{Add, Div, Mul, Sub}, +}; + +#[derive(Copy, Clone)] +#[repr(transparent)] +pub struct F64(pub f64); + +impl Eq for F64 {} + +impl PartialEq for F64 { + fn eq(&self, other: &Self) -> bool { + self.0.to_bits() == other.0.to_bits() + } +} + +impl Hash for F64 { + fn hash(&self, state: &mut H) { + self.0.to_bits().hash(state); + } +} + +impl Add for F64 { + type Output = Self; + + fn add(self, rhs: F64) -> Self::Output { + Self(self.0 + rhs.0) + } +} + +impl Sub for F64 { + type Output = Self; + + fn sub(self, rhs: F64) -> Self::Output { + Self(self.0 - rhs.0) + } +} + +impl Mul for F64 { + type Output = Self; + + fn mul(self, rhs: F64) -> Self::Output { + Self(self.0 * rhs.0) + } +} + +impl Div for F64 { + type Output = Self; + + fn div(self, rhs: F64) -> Self::Output { + Self(self.0 / rhs.0) + } +} + +impl Display for F64 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl Debug for F64 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Debug::fmt(&self.0, f) + } +}