1
0
Fork 0
forked from wry/wry

cmm: add color-management module

This commit is contained in:
Julian Orth 2025-03-01 13:54:12 +01:00
parent ed1955d3d1
commit 82085a3858
27 changed files with 1182 additions and 20 deletions

View file

@ -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},
},

8
src/cmm.rs Normal file
View file

@ -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;

View file

@ -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<ColorDescriptionId, 3>;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct ColorDescriptionId(u32);
impl From<u32> for ColorDescriptionId {
fn from(value: u32) -> Self {
Self(value)
}
}
impl From<ColorDescriptionId> 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<Xyz, Local>,
pub local_from_xyz: ColorMatrix<Local, Xyz>,
pub luminance: Luminance,
pub(super) shared: Rc<Shared>,
}
#[derive(Debug)]
pub struct ColorDescription {
pub id: ColorDescriptionId,
#[expect(dead_code)]
pub linear: Rc<LinearColorDescription>,
#[expect(dead_code)]
pub named_primaries: Option<NamedPrimaries>,
#[expect(dead_code)]
pub transfer_function: TransferFunction,
pub(super) shared: Rc<Shared>,
}
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);
}
}

72
src/cmm/cmm_luminance.rs Normal file
View file

@ -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<Xyz, Xyz> {
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],
])
}

204
src/cmm/cmm_manager.rs Normal file
View file

@ -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<LinearDescriptionKey, Weak<LinearColorDescription>>,
complete_descriptions: CopyHashMap<CompleteDescriptionKey, Weak<ColorDescription>>,
shared: Rc<Shared>,
srgb_srgb: Rc<ColorDescription>,
srgb_linear: Rc<ColorDescription>,
windows_scrgb: Rc<ColorDescription>,
}
#[derive(Debug, Default)]
pub(super) struct Shared {
pub(super) dead_linear: NumCell<usize>,
pub(super) dead_complete: NumCell<usize>,
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<NamedPrimaries>,
transfer_function: TransferFunction,
}
impl ColorManager {
pub fn new() -> Rc<Self> {
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<ColorDescription> {
&self.srgb_srgb
}
#[expect(dead_code)]
pub fn srgb_linear(&self) -> &Rc<ColorDescription> {
&self.srgb_linear
}
#[expect(dead_code)]
pub fn windows_scrgb(&self) -> &Rc<ColorDescription> {
&self.windows_scrgb
}
#[expect(dead_code)]
pub fn get_description(
self: &Rc<Self>,
named_primaries: Option<NamedPrimaries>,
primaries: Primaries,
luminance: Luminance,
transfer_function: TransferFunction,
) -> Rc<ColorDescription> {
get_description(
&self.shared,
&self.linear_descriptions,
&self.complete_descriptions,
&self.linear_ids,
named_primaries,
primaries,
luminance,
transfer_function,
)
}
}
fn get_description(
shared: &Rc<Shared>,
linear_descriptions: &CopyHashMap<LinearDescriptionKey, Weak<LinearColorDescription>>,
complete_descriptions: &CopyHashMap<CompleteDescriptionKey, Weak<ColorDescription>>,
linear_ids: &LinearColorDescriptionIds,
named_primaries: Option<NamedPrimaries>,
primaries: Primaries,
luminance: Luminance,
transfer_function: TransferFunction,
) -> Rc<ColorDescription> {
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
}

121
src/cmm/cmm_primaries.rs Normal file
View file

@ -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,
}
}
}

201
src/cmm/cmm_tests.rs Normal file
View file

@ -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],
],
)
}
}

View file

@ -0,0 +1,5 @@
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum TransferFunction {
Srgb,
Linear,
}

258
src/cmm/cmm_transform.rs Normal file
View file

@ -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<To = Local, From = Local>(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<T, U> Copy for ColorMatrix<T, U> {}
impl<T, U> Clone for ColorMatrix<T, U> {
fn clone(&self) -> Self {
*self
}
}
impl<T, U> PartialEq<Self> for ColorMatrix<T, U> {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<T, U> Eq for ColorMatrix<T, U> {}
impl<T, U> Hash for ColorMatrix<T, U> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl<T, U> Debug for ColorMatrix<T, U> {
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<T, U, V> Mul<ColorMatrix<U, T>> for ColorMatrix<V, U> {
type Output = ColorMatrix<V, T>;
fn mul(self, rhs: ColorMatrix<U, T>) -> 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<U, V> MulAssign<ColorMatrix<U, U>> for ColorMatrix<V, U> {
fn mul_assign(&mut self, rhs: ColorMatrix<U, U>) {
*self = *self * rhs;
}
}
impl<T, U> Mul<[f64; 3]> for ColorMatrix<T, U> {
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<T, U> Mul<Color> for ColorMatrix<T, U> {
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<T, U> ColorMatrix<T, U> {
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<Bradford, Xyz> {
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<Xyz, Bradford> {
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<Xyz, Xyz> {
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<Xyz, Local>, ColorMatrix<Local, Xyz>) {
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),
)
}
}

View file

@ -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);

View file

@ -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,

View file

@ -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,

View file

@ -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,

View file

@ -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},
},

View file

@ -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::{

View file

@ -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},

View file

@ -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>,
}

View file

@ -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;

View file

@ -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},
};

View file

@ -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,

View file

@ -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<bool>,
#[expect(dead_code)]
pub color_manager: Rc<ColorManager>,
}
// impl Drop for State {

View file

@ -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,
},

View file

@ -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,

View file

@ -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;

93
src/utils/free_list.rs Normal file
View file

@ -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::<Seg>() * 8;
pub struct FreeList<T, const N: usize> {
levels: UnsafeCell<[Vec<Seg>; N]>,
_phantom: PhantomData<T>,
}
impl<T, const N: usize> Default for FreeList<T, N> {
fn default() -> Self {
Self {
levels: UnsafeCell::new(array::from_fn(|_| Vec::new())),
_phantom: Default::default(),
}
}
}
impl<T, const N: usize> Debug for FreeList<T, N> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FreeList")
.field("levels", self.get())
.finish()
}
}
impl<T, const N: usize> FreeList<T, N> {
fn get(&self) -> &mut [Vec<Seg>; N] {
unsafe { self.levels.get().deref_mut() }
}
pub fn release(&self, n: T)
where
T: Into<u32>,
{
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<T>,
{
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()
}
}

View file

@ -0,0 +1,40 @@
use crate::utils::free_list::FreeList;
#[test]
fn test() {
let list = FreeList::<u32, 3>::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::<u32, 3>::default();
list.acquire();
list.release(500);
}

View file

@ -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<H: Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state);
}
}
impl Add<F64> for F64 {
type Output = Self;
fn add(self, rhs: F64) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Sub<F64> for F64 {
type Output = Self;
fn sub(self, rhs: F64) -> Self::Output {
Self(self.0 - rhs.0)
}
}
impl Mul<F64> for F64 {
type Output = Self;
fn mul(self, rhs: F64) -> Self::Output {
Self(self.0 * rhs.0)
}
}
impl Div<F64> 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)
}
}