1
0
Fork 0
forked from wry/wry

Merge pull request #405 from mahkoh/jorth/cm-5

metal: allow configuring color space and transfer function
This commit is contained in:
mahkoh 2025-03-14 20:38:25 +01:00 committed by GitHub
commit ec862648c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 1687 additions and 219 deletions

View file

@ -25,7 +25,8 @@ use {
theme::{Color, colors::Colorable, sized::Resizable},
timer::Timer,
video::{
Connector, DrmDevice, Format, GfxApi, Mode, TearingMode, Transform, VrrMode,
ColorSpace, Connector, DrmDevice, Format, GfxApi, Mode, TearingMode, TransferFunction,
Transform, VrrMode,
connector_type::{CON_UNKNOWN, ConnectorType},
},
xwayland::XScalingMode,
@ -781,6 +782,19 @@ impl Client {
self.send(&ClientMessage::ConnectorSetFormat { connector, format });
}
pub fn connector_set_colors(
&self,
connector: Connector,
color_space: ColorSpace,
transfer_function: TransferFunction,
) {
self.send(&ClientMessage::ConnectorSetColors {
connector,
color_space,
transfer_function,
});
}
pub fn connector_get_scale(&self, connector: Connector) -> f64 {
let res = self.send_with_response(&ClientMessage::ConnectorGetScale { connector });
get_response!(res, 1.0, ConnectorGetScale { scale });

View file

@ -11,8 +11,8 @@ use {
theme::{Color, colors::Colorable, sized::Resizable},
timer::Timer,
video::{
Connector, DrmDevice, Format, GfxApi, TearingMode, Transform, VrrMode,
connector_type::ConnectorType,
ColorSpace, Connector, DrmDevice, Format, GfxApi, TearingMode, TransferFunction,
Transform, VrrMode, connector_type::ConnectorType,
},
xwayland::XScalingMode,
},
@ -533,6 +533,11 @@ pub enum ClientMessage<'a> {
SetColorManagementEnabled {
enabled: bool,
},
ConnectorSetColors {
connector: Connector,
color_space: ColorSpace,
transfer_function: TransferFunction,
},
}
#[derive(Serialize, Deserialize, Debug)]

View file

@ -272,6 +272,19 @@ impl Connector {
pub fn set_format(self, format: Format) {
get!().connector_set_format(self, format);
}
/// Sets the color space and transfer function of the connector.
///
/// By default, the default values are used which usually means sRGB color space with
/// sRGB transfer function.
///
/// If the output supports it, HDR10 can be enabled by setting the color space to
/// BT.2020 and the transfer function to PQ.
///
/// Note that some displays might ignore incompatible settings.
pub fn set_colors(self, color_space: ColorSpace, transfer_function: TransferFunction) {
get!().connector_set_colors(self, color_space, transfer_function);
}
}
/// Returns all available DRM devices.
@ -662,3 +675,25 @@ impl Format {
pub const ABGR16161616F: Self = Self(26);
pub const XBGR16161616F: Self = Self(27);
}
/// A color space.
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct ColorSpace(pub u32);
impl ColorSpace {
/// The default color space (usually sRGB).
pub const DEFAULT: Self = Self(0);
/// The BT.2020 color space.
pub const BT2020: Self = Self(1);
}
/// A transfer function.
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct TransferFunction(pub u32);
impl TransferFunction {
/// The default transfer function (usually sRGB).
pub const DEFAULT: Self = Self(0);
/// The PQ transfer function.
pub const PQ: Self = Self(1);
}

View file

@ -11,6 +11,7 @@
observable. This should have no impact on performance in the common case.
- Implement color-management-v1.
- Implement cursor-shape-v1 version 2.
- Outputs can now optionally use the BT.2020/PQ color space.
# 1.9.1 (2025-02-13)

View file

@ -1,6 +1,7 @@
use {
crate::{
async_engine::SpawnedFuture,
cmm::cmm_primaries::Primaries,
drm_feedback::DrmFeedback,
fixed::Fixed,
format::Format,
@ -17,9 +18,14 @@ use {
},
},
libinput::consts::DeviceCapability,
video::drm::{ConnectorType, DrmConnector, DrmError, DrmVersion},
video::drm::{
ConnectorType, DRM_MODE_COLORIMETRY_BT2020_RGB, DRM_MODE_COLORIMETRY_DEFAULT,
DrmConnector, DrmError, DrmVersion, HDMI_EOTF_SMPTE_ST2084,
HDMI_EOTF_TRADITIONAL_GAMMA_SDR,
},
},
jay_config::{input::SwitchEvent, video::GfxApi},
linearize::Linearize,
std::{
any::Any,
error::Error,
@ -83,6 +89,12 @@ pub struct MonitorInfo {
pub height_mm: i32,
pub non_desktop: bool,
pub vrr_capable: bool,
pub transfer_functions: Vec<BackendTransferFunction>,
pub transfer_function: BackendTransferFunction,
pub color_spaces: Vec<BackendColorSpace>,
pub color_space: BackendColorSpace,
pub primaries: Primaries,
pub luminance: Option<BackendLuminance>,
}
#[derive(Copy, Clone, Debug)]
@ -129,6 +141,10 @@ pub trait Connector {
fn set_fb_format(&self, format: &'static Format) {
let _ = format;
}
fn set_colors(&self, bcs: BackendColorSpace, btf: BackendTransferFunction) {
let _ = bcs;
let _ = btf;
}
}
#[derive(Debug)]
@ -142,6 +158,7 @@ pub enum ConnectorEvent {
Available,
VrrChanged(bool),
FormatsChanged(Rc<Vec<&'static Format>>, &'static Format),
ColorsChanged(BackendColorSpace, BackendTransferFunction),
}
pub trait HardwareCursorUpdate {
@ -477,3 +494,56 @@ pub trait BackendDrmLease {
pub trait BackendDrmLessee {
fn created(&self, lease: Rc<dyn BackendDrmLease>);
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Linearize)]
pub enum BackendTransferFunction {
#[default]
Default,
Pq,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default, Linearize)]
pub enum BackendColorSpace {
#[default]
Default,
Bt2020,
}
#[derive(Copy, Clone, Debug)]
pub struct BackendLuminance {
pub min: f64,
pub max: f64,
pub max_fall: f64,
}
impl BackendTransferFunction {
pub fn to_drm(self) -> u8 {
match self {
BackendTransferFunction::Default => HDMI_EOTF_TRADITIONAL_GAMMA_SDR,
BackendTransferFunction::Pq => HDMI_EOTF_SMPTE_ST2084,
}
}
pub const fn name(self) -> &'static str {
match self {
BackendTransferFunction::Default => "default",
BackendTransferFunction::Pq => "pq",
}
}
}
impl BackendColorSpace {
pub fn to_drm(self) -> u64 {
match self {
BackendColorSpace::Default => DRM_MODE_COLORIMETRY_DEFAULT,
BackendColorSpace::Bt2020 => DRM_MODE_COLORIMETRY_BT2020_RGB,
}
}
pub const fn name(self) -> &'static str {
match self {
BackendColorSpace::Default => "default",
BackendColorSpace::Bt2020 => "bt2020",
}
}
}

View file

@ -7,6 +7,7 @@ use {
MetalConnector, MetalCrtc, MetalHardwareCursorChange, MetalPlane, RenderBuffer,
},
},
cmm::cmm_description::ColorDescription,
gfx_api::{
AcquireSync, BufferResv, GfxApiOpt, GfxRenderPass, GfxTexture, ReleaseSync, SyncFile,
create_render_pass,
@ -176,10 +177,13 @@ impl MetalConnector {
};
let buffer = &buffers[self.next_buffer.get() % buffers.len()];
let cd = node.global.color_description.get();
let linear_cd = node.global.linear_color_description.get();
if self.has_damage.get() > 0 || self.cursor_damage.get() {
node.schedule.commit_cursor();
}
self.latch_cursor(&node)?;
self.latch_cursor(&node, &cd)?;
let cursor_programming = self.compute_cursor_programming();
let latched = self.latch(&node, buffer);
node.latched(self.try_async_flip());
@ -191,11 +195,11 @@ impl MetalConnector {
let mut present_fb = None;
let mut direct_scanout_id = None;
if let Some(latched) = &latched {
let fb = self.prepare_present_fb(buffer, &plane, latched, true)?;
let fb = self.prepare_present_fb(&cd, &linear_cd, buffer, &plane, latched, true)?;
direct_scanout_id = fb.direct_scanout_data.as_ref().map(|d| d.dma_buf_id);
present_fb = Some(fb);
}
self.perform_screencopies(&present_fb, &node);
self.perform_screencopies(&present_fb, &node, &cd);
if let Some(sync_file) = self.cursor_sync_file.take() {
if let Err(e) = self.state.ring.readable(&sync_file).await {
log::error!(
@ -214,8 +218,14 @@ impl MetalConnector {
);
if res.is_err() {
if let Some(dsd_id) = direct_scanout_id {
let fb =
self.prepare_present_fb(buffer, &plane, latched.as_ref().unwrap(), false)?;
let fb = self.prepare_present_fb(
&cd,
&linear_cd,
buffer,
&plane,
latched.as_ref().unwrap(),
false,
)?;
present_fb = Some(fb);
self.await_present_fb(present_fb.as_mut()).await;
res = self.program_connector(
@ -432,7 +442,11 @@ impl MetalConnector {
res.map_err(MetalError::Commit)
}
fn latch_cursor(&self, node: &Rc<OutputNode>) -> Result<(), MetalError> {
fn latch_cursor(
&self,
node: &Rc<OutputNode>,
cd: &Rc<ColorDescription>,
) -> Result<(), MetalError> {
if !self.cursor_damage.take() {
return Ok(());
}
@ -451,9 +465,7 @@ impl MetalConnector {
};
self.state.present_hardware_cursor(node, &mut c);
if c.cursor_swap_buffer {
c.sync_file = c
.cursor_buffer
.copy_to_dev(&self.state.color_manager, c.sync_file)?;
c.sync_file = c.cursor_buffer.copy_to_dev(cd, c.sync_file)?;
}
self.cursor_swap_buffer.set(c.cursor_swap_buffer);
if c.sync_file.is_some() {
@ -544,6 +556,7 @@ impl MetalConnector {
&self,
pass: &GfxRenderPass,
plane: &Rc<MetalPlane>,
cd: &Rc<ColorDescription>,
) -> Option<DirectScanoutData> {
let ct = 'ct: {
let mut ops = pass.ops.iter().rev();
@ -560,8 +573,8 @@ impl MetalConnector {
}
return None;
};
if ct.cd.id != self.state.color_manager.srgb_srgb().id {
// Direct scanout requires identical color descriptions.
if !ct.cd.embeds_into(cd) {
// Direct scanout requires embeddable color descriptions.
return None;
}
if ct.alpha.is_some() {
@ -717,6 +730,8 @@ impl MetalConnector {
fn prepare_present_fb(
&self,
cd: &Rc<ColorDescription>,
linear_cd: &Rc<ColorDescription>,
buffer: &RenderBuffer,
plane: &Rc<MetalPlane>,
latched: &Latched,
@ -733,7 +748,7 @@ impl MetalConnector {
&& self.dev.is_render_device();
let mut direct_scanout_data = None;
if try_direct_scanout {
direct_scanout_data = self.prepare_direct_scanout(&latched.pass, plane);
direct_scanout_data = self.prepare_direct_scanout(&latched.pass, plane, cd);
}
let direct_scanout_active = direct_scanout_data.is_some();
if self.direct_scanout_active.replace(direct_scanout_active) != direct_scanout_active {
@ -753,14 +768,14 @@ impl MetalConnector {
.perform_render_pass(
AcquireSync::Unnecessary,
ReleaseSync::Explicit,
self.state.color_manager.srgb_srgb(),
cd,
&latched.pass,
&latched.damage,
buffer.blend_buffer.as_ref(),
self.state.color_manager.srgb_linear(),
linear_cd,
)
.map_err(MetalError::RenderFrame)?;
sync_file = buffer.copy_to_dev(&self.state.color_manager, sf)?;
sync_file = buffer.copy_to_dev(cd, sf)?;
fb = buffer.drm.clone();
tex = buffer.render_tex.clone();
}
@ -783,7 +798,12 @@ impl MetalConnector {
})
}
fn perform_screencopies(&self, new_fb: &Option<PresentFb>, output: &OutputNode) {
fn perform_screencopies(
&self,
new_fb: &Option<PresentFb>,
output: &OutputNode,
cd: &Rc<ColorDescription>,
) {
let active_fb;
let fb = match &new_fb {
Some(f) => f,
@ -800,7 +820,7 @@ impl MetalConnector {
None => {
output.perform_screencopies(
&fb.tex,
self.state.color_manager.srgb_srgb(),
cd,
None,
&AcquireSync::Unnecessary,
ReleaseSync::None,
@ -813,7 +833,7 @@ impl MetalConnector {
Some(dsd) => {
output.perform_screencopies(
&dsd.tex,
self.state.color_manager.srgb_srgb(),
cd,
dsd.resv.as_ref(),
&dsd.acquire_sync,
dsd.release_sync,

View file

@ -3,9 +3,10 @@ use {
allocator::BufferObject,
async_engine::{Phase, SpawnedFuture},
backend::{
BackendDrmDevice, BackendDrmLease, BackendDrmLessee, BackendEvent, Connector,
ConnectorEvent, ConnectorId, ConnectorKernelId, DrmDeviceId, HardwareCursor,
HardwareCursorUpdate, Mode, MonitorInfo,
BackendColorSpace, BackendDrmDevice, BackendDrmLease, BackendDrmLessee, BackendEvent,
BackendLuminance, BackendTransferFunction, Connector, ConnectorEvent, ConnectorId,
ConnectorKernelId, DrmDeviceId, HardwareCursor, HardwareCursorUpdate, Mode,
MonitorInfo,
},
backends::metal::{
MetalBackend, MetalError,
@ -14,7 +15,7 @@ use {
POST_COMMIT_MARGIN_DELTA, PresentFb,
},
},
cmm::cmm_manager::ColorManager,
cmm::{cmm_description::ColorDescription, cmm_primaries::Primaries},
drm_feedback::DrmFeedback,
edid::{CtaDataBlock, Descriptor, EdidExtension},
format::{ARGB8888, Format, XRGB8888},
@ -33,7 +34,8 @@ use {
utils::{
asyncevent::AsyncEvent, bitflags::BitflagsExt, cell_ext::CellExt, clonecell::CloneCell,
copyhashmap::CopyHashMap, errorfmt::ErrorFmt, geometric_decay::GeometricDecay,
numcell::NumCell, on_change::OnChange, opaque_cell::OpaqueCell, oserror::OsError,
numcell::NumCell, on_change::OnChange, opaque_cell::OpaqueCell, ordered_float::F64,
oserror::OsError,
},
video::{
INVALID_MODIFIER, Modifier,
@ -43,7 +45,7 @@ use {
DRM_MODE_ATOMIC_ALLOW_MODESET, DrmBlob, DrmConnector, DrmCrtc, DrmEncoder,
DrmError, DrmEvent, DrmFramebuffer, DrmLease, DrmMaster, DrmModeInfo, DrmObject,
DrmPlane, DrmProperty, DrmPropertyDefinition, DrmPropertyType, DrmVersion,
PropBlob, drm_mode_modeinfo,
PropBlob, drm_mode_modeinfo, hdr_output_metadata,
},
gbm::{GBM_BO_USE_LINEAR, GBM_BO_USE_RENDERING, GBM_BO_USE_SCANOUT, GbmBo, GbmDevice},
},
@ -64,6 +66,7 @@ use {
ops::DerefMut,
rc::Rc,
},
thiserror::Error,
uapi::{
OwnedFd,
c::{self, dev_t},
@ -322,6 +325,8 @@ pub struct PersistentDisplayData {
pub mode: RefCell<Option<DrmModeInfo>>,
pub vrr_requested: Cell<bool>,
pub format: Cell<&'static Format>,
pub eotf: Cell<BackendTransferFunction>,
pub color_space: Cell<BackendColorSpace>,
}
#[derive(Debug)]
@ -346,6 +351,15 @@ pub struct ConnectorDisplayData {
pub mm_width: u32,
pub mm_height: u32,
pub _subpixel: u32,
pub supports_bt2020: bool,
pub supports_pq: bool,
pub primaries: Primaries,
pub luminance: Option<BackendLuminance>,
pub colorspace: Option<MutableProperty<u64>>,
pub hdr_metadata: Option<MutableProperty<DrmBlob>>,
pub hdr_metadata_blob: Option<PropBlob>,
}
impl ConnectorDisplayData {
@ -653,6 +667,21 @@ impl MetalConnector {
pub fn send_event(&self, event: ConnectorEvent) {
let state = self.frontend_state.get();
macro_rules! desktop_event {
($name:expr) => {
match state {
FrontState::Connected { non_desktop: false } => {
self.on_change.send_event(event);
}
FrontState::Connected { non_desktop: true }
| FrontState::Removed
| FrontState::Disconnected
| FrontState::Unavailable => {
log::error!("Tried to send {} event in invalid state: {state:?}", $name);
}
}
};
}
match &event {
ConnectorEvent::Connected(ty) => match state {
FrontState::Disconnected => {
@ -665,21 +694,12 @@ impl MetalConnector {
log::error!("Tried to send connected event in invalid state: {state:?}");
}
},
ConnectorEvent::HardwareCursor(_) | ConnectorEvent::ModeChanged(_) => match state {
FrontState::Connected { non_desktop: false } => {
self.on_change.send_event(event);
}
FrontState::Connected { non_desktop: true }
| FrontState::Removed
| FrontState::Disconnected
| FrontState::Unavailable => {
let name = match &event {
ConnectorEvent::HardwareCursor(_) => "hardware cursor",
_ => "mode change",
};
log::error!("Tried to send {name} event in invalid state: {state:?}");
}
},
ConnectorEvent::HardwareCursor(_) => {
desktop_event!("hardware cursor");
}
ConnectorEvent::ModeChanged(_) => {
desktop_event!("mode change");
}
ConnectorEvent::Disconnected => match state {
FrontState::Connected { .. } | FrontState::Unavailable => {
self.on_change.send_event(event);
@ -720,28 +740,15 @@ impl MetalConnector {
log::error!("Tried to send available event in invalid state: {state:?}");
}
},
ConnectorEvent::VrrChanged(_) => match state {
FrontState::Connected { non_desktop: false } => {
self.on_change.send_event(event);
}
FrontState::Connected { non_desktop: true }
| FrontState::Removed
| FrontState::Disconnected
| FrontState::Unavailable => {
log::error!("Tried to send vrr-changed event in invalid state: {state:?}");
}
},
ConnectorEvent::FormatsChanged(_, _) => match state {
FrontState::Connected { non_desktop: false } => {
self.on_change.send_event(event);
}
FrontState::Connected { non_desktop: true }
| FrontState::Removed
| FrontState::Disconnected
| FrontState::Unavailable => {
log::error!("Tried to send format-changed event in invalid state: {state:?}");
}
},
ConnectorEvent::VrrChanged(_) => {
desktop_event!("vrr-changed");
}
ConnectorEvent::FormatsChanged(_, _) => {
desktop_event!("formats-changed");
}
ConnectorEvent::ColorsChanged(_, _) => {
desktop_event!("colors-changed");
}
}
}
@ -765,6 +772,55 @@ impl MetalConnector {
}
}
}
fn change_property(
&self,
name: &str,
needs_change: impl FnOnce(&ConnectorDisplayData) -> bool,
supports_change: impl FnOnce(&ConnectorDisplayData) -> bool,
change: impl FnOnce(&ConnectorDisplayData),
changed: impl FnOnce(),
reset: impl FnOnce(&ConnectorDisplayData),
) {
match self.frontend_state.get() {
FrontState::Connected { non_desktop: false } => {}
FrontState::Connected { non_desktop: true }
| FrontState::Removed
| FrontState::Disconnected
| FrontState::Unavailable => return,
}
let dd = self.display.borrow();
if !needs_change(&dd) {
return;
}
if !supports_change(&dd) {
return;
}
if dd.connection != ConnectorStatus::Connected {
log::warn!("Cannot change {name} of connector that is not connected");
return;
}
let Some(dev) = self.backend.device_holder.drm_devices.get(&self.dev.devnum) else {
log::warn!("Cannot change {name} because underlying device does not exist?");
return;
};
change(&dd);
drop(dd);
let Err(e) = self.backend.handle_drm_change_(&dev, true) else {
changed();
return;
};
log::warn!("Could not change {name}: {}", ErrorFmt(&e));
reset(&self.display.borrow());
if let MetalError::Modeset(DrmError::Atomic(OsError(c::EACCES))) = e {
log::warn!("Failed due to access denied. Resetting in memory only.");
return;
}
log::warn!("Trying to re-initialize the drm device");
if let Err(e) = self.backend.handle_drm_change_(&dev, true) {
log::warn!("Could not restore the previous {name}: {}", ErrorFmt(e));
};
}
}
impl Connector for MetalConnector {
@ -942,6 +998,47 @@ impl Connector for MetalConnector {
}
}
}
fn set_colors(&self, bcs: BackendColorSpace, btf: BackendTransferFunction) {
let prev_bcs = Cell::new(bcs);
let prev_btf = Cell::new(btf);
self.change_property(
"colors",
|dd| {
prev_bcs.set(dd.persistent.color_space.get());
prev_btf.set(dd.persistent.eotf.get());
prev_bcs.get() != bcs || prev_btf.get() != btf
},
|dd| {
let cs = match bcs {
BackendColorSpace::Default => true,
BackendColorSpace::Bt2020 => dd.supports_bt2020,
};
if !cs {
log::warn!("Display does not support color space {:?}", bcs);
}
let tf = match btf {
BackendTransferFunction::Default => true,
BackendTransferFunction::Pq => dd.supports_pq,
};
if !tf {
log::warn!("Display does not support transfer function {:?}", btf);
}
cs && tf
},
|dd| {
dd.persistent.color_space.set(bcs);
dd.persistent.eotf.set(btf);
},
|| {
self.send_event(ConnectorEvent::ColorsChanged(bcs, btf));
},
|dd| {
dd.persistent.color_space.set(prev_bcs.get());
dd.persistent.eotf.set(prev_btf.get());
},
);
}
}
pub struct MetalCrtc {
@ -1203,6 +1300,10 @@ fn create_connector_display_data(
ty: ConnectorType::from_drm(info.connector_type),
idx: info.connector_type_id,
};
let mut supports_bt2020 = false;
let mut supports_pq = false;
let mut luminance = None;
let mut primaries = Primaries::SRGB;
'fetch_edid: {
if connection != ConnectorStatus::Connected {
break 'fetch_edid;
@ -1286,6 +1387,40 @@ fn create_connector_display_data(
if min_vrr_hz > 0 {
vrr_refresh_max_nsec = 1_000_000_000 / min_vrr_hz;
}
let cc = &edid.base_block.chromaticity_coordinates;
let map = |c: u16| F64(c as f64 / 1024.0);
primaries = Primaries {
r: (map(cc.red_x), map(cc.red_y)),
g: (map(cc.green_x), map(cc.green_y)),
b: (map(cc.blue_x), map(cc.blue_y)),
wp: (map(cc.white_x), map(cc.white_y)),
};
for ext in &edid.extension_blocks {
if let EdidExtension::CtaV3(cta) = ext {
for data_block in &cta.data_blocks {
match data_block {
CtaDataBlock::Colorimetry(c) => {
if c.bt2020_rgb {
supports_bt2020 = true;
}
}
CtaDataBlock::StaticHdrMetadata(h) => {
if h.smpte_st_2084 {
supports_pq = true;
}
if let Some(max) = h.max_luminance {
luminance = Some(BackendLuminance {
min: h.min_luminance.unwrap_or(0.0),
max,
max_fall: h.max_luminance.unwrap_or(max),
});
}
}
_ => {}
}
}
}
}
}
let output_id = Rc::new(OutputId::new(
connector_id.to_string(),
@ -1295,7 +1430,9 @@ fn create_connector_display_data(
));
let desired_state = match dev.backend.persistent_display_data.get(&output_id) {
Some(ds) => {
log::info!("Reusing desired state for {:?}", output_id);
if connection != ConnectorStatus::Disconnected {
log::info!("Reusing desired state for {:?}", output_id);
}
ds
}
None => {
@ -1303,6 +1440,8 @@ fn create_connector_display_data(
mode: RefCell::new(info.modes.first().cloned()),
vrr_requested: Default::default(),
format: Cell::new(XRGB8888),
eotf: Default::default(),
color_space: Default::default(),
});
dev.backend
.persistent_display_data
@ -1331,12 +1470,30 @@ fn create_connector_display_data(
};
let mode = mode_opt.clone();
drop(mode_opt);
{
let viable = match desired_state.eotf.get() {
BackendTransferFunction::Default => true,
BackendTransferFunction::Pq => supports_pq,
};
if !viable {
log::warn!("Discarding previously desired transfer function");
desired_state.eotf.set(BackendTransferFunction::Default);
}
}
{
let viable = match desired_state.color_space.get() {
BackendColorSpace::Default => true,
BackendColorSpace::Bt2020 => supports_bt2020,
};
if !viable {
log::warn!("Discarding previously desired color space");
desired_state.color_space.set(BackendColorSpace::Default);
}
}
let default_properties = create_default_properties(
&props,
&[
("Broadcast RGB", DefaultValue::Enum("Automatic")),
("Colorspace", DefaultValue::Enum("Default")),
("HDR_OUTPUT_METADATA", DefaultValue::Fixed(0)),
("HDR_SOURCE_METADATA", DefaultValue::Fixed(0)),
("Output format", DefaultValue::Enum("Default")),
("WRITEBACK_FB_ID", DefaultValue::Fixed(0)),
@ -1363,8 +1520,18 @@ fn create_connector_display_data(
mm_width: info.mm_width,
mm_height: info.mm_height,
_subpixel: info.subpixel,
supports_bt2020,
supports_pq,
primaries,
luminance,
connector_id,
output_id,
colorspace: props.get("Colorspace").ok(),
hdr_metadata: props
.get("HDR_OUTPUT_METADATA")
.ok()
.map(|v| v.map(|v| DrmBlob(v as _))),
hdr_metadata_blob: None,
})
}
@ -1802,6 +1969,14 @@ impl MetalBackend {
modes.push(mode);
}
}
let mut transfer_functions = vec![];
if dd.supports_pq {
transfer_functions.push(BackendTransferFunction::Pq);
}
let mut color_spaces = vec![];
if dd.supports_bt2020 {
color_spaces.push(BackendColorSpace::Bt2020);
}
connector.send_event(ConnectorEvent::Connected(MonitorInfo {
modes,
output_id: dd.output_id.clone(),
@ -1810,6 +1985,12 @@ impl MetalBackend {
height_mm: dd.mm_height as _,
non_desktop: dd.non_desktop_effective,
vrr_capable: dd.vrr_capable,
transfer_functions,
transfer_function: dd.persistent.eotf.get(),
color_spaces,
color_space: dd.persistent.color_space.get(),
primaries: dd.primaries,
luminance: dd.luminance,
}));
connector.send_hardware_cursor();
connector.send_vrr_enabled();
@ -1971,9 +2152,16 @@ impl MetalBackend {
for c in dev.connectors.lock().values() {
let dd = &mut *c.display.borrow_mut();
collect_untyped_properties(master, c.id, &mut dd.untyped_properties)?;
let props = &dd.untyped_properties;
dd.crtc_id
.value
.set(DrmCrtc(get(&dd.untyped_properties, dd.crtc_id.id)? as _));
.set(DrmCrtc(get(props, dd.crtc_id.id)? as _));
if let Some(meta) = &dd.hdr_metadata {
meta.value.set(DrmBlob(get(props, meta.id)? as _));
}
if let Some(cs) = &dd.colorspace {
cs.value.set(get(props, cs.id)?);
}
}
for c in dev.dev.crtcs.values() {
let props = &mut *c.untyped_properties.borrow_mut();
@ -2229,9 +2417,21 @@ impl MetalBackend {
connector.version.fetch_add(1);
let dd = connector.display.borrow_mut();
dd.crtc_id.value.set(DrmCrtc::NONE);
if let Some(cs) = &dd.colorspace {
cs.value.set(0);
}
if let Some(hdr) = &dd.hdr_metadata {
hdr.value.set(DrmBlob(0));
}
changes.change_object(connector.id, |c| {
c.change(dd.crtc_id.id, 0);
})
if let Some(cs) = &dd.colorspace {
c.change(cs.id, 0);
}
if let Some(hdr) = &dd.hdr_metadata {
c.change(hdr.id, 0);
}
});
}
for crtc in dev.dev.crtcs.values() {
if preserve.crtcs.contains(&crtc.id) {
@ -2310,6 +2510,16 @@ impl MetalBackend {
fail!(c.id);
}
}
if let Some(m) = &dd.colorspace {
if m.value.get() != dd.persistent.color_space.get().to_drm() {
log::debug!("Connector has wrong colorspace");
fail!(c.id);
}
}
if let Some(diff) = self.compare_hdr_metadata(&dev.dev, &dd) {
log::debug!("{}", diff);
fail!(c.id);
}
}
}
for c in remove_connectors {
@ -2509,6 +2719,39 @@ impl MetalBackend {
Ok(())
}
fn compare_hdr_metadata(
&self,
dev: &MetalDrmDevice,
dd: &ConnectorDisplayData,
) -> Option<HdrMetadataDiff> {
let Some(m) = &dd.hdr_metadata else {
return None;
};
match dd.persistent.eotf.get() {
BackendTransferFunction::Default => {
if m.value.get() != DrmBlob::NONE {
return Some(HdrMetadataDiff::Undesired);
}
}
eotf => {
if m.value.get() == DrmBlob::NONE {
return Some(HdrMetadataDiff::No);
}
let current_metadata =
match dev.master.getblob::<hdr_output_metadata>(m.value.get()) {
Ok(m) => m,
_ => {
return Some(HdrMetadataDiff::CouldNotRetrieve);
}
};
if current_metadata != hdr_output_metadata::from_eotf(eotf.to_drm()) {
return Some(HdrMetadataDiff::Incompatible);
}
}
}
None
}
fn can_use_current_drm_mode(&self, dev: &Rc<MetalDrmDeviceData>) -> bool {
let mut used_crtcs = AHashSet::new();
let mut vrr_crtcs = AHashSet::new();
@ -2532,6 +2775,16 @@ impl MetalBackend {
if dd.should_enable_vrr() {
vrr_crtcs.insert(crtc_id);
}
if let Some(m) = &dd.colorspace {
if m.value.get() != dd.persistent.color_space.get().to_drm() {
log::debug!("Connector has wrong colorspace");
return false;
}
}
if let Some(diff) = self.compare_hdr_metadata(&dev.dev, &dd) {
log::debug!("{}", diff);
return false;
}
let crtc = dev.dev.crtcs.get(&crtc_id).unwrap();
connector.crtc.set(Some(crtc.clone()));
connector.version.fetch_add(1);
@ -2804,8 +3057,8 @@ impl MetalBackend {
connector: &Rc<MetalConnector>,
changes: &mut Change,
) -> Result<(), MetalError> {
let dd = connector.display.borrow_mut();
if should_ignore(connector, &dd) {
let dd = &mut *connector.display.borrow_mut();
if should_ignore(connector, dd) {
return Ok(());
}
let crtc = 'crtc: {
@ -2820,9 +3073,23 @@ impl MetalBackend {
Some(m) => m,
_ => return Err(MetalError::NoModeForConnector),
};
let hdr_blob = match dd.persistent.eotf.get() {
BackendTransferFunction::Default => None,
eotf => {
let m = hdr_output_metadata::from_eotf(eotf.to_drm());
Some(connector.master.create_blob(&m)?)
}
};
let hdr_blob_id = hdr_blob.as_ref().map(|b| b.id()).unwrap_or_default();
let mode_blob = mode.create_blob(&connector.master)?;
changes.change_object(connector.id, |c| {
c.change(dd.crtc_id.id, crtc.id.0 as _);
if let Some(meta) = &dd.hdr_metadata {
c.change(meta.id, hdr_blob_id.0 as _);
}
if let Some(cs) = &dd.colorspace {
c.change(cs.id, dd.persistent.color_space.get().to_drm());
}
});
changes.change_object(crtc.id, |c| {
c.change(crtc.active.id, 1);
@ -2832,6 +3099,13 @@ impl MetalBackend {
connector.crtc.set(Some(crtc.clone()));
connector.version.fetch_add(1);
dd.crtc_id.value.set(crtc.id);
dd.hdr_metadata_blob = hdr_blob;
if let Some(meta) = &dd.hdr_metadata {
meta.value.set(hdr_blob_id);
}
if let Some(cs) = &dd.colorspace {
cs.value.set(dd.persistent.color_space.get().to_drm());
}
crtc.connector.set(Some(connector.clone()));
crtc.active.value.set(true);
crtc.mode_id.value.set(mode_blob.id());
@ -3041,7 +3315,7 @@ impl RenderBuffer {
pub fn copy_to_dev(
&self,
cm: &ColorManager,
cd: &Rc<ColorDescription>,
sync_file: Option<SyncFile>,
) -> Result<Option<SyncFile>, MetalError> {
let Some(tex) = &self.dev_tex else {
@ -3051,9 +3325,9 @@ impl RenderBuffer {
.copy_texture(
AcquireSync::Unnecessary,
ReleaseSync::Explicit,
cm.srgb_srgb(),
cd,
tex,
cm.srgb_srgb(),
cd,
None,
AcquireSync::from_sync_file(sync_file),
ReleaseSync::None,
@ -3091,3 +3365,15 @@ fn should_ignore(connector: &MetalConnector, dd: &ConnectorDisplayData) -> bool
|| dd.connection != ConnectorStatus::Connected
|| dd.non_desktop_effective
}
#[derive(Error, Debug)]
enum HdrMetadataDiff {
#[error("Connector has undesired HDR metadata")]
Undesired,
#[error("Connector has no HDR metadata")]
No,
#[error("Could not retrieve current HDR metadata of connector")]
CouldNotRetrieve,
#[error("Connector has incompatible HDR metadata")]
Incompatible,
}

View file

@ -3,11 +3,12 @@ use {
allocator::BufferObject,
async_engine::{Phase, SpawnedFuture},
backend::{
AXIS_120, AxisSource, Backend, BackendDrmDevice, BackendEvent, Connector,
ConnectorEvent, ConnectorId, ConnectorKernelId, DrmDeviceId, DrmEvent, InputDevice,
InputDeviceAccelProfile, InputDeviceCapability, InputDeviceId, InputEvent, KeyState,
Mode, MonitorInfo, ScrollAxis, TransformMatrix,
AXIS_120, AxisSource, Backend, BackendColorSpace, BackendDrmDevice, BackendEvent,
BackendTransferFunction, Connector, ConnectorEvent, ConnectorId, ConnectorKernelId,
DrmDeviceId, DrmEvent, InputDevice, InputDeviceAccelProfile, InputDeviceCapability,
InputDeviceId, InputEvent, KeyState, Mode, MonitorInfo, ScrollAxis, TransformMatrix,
},
cmm::cmm_primaries::Primaries,
fixed::Fixed,
format::XRGB8888,
gfx_api::{AcquireSync, GfxContext, GfxError, GfxFramebuffer, GfxTexture, ReleaseSync},
@ -581,6 +582,12 @@ impl XBackend {
height_mm: output.height.get(),
non_desktop: false,
vrr_capable: false,
transfer_functions: vec![],
transfer_function: BackendTransferFunction::Default,
color_spaces: vec![],
color_space: BackendColorSpace::Default,
primaries: Primaries::SRGB,
luminance: None,
}));
output.changed();
self.present(output).await;

View file

@ -1,5 +1,6 @@
use {
crate::{
backend::{BackendColorSpace, BackendTransferFunction},
cli::GlobalArgs,
format::{Format, XRGB8888},
scale::Scale,
@ -7,9 +8,13 @@ use {
utils::{errorfmt::ErrorFmt, transform_ext::TransformExt},
wire::{JayRandrId, jay_compositor, jay_randr},
},
clap::{Args, Subcommand, ValueEnum},
clap::{
Args, Subcommand, ValueEnum,
builder::{PossibleValue, PossibleValuesParser},
},
isnt::std_1::vec::IsntVecExt,
jay_config::video::{TearingMode, Transform, VrrMode},
linearize::LinearizeExt,
std::{
cell::RefCell,
fmt::{Display, Formatter},
@ -154,6 +159,8 @@ pub enum OutputCommand {
Tearing(TearingArgs),
/// Change format settings.
Format(FormatSettings),
/// Change color settings.
Colors(ColorsSettings),
}
#[derive(ValueEnum, Debug, Clone)]
@ -315,6 +322,51 @@ pub enum TransformCmd {
FlipRotate270,
}
#[derive(Args, Debug, Clone)]
pub struct ColorsSettings {
#[clap(subcommand)]
pub command: ColorsCommand,
}
#[derive(Subcommand, Debug, Clone)]
pub enum ColorsCommand {
/// Sets the color space and transfer function of the output.
Set {
/// The name of the color space.
#[clap(value_parser = PossibleValuesParser::new(color_space_possible_values()))]
color_space: String,
/// The name of the transfer function.
#[clap(value_parser = PossibleValuesParser::new(transfer_function_possible_values()))]
transfer_function: String,
},
}
fn color_space_possible_values() -> Vec<PossibleValue> {
let mut res = vec![];
for cs in BackendColorSpace::variants() {
use BackendColorSpace::*;
let help = match cs {
Default => "The default color space (usually sRGB)",
Bt2020 => "The BT.2020 color space",
};
res.push(PossibleValue::new(cs.name()).help(help));
}
res
}
fn transfer_function_possible_values() -> Vec<PossibleValue> {
let mut res = vec![];
for cs in BackendTransferFunction::variants() {
use BackendTransferFunction::*;
let help = match cs {
Default => "The default transfer function (usually sRGB)",
Pq => "The PQ transfer function",
};
res.push(PossibleValue::new(cs.name()).help(help));
}
res
}
pub fn main(global: GlobalArgs, args: RandrArgs) {
with_tool_client(global.log_level.into(), |tc| async move {
let idle = Rc::new(Randr { tc: tc.clone() });
@ -368,6 +420,10 @@ struct Output {
pub formats: Vec<String>,
pub format: Option<String>,
pub flip_margin_ns: Option<u64>,
pub supported_color_spaces: Vec<String>,
pub current_color_space: Option<String>,
pub supported_transfer_functions: Vec<String>,
pub current_transfer_function: Option<String>,
}
#[derive(Copy, Clone, Debug)]
@ -610,6 +666,24 @@ impl Randr {
}
}
}
OutputCommand::Colors(a) => {
self.handle_error(randr, move |msg| {
eprintln!("Could not change the colors: {}", msg);
});
match a.command {
ColorsCommand::Set {
color_space,
transfer_function,
} => {
tc.send(jay_randr::SetColors {
self_id: randr,
output: &args.output,
color_space: &color_space,
transfer_function: &transfer_function,
});
}
}
}
}
tc.round_trip().await;
}
@ -806,6 +880,32 @@ impl Randr {
);
}
}
if o.supported_color_spaces.is_not_empty() {
println!(" color spaces:");
let handle_cs = |cs: &str| {
let current = match Some(cs) == o.current_color_space.as_deref() {
false => "",
true => " (current)",
};
println!(" {cs}{current}");
};
handle_cs("default");
o.supported_color_spaces.iter().for_each(|cs| handle_cs(cs));
}
if o.supported_transfer_functions.is_not_empty() {
println!(" transfer functions:");
let handle_tf = |tf: &str| {
let current = match Some(tf) == o.current_transfer_function.as_deref() {
false => "",
true => " (current)",
};
println!(" {tf}{current}");
};
handle_tf("default");
o.supported_transfer_functions
.iter()
.for_each(|tf| handle_tf(tf));
}
if o.modes.is_not_empty() && modes {
println!(" modes:");
for mode in &o.modes {
@ -886,6 +986,10 @@ impl Randr {
formats: vec![],
format: None,
flip_margin_ns: None,
supported_color_spaces: vec![],
current_color_space: None,
supported_transfer_functions: vec![],
current_transfer_function: None,
});
});
jay_randr::NonDesktopOutput::handle(tc, randr, data.clone(), |data, msg| {
@ -914,6 +1018,10 @@ impl Randr {
formats: vec![],
format: None,
flip_margin_ns: None,
supported_color_spaces: vec![],
current_color_space: None,
supported_transfer_functions: vec![],
current_transfer_function: None,
});
});
jay_randr::VrrState::handle(tc, randr, data.clone(), |data, msg| {
@ -966,6 +1074,34 @@ impl Randr {
}
o.modes.push(mode);
});
jay_randr::SupportedColorSpace::handle(tc, randr, data.clone(), |data, msg| {
let mut data = data.borrow_mut();
let c = data.connectors.last_mut().unwrap();
let output = c.output.as_mut().unwrap();
output
.supported_color_spaces
.push(msg.color_space.to_string());
});
jay_randr::CurrentColorSpace::handle(tc, randr, data.clone(), |data, msg| {
let mut data = data.borrow_mut();
let c = data.connectors.last_mut().unwrap();
let output = c.output.as_mut().unwrap();
output.current_color_space = Some(msg.color_space.to_string());
});
jay_randr::SupportedTransferFunction::handle(tc, randr, data.clone(), |data, msg| {
let mut data = data.borrow_mut();
let c = data.connectors.last_mut().unwrap();
let output = c.output.as_mut().unwrap();
output
.supported_transfer_functions
.push(msg.transfer_function.to_string());
});
jay_randr::CurrentTransferFunction::handle(tc, randr, data.clone(), |data, msg| {
let mut data = data.borrow_mut();
let c = data.connectors.last_mut().unwrap();
let output = c.output.as_mut().unwrap();
output.current_transfer_function = Some(msg.transfer_function.to_string());
});
tc.round_trip().await;
let x = data.borrow_mut().clone();
x

View file

@ -1,13 +1,13 @@
use {
crate::{
cmm::{
cmm_luminance::{Luminance, white_balance},
cmm_luminance::{Luminance, TargetLuminance, white_balance},
cmm_manager::Shared,
cmm_primaries::{NamedPrimaries, Primaries},
cmm_transfer_function::TransferFunction,
cmm_transform::{ColorMatrix, Local, Xyz, bradford_adjustment},
},
utils::free_list::FreeList,
utils::{free_list::FreeList, ordered_float::F64},
},
std::rc::Rc,
};
@ -19,6 +19,12 @@ pub type ColorDescriptionIds = FreeList<ColorDescriptionId, 3>;
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct ColorDescriptionId(u32);
impl ColorDescriptionId {
pub fn raw(self) -> u32 {
self.0
}
}
impl From<u32> for ColorDescriptionId {
fn from(value: u32) -> Self {
Self(value)
@ -38,6 +44,10 @@ pub struct LinearColorDescription {
pub xyz_from_local: ColorMatrix<Xyz, Local>,
pub local_from_xyz: ColorMatrix<Local, Xyz>,
pub luminance: Luminance,
pub target_primaries: Primaries,
pub target_luminance: TargetLuminance,
pub max_cll: Option<F64>,
pub max_fall: Option<F64>,
pub(super) shared: Rc<Shared>,
}
@ -45,7 +55,6 @@ pub struct LinearColorDescription {
pub struct ColorDescription {
pub id: ColorDescriptionId,
pub linear: Rc<LinearColorDescription>,
#[expect(dead_code)]
pub named_primaries: Option<NamedPrimaries>,
pub transfer_function: TransferFunction,
pub(super) shared: Rc<Shared>,
@ -62,6 +71,26 @@ impl LinearColorDescription {
}
mat * self.xyz_from_local
}
pub fn embeds_into(&self, target: &Self) -> bool {
if self.id == target.id {
return true;
}
if self.primaries != target.primaries {
return false;
}
if self.luminance != target.luminance {
return false;
}
true
}
}
impl ColorDescription {
pub fn embeds_into(&self, target: &Self) -> bool {
self.transfer_function == target.transfer_function
&& self.linear.embeds_into(&target.linear)
}
}
impl Drop for LinearColorDescription {

View file

@ -10,6 +10,12 @@ pub struct Luminance {
pub white: F64,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct TargetLuminance {
pub min: F64,
pub max: F64,
}
impl Luminance {
pub const SRGB: Self = Self {
min: F64(0.2),
@ -46,6 +52,15 @@ impl Luminance {
};
}
impl Luminance {
pub fn to_target(&self) -> TargetLuminance {
TargetLuminance {
min: self.min,
max: self.max,
}
}
}
impl Default for Luminance {
fn default() -> Self {
Self::SRGB

View file

@ -5,11 +5,11 @@ use {
ColorDescription, ColorDescriptionIds, LinearColorDescription,
LinearColorDescriptionId, LinearColorDescriptionIds,
},
cmm_luminance::Luminance,
cmm_luminance::{Luminance, TargetLuminance},
cmm_primaries::{NamedPrimaries, Primaries},
cmm_transfer_function::TransferFunction,
},
utils::{copyhashmap::CopyHashMap, numcell::NumCell},
utils::{copyhashmap::CopyHashMap, numcell::NumCell, ordered_float::F64},
},
std::rc::{Rc, Weak},
};
@ -35,6 +35,10 @@ pub(super) struct Shared {
struct LinearDescriptionKey {
primaries: Primaries,
luminance: Luminance,
target_primaries: Primaries,
target_luminance: TargetLuminance,
max_cll: Option<F64>,
max_fall: Option<F64>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
@ -60,15 +64,16 @@ impl ColorManager {
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,
Luminance::SRGB.to_target(),
None,
None,
);
let srgb_linear = get_description2(
&shared,
&srgb_srgb.linear,
&complete_descriptions,
Some(NamedPrimaries::Srgb),
TransferFunction::Linear,
);
let windows_scrgb = get_description(
@ -80,6 +85,10 @@ impl ColorManager {
Primaries::SRGB,
Luminance::WINDOWS_SCRGB,
TransferFunction::Linear,
Primaries::BT2020,
Luminance::ST2084_PQ.to_target(),
None,
None,
);
Rc::new(Self {
linear_ids,
@ -110,6 +119,10 @@ impl ColorManager {
primaries: Primaries,
luminance: Luminance,
transfer_function: TransferFunction,
target_primaries: Primaries,
target_luminance: TargetLuminance,
max_cll: Option<F64>,
max_fall: Option<F64>,
) -> Rc<ColorDescription> {
get_description(
&self.shared,
@ -120,6 +133,24 @@ impl ColorManager {
primaries,
luminance,
transfer_function,
target_primaries,
target_luminance,
max_cll,
max_fall,
)
}
pub fn get_with_tf(
self: &Rc<Self>,
cd: &Rc<ColorDescription>,
transfer_function: TransferFunction,
) -> Rc<ColorDescription> {
get_description2(
&self.shared,
&cd.linear,
&self.complete_descriptions,
cd.named_primaries,
transfer_function,
)
}
}
@ -133,6 +164,10 @@ fn get_description(
primaries: Primaries,
luminance: Luminance,
transfer_function: TransferFunction,
target_primaries: Primaries,
target_luminance: TargetLuminance,
max_cll: Option<F64>,
max_fall: Option<F64>,
) -> Rc<ColorDescription> {
macro_rules! gc {
($d:ident, $i:expr) => {
@ -147,29 +182,20 @@ fn get_description(
let key = LinearDescriptionKey {
primaries,
luminance,
target_primaries,
target_luminance,
max_cll,
max_fall,
};
if let Some(d) = linear_descriptions.get(&key) {
if let Some(d) = d.upgrade() {
let key = CompleteDescriptionKey {
linear: d.id,
return get_description2(
shared,
&d,
complete_descriptions,
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);
}
@ -180,6 +206,10 @@ fn get_description(
xyz_from_local,
local_from_xyz,
luminance,
target_primaries,
target_luminance,
max_cll,
max_fall,
shared: shared.clone(),
});
linear_descriptions.set(key, Rc::downgrade(&d));
@ -198,3 +228,32 @@ fn get_description(
complete_descriptions.set(key, Rc::downgrade(&d));
d
}
fn get_description2(
shared: &Rc<Shared>,
ld: &Rc<LinearColorDescription>,
complete_descriptions: &CopyHashMap<CompleteDescriptionKey, Weak<ColorDescription>>,
named_primaries: Option<NamedPrimaries>,
transfer_function: TransferFunction,
) -> Rc<ColorDescription> {
let key = CompleteDescriptionKey {
linear: ld.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: ld.clone(),
named_primaries,
transfer_function,
shared: shared.clone(),
});
complete_descriptions.set(key, Rc::downgrade(&d));
d
}

View file

@ -141,7 +141,18 @@ mod transforms {
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 d = |p| {
manager.get_description(
None,
p,
Luminance::SRGB,
TransferFunction::Linear,
p,
Luminance::SRGB.to_target(),
None,
None,
)
};
let d1 = d(p1);
let d2 = d(p2);
let m = d1.linear.color_transform(&d2.linear);

View file

@ -4,7 +4,7 @@ use {
crate::{
acceptor::{Acceptor, AcceptorError},
async_engine::{AsyncEngine, Phase, SpawnedFuture},
backend::{self, Backend, Connector},
backend::{self, Backend, BackendColorSpace, BackendTransferFunction, Connector},
backends::{
dummy::{DummyBackend, DummyOutput},
metal, x,
@ -12,7 +12,7 @@ use {
cli::{CliBackend, GlobalArgs, RunArgs},
client::{ClientId, Clients},
clientmem::{self, ClientMemError},
cmm::cmm_manager::ColorManager,
cmm::{cmm_manager::ColorManager, cmm_primaries::Primaries},
config::ConfigProxy,
cpu_worker::{CpuWorker, CpuWorkerError},
damage::{DamageVisualizer, visualize_damage},
@ -574,6 +574,12 @@ fn create_dummy_output(state: &Rc<State>) {
0,
&output_id,
&persistent_state,
Vec::new(),
BackendTransferFunction::Default,
Vec::new(),
BackendColorSpace::Default,
Primaries::SRGB,
None,
)),
jay_outputs: Default::default(),
workspaces: Default::default(),

View file

@ -2,8 +2,8 @@ use {
crate::{
async_engine::SpawnedFuture,
backend::{
self, ConnectorId, DrmDeviceId, InputDeviceAccelProfile, InputDeviceCapability,
InputDeviceId,
self, BackendColorSpace, BackendTransferFunction, ConnectorId, DrmDeviceId,
InputDeviceAccelProfile, InputDeviceCapability, InputDeviceId,
},
cmm::cmm_transfer_function::TransferFunction,
compositor::MAX_EXTENTS,
@ -51,7 +51,8 @@ use {
theme::{colors::Colorable, sized::Resizable},
timer::Timer as JayTimer,
video::{
Connector, DrmDevice, Format as ConfigFormat, GfxApi, TearingMode as ConfigTearingMode,
ColorSpace, Connector, DrmDevice, Format as ConfigFormat, GfxApi,
TearingMode as ConfigTearingMode, TransferFunction as ConfigTransferFunction,
Transform, VrrMode as ConfigVrrMode,
},
xwayland::XScalingMode,
@ -1104,6 +1105,27 @@ impl ConfigProxyHandler {
Ok(())
}
fn handle_connector_set_colors(
&self,
connector: Connector,
color_space: ColorSpace,
transfer_function: ConfigTransferFunction,
) -> Result<(), CphError> {
let bcs = match color_space {
ColorSpace::DEFAULT => BackendColorSpace::Default,
ColorSpace::BT2020 => BackendColorSpace::Bt2020,
_ => return Err(CphError::UnknownColorSpace(color_space)),
};
let btf = match transfer_function {
ConfigTransferFunction::DEFAULT => BackendTransferFunction::Default,
ConfigTransferFunction::PQ => BackendTransferFunction::Pq,
_ => return Err(CphError::UnknownTransferFunction(transfer_function)),
};
let connector = self.get_connector(connector)?;
connector.connector.set_colors(bcs, btf);
Ok(())
}
fn handle_set_vrr_mode(
&self,
connector: Option<Connector>,
@ -1994,6 +2016,13 @@ impl ConfigProxyHandler {
ClientMessage::SetColorManagementEnabled { enabled } => {
self.handle_set_color_management_enabled(enabled)
}
ClientMessage::ConnectorSetColors {
connector,
color_space,
transfer_function,
} => self
.handle_connector_set_colors(connector, color_space, transfer_function)
.wrn("connector_set_colors")?,
}
Ok(())
}
@ -2065,6 +2094,10 @@ enum CphError {
UnknownFormat(ConfigFormat),
#[error("Unknown x scaling mode {0:?}")]
UnknownXScalingMode(XScalingMode),
#[error("Unknown color space {0:?}")]
UnknownColorSpace(ColorSpace),
#[error("Unknown transfer function {0:?}")]
UnknownTransferFunction(ConfigTransferFunction),
}
trait WithRequestName {

View file

@ -468,6 +468,7 @@ impl CursorUser {
let transform = output.global.persistent.transform.get();
let render = output.hardware_cursor_needs_render.take();
let scale = output.global.persistent.scale.get();
let cd = output.global.color_description.get();
if render {
cursor.tick();
}
@ -507,7 +508,7 @@ impl CursorUser {
&self.group.state,
scale,
transform,
self.group.state.color_manager.srgb_srgb(),
&cd,
);
match res {
Ok(sync_file) => {

View file

@ -75,7 +75,6 @@ pub struct ScreenDimensions {
}
#[derive(Copy, Clone, Debug)]
#[expect(dead_code)]
pub struct ChromaticityCoordinates {
pub red_x: u16,
pub red_y: u16,
@ -1035,9 +1034,59 @@ impl<'a> EdidParser<'a> {
}
}
fn parse_cta_colorimetry_data_block(&mut self) -> Result<CtaDataBlock, EdidError> {
let [lo, hi] = *self.read_n::<2>()?;
Ok(CtaDataBlock::Colorimetry(CtaColorimetryDataBlock {
bt2020_rgb: lo.contains(0x80),
bt2020_ycc: lo.contains(0x40),
bt2020_cycc: lo.contains(0x20),
op_rgb: lo.contains(0x10),
op_ycc_601601: lo.contains(0x08),
s_ycc_601: lo.contains(0x04),
xv_ycc_709: lo.contains(0x02),
xv_ycc_601: lo.contains(0x01),
dci_p3: hi.contains(0x80),
}))
}
fn parse_cta_hdr_static_metadata_data_block(&mut self) -> Result<CtaDataBlock, EdidError> {
let et = self.read_u8()?;
let _ = self.read_u8()?;
let mut read_luminance = |min: bool| {
let v = self.read_u8().unwrap_or_default();
if v == 0 {
None
} else if min {
Some((v as f64 / 255.0).powi(2) / 100.0)
} else {
Some(50.0 * 2.0f64.powf(v as f64 / 32.0))
}
};
Ok(CtaDataBlock::StaticHdrMetadata(
CtaStaticHdrMetadataDataBlock {
traditional_gamma_sdr_luminance: et.contains(0x01),
traditional_gamma_hdr_luminance: et.contains(0x02),
smpte_st_2084: et.contains(0x04),
hlg: et.contains(0x08),
max_luminance: read_luminance(false),
max_frame_average_luminance: read_luminance(false),
min_luminance: read_luminance(true),
},
))
}
fn parse_cta_extended_data_block(&mut self) -> Result<CtaDataBlock, EdidError> {
match self.read_u8()? {
0x5 => self.parse_cta_colorimetry_data_block(),
0x6 => self.parse_cta_hdr_static_metadata_data_block(),
_ => Ok(CtaDataBlock::Unknown),
}
}
fn parse_cta_data_block(&mut self, tag: u8) -> Result<CtaDataBlock, EdidError> {
match tag {
0x3 => self.parse_cta_vendor_data_block(),
0x7 => self.parse_cta_extended_data_block(),
_ => Ok(CtaDataBlock::Unknown),
}
}
@ -1173,6 +1222,8 @@ pub struct CtaExtensionV3 {
pub enum CtaDataBlock {
Unknown,
VendorAmd(CtaAmdVendorDataBlock),
Colorimetry(CtaColorimetryDataBlock),
StaticHdrMetadata(CtaStaticHdrMetadataDataBlock),
}
#[derive(Debug)]
@ -1182,6 +1233,32 @@ pub struct CtaAmdVendorDataBlock {
pub maximum_refresh_hz: u8,
}
#[derive(Copy, Clone, Debug)]
#[expect(dead_code)]
pub struct CtaColorimetryDataBlock {
pub bt2020_rgb: bool,
pub bt2020_ycc: bool,
pub bt2020_cycc: bool,
pub op_rgb: bool,
pub op_ycc_601601: bool,
pub s_ycc_601: bool,
pub xv_ycc_709: bool,
pub xv_ycc_601: bool,
pub dci_p3: bool,
}
#[derive(Copy, Clone, Debug)]
#[expect(dead_code)]
pub struct CtaStaticHdrMetadataDataBlock {
pub traditional_gamma_sdr_luminance: bool,
pub traditional_gamma_hdr_luminance: bool,
pub smpte_st_2084: bool,
pub hlg: bool,
pub max_luminance: Option<f64>,
pub max_frame_average_luminance: Option<f64>,
pub min_luminance: Option<f64>,
}
#[derive(Debug)]
pub struct EdidFile {
pub base_block: EdidBaseBlock,

View file

@ -2138,7 +2138,7 @@ impl ColorTransforms {
src: &LinearColorDescription,
dst: &ColorDescription,
) -> Option<&mut ColorTransform> {
if src.id == dst.linear.id {
if src.embeds_into(&dst.linear) {
return None;
}
let ct = match self.map.entry([src.id, dst.linear.id]) {

View file

@ -1,7 +1,10 @@
use {
crate::{
client::{Client, ClientError},
ifs::color_management::wp_image_description_v1::WpImageDescriptionV1,
ifs::{
color_management::{CAUSE_NO_OUTPUT, wp_image_description_v1::WpImageDescriptionV1},
wl_output::OutputGlobalOpt,
},
leaks::Tracker,
object::{Object, Version},
wire::{WpColorManagementOutputV1Id, wp_color_management_output_v1::*},
@ -15,20 +18,29 @@ pub struct WpColorManagementOutputV1 {
pub client: Rc<Client>,
pub version: Version,
pub tracker: Tracker<Self>,
pub output: Rc<OutputGlobalOpt>,
}
impl WpColorManagementOutputV1 {
#[expect(dead_code)]
pub fn send_image_description_changed(&self) {
self.client
.event(ImageDescriptionChanged { self_id: self.id });
}
fn detach(&self) {
if let Some(output) = self.output.get() {
output
.color_description_listeners
.remove(&(self.client.id, self.id));
}
}
}
impl WpColorManagementOutputV1RequestHandler for WpColorManagementOutputV1 {
type Error = WpColorManagementOutputV1Error;
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.detach();
self.client.remove_obj(self)?;
Ok(())
}
@ -43,11 +55,15 @@ impl WpColorManagementOutputV1RequestHandler for WpColorManagementOutputV1 {
client: self.client.clone(),
version: self.version,
tracker: Default::default(),
description: self.client.state.color_manager.srgb_srgb().clone(),
description: self.output.get().map(|o| o.color_description.get()),
});
track!(self.client, obj);
self.client.add_client_obj(&obj)?;
obj.send_ready();
if obj.description.is_some() {
obj.send_ready();
} else {
obj.send_failed(CAUSE_NO_OUTPUT, "the output no longer exists");
}
Ok(())
}
}
@ -57,7 +73,11 @@ object_base! {
version = self.version;
}
impl Object for WpColorManagementOutputV1 {}
impl Object for WpColorManagementOutputV1 {
fn break_loops(&self) {
self.detach();
}
}
simple_add_obj!(WpColorManagementOutputV1);

View file

@ -1,7 +1,10 @@
use {
crate::{
client::{Client, ClientError},
ifs::color_management::wp_image_description_v1::WpImageDescriptionV1,
cmm::cmm_description::ColorDescription,
ifs::{
color_management::wp_image_description_v1::WpImageDescriptionV1, wl_surface::WlSurface,
},
leaks::Tracker,
object::{Object, Version},
wire::{
@ -18,6 +21,7 @@ pub struct WpColorManagementSurfaceFeedbackV1 {
pub client: Rc<Client>,
pub version: Version,
pub tracker: Tracker<Self>,
pub surface: Rc<WlSurface>,
}
impl WpColorManagementSurfaceFeedbackV1 {
@ -30,13 +34,20 @@ impl WpColorManagementSurfaceFeedbackV1 {
client: self.client.clone(),
version: self.version,
tracker: Default::default(),
description: self.client.state.color_manager.srgb_srgb().clone(),
description: Some(self.surface.get_output().global.color_description.get()),
});
track!(self.client, obj);
self.client.add_client_obj(&obj)?;
obj.send_ready();
Ok(())
}
pub fn send_preferred_changed(&self, cd: &ColorDescription) {
self.client.event(PreferredChanged {
self_id: self.id,
identity: cd.id.raw(),
});
}
}
impl WpColorManagementSurfaceFeedbackV1RequestHandler for WpColorManagementSurfaceFeedbackV1 {
@ -44,6 +55,7 @@ impl WpColorManagementSurfaceFeedbackV1RequestHandler for WpColorManagementSurfa
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.client.remove_obj(self)?;
self.surface.remove_color_management_feedback(self);
Ok(())
}

View file

@ -4,6 +4,7 @@ use {
globals::{Global, GlobalName},
ifs::{
color_management::{
FEATURE_EXTENDED_TARGET_VOLUME, FEATURE_SET_MASTERING_DISPLAY_PRIMARIES,
consts::{
FEATURE_PARAMETRIC, FEATURE_SET_LUMINANCES, FEATURE_SET_PRIMARIES,
FEATURE_WINDOWS_SCRGB, PRIMARIES_ADOBE_RGB, PRIMARIES_BT2020,
@ -77,6 +78,8 @@ impl WpColorManagerV1 {
self.send_supported_feature(FEATURE_PARAMETRIC);
self.send_supported_feature(FEATURE_SET_PRIMARIES);
self.send_supported_feature(FEATURE_SET_LUMINANCES);
self.send_supported_feature(FEATURE_SET_MASTERING_DISPLAY_PRIMARIES);
self.send_supported_feature(FEATURE_EXTENDED_TARGET_VOLUME);
self.send_supported_feature(FEATURE_WINDOWS_SCRGB);
self.send_supported_tf_named(TRANSFER_FUNCTION_BT1886);
self.send_supported_tf_named(TRANSFER_FUNCTION_GAMMA22);
@ -144,15 +147,21 @@ impl WpColorManagerV1RequestHandler for WpColorManagerV1 {
}
fn get_output(&self, req: GetOutput, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let _ = self.client.lookup(req.output)?;
let output = self.client.lookup(req.output)?;
let obj = Rc::new(WpColorManagementOutputV1 {
id: req.id,
client: self.client.clone(),
version: self.version,
tracker: Default::default(),
output: output.global.clone(),
});
track!(self.client, obj);
self.client.add_client_obj(&obj)?;
if let Some(global) = output.global.get() {
global
.color_description_listeners
.set((self.client.id, req.id), obj);
}
Ok(())
}
@ -176,15 +185,17 @@ impl WpColorManagerV1RequestHandler for WpColorManagerV1 {
req: GetSurfaceFeedback,
_slf: &Rc<Self>,
) -> Result<(), Self::Error> {
let _ = self.client.lookup(req.surface)?;
let surface = self.client.lookup(req.surface)?;
let obj = Rc::new(WpColorManagementSurfaceFeedbackV1 {
id: req.id,
client: self.client.clone(),
version: self.version,
tracker: Default::default(),
surface: surface.clone(),
});
track!(self.client, obj);
self.client.add_client_obj(&obj)?;
surface.add_color_management_feedback(&obj);
Ok(())
}
@ -209,6 +220,10 @@ impl WpColorManagerV1RequestHandler for WpColorManagerV1 {
tf: Default::default(),
primaries: Default::default(),
luminance: Default::default(),
mastering_primaries: Default::default(),
mastering_luminance: Default::default(),
max_cll: Default::default(),
max_fall: Default::default(),
});
track!(self.client, obj);
self.client.add_client_obj(&obj)?;
@ -225,7 +240,7 @@ impl WpColorManagerV1RequestHandler for WpColorManagerV1 {
client: self.client.clone(),
version: self.version,
tracker: Default::default(),
description: self.client.state.color_manager.windows_scrgb().clone(),
description: Some(self.client.state.color_manager.windows_scrgb().clone()),
});
track!(self.client, obj);
self.client.add_client_obj(&obj)?;

View file

@ -2,7 +2,7 @@ use {
crate::{
client::{Client, ClientError},
cmm::{
cmm_luminance::Luminance,
cmm_luminance::{Luminance, TargetLuminance},
cmm_primaries::{NamedPrimaries, Primaries},
cmm_transfer_function::TransferFunction,
},
@ -43,6 +43,10 @@ pub struct WpImageDescriptionCreatorParamsV1 {
pub tf: Cell<Option<TransferFunction>>,
pub primaries: Cell<Option<(Option<NamedPrimaries>, Primaries)>>,
pub luminance: Cell<Option<Luminance>>,
pub mastering_primaries: Cell<Option<Primaries>>,
pub mastering_luminance: Cell<Option<TargetLuminance>>,
pub max_cll: Cell<Option<F64>>,
pub max_fall: Cell<Option<F64>>,
}
impl WpImageDescriptionCreatorParamsV1RequestHandler for WpImageDescriptionCreatorParamsV1 {
@ -67,18 +71,27 @@ impl WpImageDescriptionCreatorParamsV1RequestHandler for WpImageDescriptionCreat
if luminance.max.0 <= luminance.min.0 || luminance.white.0 <= luminance.min.0 {
return Err(WpImageDescriptionCreatorParamsV1Error::MinLuminanceTooLow);
}
let target_primaries = self.mastering_primaries.get().unwrap_or(primaries);
let target_luminance = self
.mastering_luminance
.get()
.unwrap_or(luminance.to_target());
let description = self.client.state.color_manager.get_description(
named_primaries,
primaries,
luminance,
transfer_function,
target_primaries,
target_luminance,
self.max_cll.get(),
self.max_fall.get(),
);
let obj = Rc::new(WpImageDescriptionV1 {
id: req.image_description,
client: self.client.clone(),
version: self.version,
tracker: Default::default(),
description,
description: Some(description),
});
track!(self.client, obj);
self.client.add_client_obj(&obj)?;
@ -174,25 +187,59 @@ impl WpImageDescriptionCreatorParamsV1RequestHandler for WpImageDescriptionCreat
fn set_mastering_display_primaries(
&self,
_req: SetMasteringDisplayPrimaries,
req: SetMasteringDisplayPrimaries,
_slf: &Rc<Self>,
) -> Result<(), Self::Error> {
Err(WpImageDescriptionCreatorParamsV1Error::SetMasteringDisplayPrimariesNotSupported)
let map = |n: i32| F64(n as f64 * PRIMARIES_MUL_INV);
let primaries = Primaries {
r: (map(req.r_x), map(req.r_y)),
g: (map(req.g_x), map(req.g_y)),
b: (map(req.b_x), map(req.b_y)),
wp: (map(req.w_x), map(req.w_y)),
};
if self.mastering_primaries.replace(Some(primaries)).is_some() {
return Err(WpImageDescriptionCreatorParamsV1Error::MasteringPrimariesAlreadySet);
}
Ok(())
}
fn set_mastering_luminance(
&self,
_req: SetMasteringLuminance,
req: SetMasteringLuminance,
_slf: &Rc<Self>,
) -> Result<(), Self::Error> {
Err(WpImageDescriptionCreatorParamsV1Error::SetMasteringLuminanceNotSupported)
}
fn set_max_cll(&self, _req: SetMaxCll, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let luminance = TargetLuminance {
min: F64(req.min_lum as f64 * MIN_LUM_MUL_INV),
max: F64(req.max_lum as f64),
};
if luminance.max.0 <= luminance.min.0 {
return Err(WpImageDescriptionCreatorParamsV1Error::MinMasteringLuminanceTooLow);
}
if self.mastering_luminance.replace(Some(luminance)).is_some() {
return Err(WpImageDescriptionCreatorParamsV1Error::MasteringLuminancesAlreadySet);
}
Ok(())
}
fn set_max_fall(&self, _req: SetMaxFall, _slf: &Rc<Self>) -> Result<(), Self::Error> {
fn set_max_cll(&self, req: SetMaxCll, _slf: &Rc<Self>) -> Result<(), Self::Error> {
if self
.max_cll
.replace(Some(F64(req.max_cll as f64)))
.is_some()
{
return Err(WpImageDescriptionCreatorParamsV1Error::MaxCllAlreadySet);
}
Ok(())
}
fn set_max_fall(&self, req: SetMaxFall, _slf: &Rc<Self>) -> Result<(), Self::Error> {
if self
.max_fall
.replace(Some(F64(req.max_fall as f64)))
.is_some()
{
return Err(WpImageDescriptionCreatorParamsV1Error::MaxFallAlreadySet);
}
Ok(())
}
}
@ -210,10 +257,6 @@ simple_add_obj!(WpImageDescriptionCreatorParamsV1);
pub enum WpImageDescriptionCreatorParamsV1Error {
#[error(transparent)]
ClientError(Box<ClientError>),
#[error("set_mastering_luminance is not supported")]
SetMasteringLuminanceNotSupported,
#[error("set_mastering_display_primaries is not supported")]
SetMasteringDisplayPrimariesNotSupported,
#[error("{} is not a supported named primary", .0)]
UnsupportedPrimaries(u32),
#[error("set_tf_power is not supported")]
@ -232,5 +275,15 @@ pub enum WpImageDescriptionCreatorParamsV1Error {
TfNotSet,
#[error("The primaries were not set")]
PrimariesNotSet,
#[error("The mastering display primaries have already been set")]
MasteringPrimariesAlreadySet,
#[error("The mastering display luminances have already been set")]
MasteringLuminancesAlreadySet,
#[error("The minimum mastering luminance is too low")]
MinMasteringLuminanceTooLow,
#[error("The max CLL has already been set")]
MaxCllAlreadySet,
#[error("The max FALL has already been set")]
MaxFallAlreadySet,
}
efrom!(WpImageDescriptionCreatorParamsV1Error, ClientError);

View file

@ -1,9 +1,22 @@
use {
crate::{
client::Client,
ifs::color_management::consts::{PRIMARIES_SRGB, TRANSFER_FUNCTION_SRGB},
cmm::{
cmm_description::ColorDescription, cmm_primaries::NamedPrimaries,
cmm_transfer_function::TransferFunction,
},
ifs::color_management::{
MIN_LUM_MUL, PRIMARIES_ADOBE_RGB, PRIMARIES_BT2020, PRIMARIES_CIE1931_XYZ,
PRIMARIES_DCI_P3, PRIMARIES_DISPLAY_P3, PRIMARIES_GENERIC_FILM, PRIMARIES_MUL,
PRIMARIES_NTSC, PRIMARIES_PAL, PRIMARIES_PAL_M, TRANSFER_FUNCTION_BT1886,
TRANSFER_FUNCTION_EXT_LINEAR, TRANSFER_FUNCTION_EXT_SRGB, TRANSFER_FUNCTION_GAMMA22,
TRANSFER_FUNCTION_GAMMA28, TRANSFER_FUNCTION_LOG_100, TRANSFER_FUNCTION_LOG_316,
TRANSFER_FUNCTION_ST240, TRANSFER_FUNCTION_ST428, TRANSFER_FUNCTION_ST2084_PQ,
consts::{PRIMARIES_SRGB, TRANSFER_FUNCTION_SRGB},
},
leaks::Tracker,
object::{Object, Version},
utils::ordered_float::F64,
wire::{WpImageDescriptionInfoV1Id, wp_image_description_info_v1::*},
},
std::{convert::Infallible, rc::Rc},
@ -18,17 +31,46 @@ pub struct WpImageDescriptionInfoV1 {
}
impl WpImageDescriptionInfoV1 {
pub fn send_srgb(&self) {
let red = [0.64, 0.33];
let green = [0.3, 0.6];
let blue = [0.15, 0.06];
let white = [0.3127, 0.3290];
self.send_primaries(red, green, blue, white);
self.send_primaries_named(PRIMARIES_SRGB);
self.send_tf_named(TRANSFER_FUNCTION_SRGB);
self.send_luminances(0.2, 80.0, 80.0);
self.send_target_primaries(red, green, blue, white);
self.send_target_luminances(0.2, 80.0);
pub fn send_description(&self, d: &ColorDescription) {
let tf = match d.transfer_function {
TransferFunction::Srgb => TRANSFER_FUNCTION_SRGB,
TransferFunction::Linear => TRANSFER_FUNCTION_EXT_LINEAR,
TransferFunction::St2084Pq => TRANSFER_FUNCTION_ST2084_PQ,
TransferFunction::Bt1886 => TRANSFER_FUNCTION_BT1886,
TransferFunction::Gamma22 => TRANSFER_FUNCTION_GAMMA22,
TransferFunction::Gamma28 => TRANSFER_FUNCTION_GAMMA28,
TransferFunction::St240 => TRANSFER_FUNCTION_ST240,
TransferFunction::ExtSrgb => TRANSFER_FUNCTION_EXT_SRGB,
TransferFunction::Log100 => TRANSFER_FUNCTION_LOG_100,
TransferFunction::Log316 => TRANSFER_FUNCTION_LOG_316,
TransferFunction::St428 => TRANSFER_FUNCTION_ST428,
};
self.send_primaries(&d.linear.primaries);
if let Some(n) = d.named_primaries {
let n = match n {
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,
};
self.send_primaries_named(n);
}
self.send_tf_named(tf);
self.send_luminances(&d.linear.luminance);
self.send_target_primaries(&d.linear.target_primaries);
self.send_target_luminances(&d.linear.target_luminance);
if let Some(max_cll) = d.linear.max_cll {
self.send_target_max_cll(max_cll.0);
}
if let Some(max_fall) = d.linear.max_fall {
self.send_target_max_fall(max_fall.0);
}
self.send_done();
}
@ -45,18 +87,18 @@ impl WpImageDescriptionInfoV1 {
});
}
pub fn send_primaries(&self, r: [f64; 2], g: [f64; 2], b: [f64; 2], w: [f64; 2]) {
let map = |c: f64| (c * 1_000_000.0) as i32;
pub fn send_primaries(&self, p: &crate::cmm::cmm_primaries::Primaries) {
let map = |c: F64| (c.0 * PRIMARIES_MUL) as i32;
self.client.event(Primaries {
self_id: self.id,
r_x: map(r[0]),
r_y: map(r[1]),
g_x: map(g[0]),
g_y: map(g[1]),
b_x: map(b[0]),
b_y: map(b[1]),
w_x: map(w[0]),
w_y: map(w[1]),
r_x: map(p.r.0),
r_y: map(p.r.1),
g_x: map(p.g.0),
g_y: map(p.g.1),
b_x: map(p.b.0),
b_y: map(p.b.1),
w_x: map(p.wp.0),
w_y: map(p.wp.1),
});
}
@ -82,39 +124,38 @@ impl WpImageDescriptionInfoV1 {
});
}
pub fn send_luminances(&self, min_lum: f64, max_lum: f64, reference_lum: f64) {
pub fn send_luminances(&self, l: &crate::cmm::cmm_luminance::Luminance) {
self.client.event(Luminances {
self_id: self.id,
min_lum: (min_lum * 10_000.0) as u32,
max_lum: max_lum as _,
reference_lum: reference_lum as _,
min_lum: (l.min.0 * MIN_LUM_MUL) as u32,
max_lum: l.max.0 as _,
reference_lum: l.white.0 as _,
});
}
pub fn send_target_primaries(&self, r: [f64; 2], g: [f64; 2], b: [f64; 2], w: [f64; 2]) {
let map = |c: f64| (c * 1_000_000.0) as i32;
pub fn send_target_primaries(&self, p: &crate::cmm::cmm_primaries::Primaries) {
let map = |c: F64| (c.0 * PRIMARIES_MUL) as i32;
self.client.event(TargetPrimaries {
self_id: self.id,
r_x: map(r[0]),
r_y: map(r[1]),
g_x: map(g[0]),
g_y: map(g[1]),
b_x: map(b[0]),
b_y: map(b[1]),
w_x: map(w[0]),
w_y: map(w[1]),
r_x: map(p.r.0),
r_y: map(p.r.1),
g_x: map(p.g.0),
g_y: map(p.g.1),
b_x: map(p.b.0),
b_y: map(p.b.1),
w_x: map(p.wp.0),
w_y: map(p.wp.1),
});
}
pub fn send_target_luminances(&self, min_lum: f64, max_lum: f64) {
pub fn send_target_luminances(&self, l: &crate::cmm::cmm_luminance::TargetLuminance) {
self.client.event(TargetLuminance {
self_id: self.id,
min_lum: (min_lum * 10_000.0) as u32,
max_lum: max_lum as _,
min_lum: (l.min.0 * MIN_LUM_MUL) as u32,
max_lum: l.max.0 as _,
});
}
#[expect(dead_code)]
pub fn send_target_max_cll(&self, max_cll: f64) {
self.client.event(TargetMaxCll {
self_id: self.id,
@ -122,7 +163,6 @@ impl WpImageDescriptionInfoV1 {
});
}
#[expect(dead_code)]
pub fn send_target_max_fall(&self, max_fall: f64) {
self.client.event(TargetMaxFall {
self_id: self.id,

View file

@ -16,11 +16,10 @@ pub struct WpImageDescriptionV1 {
pub client: Rc<Client>,
pub version: Version,
pub tracker: Tracker<Self>,
pub description: Rc<ColorDescription>,
pub description: Option<Rc<ColorDescription>>,
}
impl WpImageDescriptionV1 {
#[expect(dead_code)]
pub fn send_failed(&self, cause: u32, msg: &str) {
self.client.event(Failed {
self_id: self.id,
@ -32,7 +31,7 @@ impl WpImageDescriptionV1 {
pub fn send_ready(&self) {
self.client.event(Ready {
self_id: self.id,
identity: self.description.id.into(),
identity: self.description.as_ref().unwrap().id.raw(),
});
}
}
@ -46,6 +45,9 @@ impl WpImageDescriptionV1RequestHandler for WpImageDescriptionV1 {
}
fn get_information(&self, req: GetInformation, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let Some(desc) = &self.description else {
return Err(WpImageDescriptionV1Error::NotReady);
};
let obj = Rc::new(WpImageDescriptionInfoV1 {
id: req.information,
client: self.client.clone(),
@ -54,7 +56,7 @@ impl WpImageDescriptionV1RequestHandler for WpImageDescriptionV1 {
});
self.client.add_client_obj(&obj)?;
track!(self.client, obj);
obj.send_srgb();
obj.send_description(desc);
self.client.remove_obj(&*obj)?;
Ok(())
}
@ -77,5 +79,7 @@ dedicated_add_obj!(
pub enum WpImageDescriptionV1Error {
#[error(transparent)]
ClientError(Box<ClientError>),
#[error("The description is not ready")]
NotReady,
}
efrom!(WpImageDescriptionV1Error, ClientError);

View file

@ -73,7 +73,7 @@ impl Global for JayCompositorGlobal {
}
fn version(&self) -> u32 {
14
15
}
fn required_caps(&self) -> ClientCaps {

View file

@ -1,6 +1,6 @@
use {
crate::{
backend,
backend::{self, BackendColorSpace, BackendTransferFunction},
client::{Client, ClientError},
compositor::MAX_EXTENTS,
format::named_formats,
@ -15,6 +15,7 @@ use {
jay_config::video::{
GfxApi, TearingMode as ConfigTearingMode, Transform, VrrMode as ConfigVrrMode,
},
linearize::LinearizeExt,
std::rc::Rc,
thiserror::Error,
};
@ -30,6 +31,7 @@ const VRR_CAPABLE_SINCE: Version = Version(2);
const TEARING_SINCE: Version = Version(3);
const FORMAT_SINCE: Version = Version(8);
const FLIP_MARGIN_SINCE: Version = Version(10);
const COLORIMETRY_SINCE: Version = Version(15);
impl JayRandr {
pub fn new(id: JayRandrId, client: &Rc<Client>, version: Version) -> Self {
@ -163,6 +165,28 @@ impl JayRandr {
current: (mode == &current_mode) as _,
});
}
if self.version >= COLORIMETRY_SINCE {
for tf in &node.global.transfer_functions {
self.client.event(SupportedTransferFunction {
self_id: self.id,
transfer_function: tf.name(),
});
}
self.client.event(CurrentTransferFunction {
self_id: self.id,
transfer_function: node.global.btf.get().name(),
});
for cs in &node.global.color_spaces {
self.client.event(SupportedColorSpace {
self_id: self.id,
color_space: cs.name(),
});
}
self.client.event(CurrentColorSpace {
self_id: self.id,
color_space: node.global.bcs.get().name(),
});
}
}
fn send_error(&self, msg: &str) {
@ -412,6 +436,34 @@ impl JayRandrRequestHandler for JayRandr {
dev.dev.set_flip_margin(req.margin_ns);
Ok(())
}
fn set_colors(&self, req: SetColors<'_>, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let cs = 'cs: {
for cs in BackendColorSpace::variants() {
if cs.name() == req.color_space {
break 'cs cs;
}
}
return Err(JayRandrError::UnknownColorSpace(
req.color_space.to_string(),
));
};
let tf = 'tf: {
for tf in BackendTransferFunction::variants() {
if tf.name() == req.transfer_function {
break 'tf tf;
}
}
return Err(JayRandrError::UnknownTransferFunction(
req.transfer_function.to_string(),
));
};
let Some(c) = self.get_connector(req.output) else {
return Ok(());
};
c.connector.set_colors(cs, tf);
Ok(())
}
}
object_base! {
@ -433,5 +485,9 @@ pub enum JayRandrError {
UnknownTearingMode(u32),
#[error("Unknown format {0}")]
UnknownFormat(String),
#[error("Unknown color space {0}")]
UnknownColorSpace(String),
#[error("Unknown transfer function {0}")]
UnknownTransferFunction(String),
}
efrom!(JayRandrError, ClientError);

View file

@ -2,22 +2,31 @@ mod removed_output;
use {
crate::{
backend,
backend::{self, BackendColorSpace, BackendLuminance, BackendTransferFunction},
client::{Client, ClientError, ClientId},
cmm::{
cmm_description::ColorDescription,
cmm_luminance::Luminance,
cmm_primaries::{NamedPrimaries, Primaries},
cmm_transfer_function::TransferFunction,
},
damage::DamageMatrix,
format::{Format, XRGB8888},
globals::{Global, GlobalName},
ifs::{wl_surface::WlSurface, zxdg_output_v1::ZxdgOutputV1},
ifs::{
color_management::wp_color_management_output_v1::WpColorManagementOutputV1,
wl_surface::WlSurface, zxdg_output_v1::ZxdgOutputV1,
},
leaks::Tracker,
object::{Object, Version},
rect::Rect,
state::{ConnectorData, State},
tree::{OutputNode, TearingMode, VrrMode, calculate_logical_size},
utils::{
cell_ext::CellExt, clonecell::CloneCell, copyhashmap::CopyHashMap, rc_eq::rc_eq,
transform_ext::TransformExt,
cell_ext::CellExt, clonecell::CloneCell, copyhashmap::CopyHashMap, ordered_float::F64,
rc_eq::rc_eq, transform_ext::TransformExt,
},
wire::{WlOutputId, ZxdgOutputV1Id, wl_output::*},
wire::{WlOutputId, WpColorManagementOutputV1Id, ZxdgOutputV1Id, wl_output::*},
},
ahash::AHashMap,
jay_config::video::Transform,
@ -67,12 +76,22 @@ pub struct WlOutputGlobal {
pub format: Cell<&'static Format>,
pub width_mm: i32,
pub height_mm: i32,
pub transfer_functions: Vec<BackendTransferFunction>,
pub color_spaces: Vec<BackendColorSpace>,
pub primaries: Primaries,
pub luminance: Option<BackendLuminance>,
pub bindings: RefCell<AHashMap<ClientId, AHashMap<WlOutputId, Rc<WlOutput>>>>,
pub destroyed: Cell<bool>,
pub legacy_scale: Cell<u32>,
pub persistent: Rc<PersistentOutputState>,
pub opt: Rc<OutputGlobalOpt>,
pub damage_matrix: Cell<DamageMatrix>,
pub btf: Cell<BackendTransferFunction>,
pub bcs: Cell<BackendColorSpace>,
pub color_description: CloneCell<Rc<ColorDescription>>,
pub linear_color_description: CloneCell<Rc<ColorDescription>>,
pub color_description_listeners:
CopyHashMap<(ClientId, WpColorManagementOutputV1Id), Rc<WpColorManagementOutputV1>>,
}
#[derive(Default)]
@ -133,6 +152,7 @@ impl WlOutputGlobal {
pub fn clear(&self) {
self.opt.clear();
self.bindings.borrow_mut().clear();
self.color_description_listeners.clear();
}
pub fn new(
@ -145,6 +165,12 @@ impl WlOutputGlobal {
height_mm: i32,
output_id: &Rc<OutputId>,
persistent_state: &Rc<PersistentOutputState>,
transfer_functions: Vec<BackendTransferFunction>,
btf: BackendTransferFunction,
color_spaces: Vec<BackendColorSpace>,
bcs: BackendColorSpace,
primaries: Primaries,
luminance: Option<BackendLuminance>,
) -> Self {
let (x, y) = persistent_state.pos.get();
let scale = persistent_state.scale.get();
@ -166,14 +192,24 @@ impl WlOutputGlobal {
format: Cell::new(XRGB8888),
width_mm,
height_mm,
transfer_functions,
color_spaces,
primaries,
luminance,
bindings: Default::default(),
destroyed: Cell::new(false),
legacy_scale: Cell::new(scale.round_up()),
persistent: persistent_state.clone(),
opt: Default::default(),
damage_matrix: Default::default(),
btf: Cell::new(btf),
bcs: Cell::new(bcs),
color_description: CloneCell::new(state.color_manager.srgb_srgb().clone()),
linear_color_description: CloneCell::new(state.color_manager.srgb_linear().clone()),
color_description_listeners: Default::default(),
};
global.update_damage_matrix();
global.update_color_description();
global
}
@ -292,6 +328,46 @@ impl WlOutputGlobal {
pub fn add_visualizer_damage(&self) {
self.state.damage_visualizer.copy_damage(self);
}
pub fn update_color_description(&self) -> bool {
let mut luminance = Luminance::SRGB;
let tf = match self.btf.get() {
BackendTransferFunction::Default => TransferFunction::Srgb,
BackendTransferFunction::Pq => {
luminance = Luminance::ST2084_PQ;
TransferFunction::St2084Pq
}
};
let mut target_luminance = luminance.to_target();
let mut max_cll = None;
let mut max_fall = None;
if let Some(l) = self.luminance {
target_luminance.min = F64(l.min);
target_luminance.max = F64(l.max);
max_cll = Some(F64(l.max));
max_fall = Some(F64(l.max_fall));
}
let primaries = match self.bcs.get() {
BackendColorSpace::Default => NamedPrimaries::Srgb,
BackendColorSpace::Bt2020 => NamedPrimaries::Bt2020,
};
let cd = self.state.color_manager.get_description(
Some(primaries),
primaries.primaries(),
luminance,
tf,
self.primaries,
target_luminance,
max_cll,
max_fall,
);
let cd_linear = self
.state
.color_manager
.get_with_tf(&cd, TransferFunction::Linear);
self.linear_color_description.set(cd_linear.clone());
self.color_description.set(cd.clone()).id != cd.id
}
}
global_base!(WlOutputGlobal, WlOutput, WlOutputError);

View file

@ -33,6 +33,7 @@ use {
ReleaseSync, SampleRect, SyncFile,
},
ifs::{
color_management::wp_color_management_surface_feedback_v1::WpColorManagementSurfaceFeedbackV1,
wl_buffer::WlBuffer,
wl_callback::WlCallback,
wl_seat::{
@ -89,8 +90,8 @@ use {
drm::sync_obj::{SyncObj, SyncObjPoint},
},
wire::{
WlOutputId, WlSurfaceId, ZwpIdleInhibitorV1Id, ZwpLinuxDmabufFeedbackV1Id,
wl_surface::*,
WlOutputId, WlSurfaceId, WpColorManagementSurfaceFeedbackV1Id, ZwpIdleInhibitorV1Id,
ZwpLinuxDmabufFeedbackV1Id, wl_surface::*,
},
xwayland::XWaylandEvent,
},
@ -201,6 +202,14 @@ impl NodeVisitorBase for SurfaceSendPreferredTransformVisitor {
}
}
pub struct SurfaceSendPreferredColorDescription;
impl NodeVisitorBase for SurfaceSendPreferredColorDescription {
fn visit_surface(&mut self, node: &Rc<WlSurface>) {
node.send_preferred_color_description();
node.node_visit_children(self);
}
}
struct SurfaceBufferExplicitRelease {
sync_obj: Rc<SyncObj>,
point: SyncObjPoint,
@ -336,6 +345,8 @@ pub struct WlSurface {
before_latch_listener: EventListener<dyn BeforeLatchListener>,
is_opaque: Cell<bool>,
color_management_surface: CloneCell<Option<Rc<WpColorManagementSurfaceV1>>>,
color_management_feedback:
CopyHashMap<WpColorManagementSurfaceFeedbackV1Id, Rc<WpColorManagementSurfaceFeedbackV1>>,
color_description: CloneCell<Option<Rc<ColorDescription>>>,
}
@ -678,6 +689,7 @@ impl WlSurface {
before_latch_listener: EventListener::new(slf.clone()),
is_opaque: Cell::new(false),
color_management_surface: Default::default(),
color_management_feedback: Default::default(),
color_description: Default::default(),
}
}
@ -699,7 +711,6 @@ impl WlSurface {
Ok(ext.into_xsurface().unwrap())
}
#[cfg_attr(not(feature = "it"), expect(dead_code))]
pub fn get_output(&self) -> Rc<OutputNode> {
self.output.get()
}
@ -720,6 +731,9 @@ impl WlSurface {
if old.global.persistent.transform.get() != output.global.persistent.transform.get() {
self.send_preferred_buffer_transform();
}
if old.global.color_description.get().id != output.global.color_description.get().id {
self.send_preferred_color_description();
}
let children = self.children.borrow_mut();
if let Some(children) = &*children {
for ss in children.subsurfaces.values() {
@ -1682,6 +1696,24 @@ impl WlSurface {
None => self.client.state.color_manager.srgb_srgb().clone(),
}
}
pub fn add_color_management_feedback(&self, fb: &Rc<WpColorManagementSurfaceFeedbackV1>) {
self.color_management_feedback.set(fb.id, fb.clone());
}
pub fn remove_color_management_feedback(&self, fb: &WpColorManagementSurfaceFeedbackV1) {
self.color_management_feedback.remove(&fb.id);
}
pub fn send_preferred_color_description(&self) {
if self.color_management_feedback.is_empty() {
return;
}
let cd = self.output.get().global.color_description.get();
for fb in self.color_management_feedback.lock().values() {
fb.send_preferred_changed(&cd);
}
}
}
object_base! {
@ -1714,6 +1746,7 @@ impl Object for WlSurface {
self.fifo.take();
self.commit_timer.take();
self.color_management_surface.take();
self.color_management_feedback.clear();
}
}

View file

@ -41,6 +41,7 @@ impl WpColorManagementSurfaceV1RequestHandler for WpColorManagementSurfaceV1 {
fn destroy(&self, _req: Destroy, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.surface.color_management_surface.take();
self.surface.pending.borrow_mut().color_description = Some(None);
self.client.remove_obj(self)?;
Ok(())
}
@ -56,7 +57,10 @@ impl WpColorManagementSurfaceV1RequestHandler for WpColorManagementSurfaceV1 {
));
}
let desc = self.client.lookup(req.image_description)?;
self.surface.pending.borrow_mut().color_description = Some(Some(desc.description.clone()));
if desc.description.is_none() {
return Err(WpColorManagementSurfaceV1Error::NotReady);
}
self.surface.pending.borrow_mut().color_description = Some(desc.description.clone());
Ok(())
}
@ -87,5 +91,7 @@ pub enum WpColorManagementSurfaceV1Error {
UnsupportedRenderIntent(u32),
#[error("wl_surface already has a color-management extension")]
HasSurface,
#[error("The color description is not ready")]
NotReady,
}
efrom!(WpColorManagementSurfaceV1Error, ClientError);

View file

@ -3,11 +3,12 @@ use {
allocator::{Allocator, AllocatorError},
async_engine::SpawnedFuture,
backend::{
AxisSource, Backend, BackendEvent, Connector, ConnectorEvent, ConnectorId,
ConnectorKernelId, DrmDeviceId, InputDevice, InputDeviceAccelProfile,
InputDeviceCapability, InputDeviceId, InputEvent, KeyState, Mode, MonitorInfo,
ScrollAxis, TransformMatrix,
AxisSource, Backend, BackendColorSpace, BackendEvent, BackendTransferFunction,
Connector, ConnectorEvent, ConnectorId, ConnectorKernelId, DrmDeviceId, InputDevice,
InputDeviceAccelProfile, InputDeviceCapability, InputDeviceId, InputEvent, KeyState,
Mode, MonitorInfo, ScrollAxis, TransformMatrix,
},
cmm::cmm_primaries::Primaries,
compositor::TestFuture,
drm_feedback::DrmFeedback,
fixed::Fixed,
@ -129,6 +130,12 @@ impl TestBackend {
height_mm: 60,
non_desktop: false,
vrr_capable: false,
transfer_functions: vec![],
transfer_function: BackendTransferFunction::Default,
color_spaces: vec![],
color_space: BackendColorSpace::Default,
primaries: Primaries::SRGB,
luminance: None,
};
Self {
state: state.clone(),

View file

@ -1,6 +1,10 @@
use {
crate::{
backend::{BackendEvent, ConnectorEvent, ConnectorKernelId, Mode, MonitorInfo},
backend::{
BackendColorSpace, BackendEvent, BackendTransferFunction, ConnectorEvent,
ConnectorKernelId, Mode, MonitorInfo,
},
cmm::cmm_primaries::Primaries,
ifs::wl_output::OutputId,
it::{test_backend::TestConnector, test_error::TestResult, testrun::TestRun},
video::drm::ConnectorType,
@ -48,6 +52,12 @@ async fn test(run: Rc<TestRun>) -> TestResult {
height_mm: 0,
non_desktop: false,
vrr_capable: false,
transfer_functions: vec![],
transfer_function: BackendTransferFunction::Default,
color_spaces: vec![],
color_space: BackendColorSpace::Default,
primaries: Primaries::SRGB,
luminance: None,
};
run.backend
.state

View file

@ -136,6 +136,12 @@ impl ConnectorHandler {
info.height_mm,
&output_id,
&desired_state,
info.transfer_functions.clone(),
info.transfer_function,
info.color_spaces.clone(),
info.color_space,
info.primaries,
info.luminance,
));
let schedule = Rc::new(OutputSchedule::new(
&self.state.ring,
@ -270,6 +276,9 @@ impl ConnectorHandler {
on.global.formats.set(formats);
on.global.format.set(format);
}
ConnectorEvent::ColorsChanged(bcs, btf) => {
on.update_btf_and_bcs(btf, bcs);
}
ev => unreachable!("received unexpected event {:?}", ev),
}
}

View file

@ -332,7 +332,7 @@ impl ToolClient {
self_id: s.registry,
name: s.jay_compositor.0,
interface: JayCompositor.name(),
version: s.jay_compositor.1.min(14),
version: s.jay_compositor.1.min(15),
id: id.into(),
});
self.jay_compositor.set(Some(id));

View file

@ -1,6 +1,6 @@
use {
crate::{
backend::{HardwareCursor, KeyState, Mode},
backend::{BackendColorSpace, BackendTransferFunction, HardwareCursor, KeyState, Mode},
client::ClientId,
cmm::cmm_description::ColorDescription,
cursor::KnownCursor,
@ -18,7 +18,8 @@ use {
wl_pointer::PendingScroll,
},
wl_surface::{
SurfaceSendPreferredScaleVisitor, SurfaceSendPreferredTransformVisitor,
SurfaceSendPreferredColorDescription, SurfaceSendPreferredScaleVisitor,
SurfaceSendPreferredTransformVisitor,
ext_session_lock_surface_v1::ExtSessionLockSurfaceV1,
tray::DynTrayItem,
zwlr_layer_surface_v1::{ExclusiveSize, ZwlrLayerSurfaceV1},
@ -837,6 +838,25 @@ impl OutputNode {
self.state.tree_changed();
}
pub fn update_btf_and_bcs(&self, btf: BackendTransferFunction, bcs: BackendColorSpace) {
let old_btf = self.global.btf.replace(btf);
let old_bcs = self.global.bcs.replace(bcs);
if (old_btf, old_bcs) == (btf, bcs) {
return;
}
if self.global.update_color_description() {
self.state.damage(self.global.position());
if let Some(hc) = self.hardware_cursor.get() {
self.hardware_cursor_needs_render.set(true);
hc.damage();
}
for fb in self.global.color_description_listeners.lock().values() {
fb.send_image_description_changed();
}
self.node_visit_children(&mut SurfaceSendPreferredColorDescription);
}
}
fn find_stacked_at(
&self,
stack: &LinkedList<Rc<dyn StackedNode>>,

View file

@ -1,6 +1,7 @@
pub mod sync_obj;
mod sys;
pub mod wait_for_sync_obj;
pub use consts::*;
use {
crate::{
@ -668,7 +669,7 @@ pub struct DrmPropertyDefinition {
pub ty: DrmPropertyType,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub enum DrmPropertyType {
Range {
_min: u64,
@ -688,7 +689,7 @@ pub enum DrmPropertyType {
},
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct DrmPropertyEnumValue {
pub value: u64,
pub name: BString,
@ -803,6 +804,132 @@ pub struct DrmVersion {
pub desc: BString,
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct HdrMetadata {
pub eotf: u8,
pub metadata_type: u8,
pub red: (u16, u16),
pub green: (u16, u16),
pub blue: (u16, u16),
pub white: (u16, u16),
pub max_display_mastering_luminance: u16,
pub min_display_mastering_luminance: u16,
pub max_cll: u16,
pub max_fall: u16,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
pub struct hdr_metadata_primary {
pub x: u16,
pub y: u16,
}
#[repr(C)]
#[derive(Copy, Clone)]
union hdr_output_metadata_type {
hdmi_metadata_type1: hdr_metadata_infoframe,
}
#[repr(C)]
#[derive(Copy, Clone)]
pub struct hdr_output_metadata {
metadata_type: u32,
ty: hdr_output_metadata_type,
}
impl hdr_output_metadata {
pub fn new(infoframe: hdr_metadata_infoframe) -> Self {
Self {
metadata_type: 0,
ty: hdr_output_metadata_type {
hdmi_metadata_type1: infoframe,
},
}
}
pub fn from_eotf(eotf: u8) -> Self {
Self::new(hdr_metadata_infoframe {
eotf,
metadata_type: 0,
..hdr_metadata_infoframe::default()
})
}
}
unsafe impl Pod for hdr_output_metadata {}
impl Debug for hdr_output_metadata {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut f = f.debug_struct("hdr_output_metadata");
f.field("metadata_type", &self.metadata_type);
match self.metadata_type {
0 => unsafe {
f.field("hdmi_metadata_type1", &self.ty.hdmi_metadata_type1)
.finish()
},
_ => f.finish_non_exhaustive(),
}
}
}
impl Eq for hdr_output_metadata {}
impl PartialEq for hdr_output_metadata {
fn eq(&self, other: &Self) -> bool {
if self.metadata_type != other.metadata_type {
return false;
}
match self.metadata_type {
0 => unsafe {
self.ty
.hdmi_metadata_type1
.eq(&other.ty.hdmi_metadata_type1)
},
_ => return false,
}
}
}
#[expect(dead_code)]
mod consts {
pub const HDMI_EOTF_TRADITIONAL_GAMMA_SDR: u8 = 0;
pub const HDMI_EOTF_TRADITIONAL_GAMMA_HDR: u8 = 1;
pub const HDMI_EOTF_SMPTE_ST2084: u8 = 2;
pub const HDMI_EOTF_BT_2100_HLG: u8 = 3;
pub const DRM_MODE_COLORIMETRY_DEFAULT: u64 = 0;
pub const DRM_MODE_COLORIMETRY_NO_DATA: u64 = 0;
pub const DRM_MODE_COLORIMETRY_SMPTE_170M_YCC: u64 = 1;
pub const DRM_MODE_COLORIMETRY_BT709_YCC: u64 = 2;
pub const DRM_MODE_COLORIMETRY_XVYCC_601: u64 = 3;
pub const DRM_MODE_COLORIMETRY_XVYCC_709: u64 = 4;
pub const DRM_MODE_COLORIMETRY_SYCC_601: u64 = 5;
pub const DRM_MODE_COLORIMETRY_OPYCC_601: u64 = 6;
pub const DRM_MODE_COLORIMETRY_OPRGB: u64 = 7;
pub const DRM_MODE_COLORIMETRY_BT2020_CYCC: u64 = 8;
pub const DRM_MODE_COLORIMETRY_BT2020_RGB: u64 = 9;
pub const DRM_MODE_COLORIMETRY_BT2020_YCC: u64 = 10;
pub const DRM_MODE_COLORIMETRY_DCI_P3_RGB_D65: u64 = 11;
pub const DRM_MODE_COLORIMETRY_DCI_P3_RGB_THEATER: u64 = 12;
pub const DRM_MODE_COLORIMETRY_RGB_WIDE_FIXED: u64 = 13;
pub const DRM_MODE_COLORIMETRY_RGB_WIDE_FLOAT: u64 = 14;
pub const DRM_MODE_COLORIMETRY_BT601_YCC: u64 = 15;
}
#[repr(C)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
pub struct hdr_metadata_infoframe {
pub eotf: u8,
pub metadata_type: u8,
pub display_primaries: [hdr_metadata_primary; 3],
pub white_point: hdr_metadata_primary,
pub max_display_mastering_luminance: u16,
pub min_display_mastering_luminance: u16,
pub max_cll: u16,
pub max_fall: u16,
}
impl DrmModeInfo {
pub fn create_blob(&self, master: &Rc<DrmMaster>) -> Result<PropBlob, DrmError> {
let raw = self.to_raw();

View file

@ -26,7 +26,7 @@ use {
logging::LogLevel,
status::MessageFormat,
theme::Color,
video::{Format, GfxApi, TearingMode, Transform, VrrMode},
video::{ColorSpace, Format, GfxApi, TearingMode, TransferFunction, Transform, VrrMode},
xwayland::XScalingMode,
},
std::{
@ -220,6 +220,8 @@ pub struct Output {
pub vrr: Option<Vrr>,
pub tearing: Option<Tearing>,
pub format: Option<Format>,
pub color_space: Option<ColorSpace>,
pub transfer_function: Option<TransferFunction>,
}
#[derive(Debug, Clone)]

View file

@ -19,7 +19,7 @@ use {
},
},
indexmap::IndexMap,
jay_config::video::Transform,
jay_config::video::{ColorSpace, TransferFunction, Transform},
thiserror::Error,
};
@ -49,8 +49,11 @@ impl Parser for OutputParser<'_> {
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.cx, span, table);
let (name, match_val, x, y, scale, transform, mode, vrr_val, tearing_val, format_val) = ext
.extract((
let (
(name, match_val, x, y, scale, transform, mode, vrr_val, tearing_val, format_val),
(color_space, transfer_function),
) = ext.extract((
(
opt(str("name")),
val("match"),
recover(opt(s32("x"))),
@ -61,7 +64,12 @@ impl Parser for OutputParser<'_> {
opt(val("vrr")),
opt(val("tearing")),
opt(val("format")),
))?;
),
(
recover(opt(str("color-space"))),
recover(opt(str("transfer-function"))),
),
))?;
let transform = match transform {
None => None,
Some(t) => match t.value {
@ -79,6 +87,36 @@ impl Parser for OutputParser<'_> {
}
},
};
let color_space = match color_space {
None => None,
Some(cs) => match cs.value {
"default" => Some(ColorSpace::DEFAULT),
"bt2020" => Some(ColorSpace::BT2020),
_ => {
log::warn!(
"Unknown color space {}: {}",
cs.value,
self.cx.error3(cs.span)
);
None
}
},
};
let transfer_function = match transfer_function {
None => None,
Some(tf) => match tf.value {
"default" => Some(TransferFunction::DEFAULT),
"pq" => Some(TransferFunction::PQ),
_ => {
log::warn!(
"Unknown transfer function {}: {}",
tf.value,
self.cx.error3(tf.span)
);
None
}
},
};
let mode = match mode {
Some(mode) => match mode.parse(&mut ModeParser(self.cx)) {
Ok(m) => Some(m),
@ -144,6 +182,8 @@ impl Parser for OutputParser<'_> {
vrr,
tearing,
format,
color_space,
transfer_function,
})
}
}

View file

@ -30,10 +30,10 @@ use {
switch_to_vt,
theme::{reset_colors, reset_font, reset_sizes, set_font},
video::{
Connector, DrmDevice, connectors, drm_devices, on_connector_connected,
on_connector_disconnected, on_graphics_initialized, on_new_connector,
on_new_drm_device, set_direct_scanout_enabled, set_gfx_api, set_tearing_mode,
set_vrr_cursor_hz, set_vrr_mode,
ColorSpace, Connector, DrmDevice, TransferFunction, connectors, drm_devices,
on_connector_connected, on_connector_disconnected, on_graphics_initialized,
on_new_connector, on_new_drm_device, set_direct_scanout_enabled, set_gfx_api,
set_tearing_mode, set_vrr_cursor_hz, set_vrr_mode,
},
xwayland::set_x_scaling_mode,
},
@ -588,6 +588,11 @@ impl Output {
if let Some(format) = self.format {
c.set_format(format);
}
if self.color_space.is_some() || self.transfer_function.is_some() {
let cs = self.color_space.unwrap_or(ColorSpace::DEFAULT);
let tf = self.transfer_function.unwrap_or(TransferFunction::DEFAULT);
c.set_colors(cs, tf);
}
}
}

View file

@ -438,6 +438,14 @@
},
"required": []
},
"ColorSpace": {
"type": "string",
"description": "The color space of an output.\n",
"enum": [
"default",
"bt2020"
]
},
"ComplexShortcut": {
"description": "Describes a complex shortcut.\n\n- Example:\n\n ```toml\n [complex-shortcuts.XF86AudioRaiseVolume]\n mod-mask = \"alt\"\n action = { type = \"exec\", exec = [\"pactl\", \"set-sink-volume\", \"0\", \"+10%\"] }\n ```\n",
"type": "object",
@ -1152,6 +1160,14 @@
"format": {
"description": "Configures the framebuffer format of this output.\n\nBy default, the format is `xrgb8888`.\n\n- Example:\n\n ```toml\n [[outputs]]\n match.serial-number = \"33K03894SL0\"\n format = \"rgb565\"\n ```\n",
"$ref": "#/$defs/Format"
},
"color-space": {
"description": "The color space of the output.\n",
"$ref": "#/$defs/ColorSpace"
},
"transfer-function": {
"description": "The transfer function of the output.\n",
"$ref": "#/$defs/TransferFunction"
}
},
"required": [
@ -1372,6 +1388,14 @@
},
"required": []
},
"TransferFunction": {
"type": "string",
"description": "The transfer function of an output.\n",
"enum": [
"default",
"pq"
]
},
"Transform": {
"type": "string",
"description": "An output transformation.",

View file

@ -617,6 +617,25 @@ The table has the following fields:
The value of this field should be a boolean.
<a name="types-ColorSpace"></a>
### `ColorSpace`
The color space of an output.
Values of this type should be strings.
The string should have one of the following values:
- `default`:
The default color space (usually sRGB).
- `bt2020`:
The BT.2020 color space.
<a name="types-ComplexShortcut"></a>
### `ComplexShortcut`
@ -2530,6 +2549,18 @@ The table has the following fields:
The value of this field should be a [Format](#types-Format).
- `color-space` (optional):
The color space of the output.
The value of this field should be a [ColorSpace](#types-ColorSpace).
- `transfer-function` (optional):
The transfer function of the output.
The value of this field should be a [TransferFunction](#types-TransferFunction).
<a name="types-OutputMatch"></a>
### `OutputMatch`
@ -3050,6 +3081,25 @@ The table has the following fields:
The value of this field should be a string.
<a name="types-TransferFunction"></a>
### `TransferFunction`
The transfer function of an output.
Values of this type should be strings.
The string should have one of the following values:
- `default`:
The default transfer function (usually sRGB).
- `pq`:
The PQ transfer function.
<a name="types-Transform"></a>
### `Transform`

View file

@ -1631,6 +1631,16 @@ Output:
match.serial-number = "33K03894SL0"
format = "rgb565"
```
color-space:
ref: ColorSpace
required: false
description: |
The color space of the output.
transfer-function:
ref: TransferFunction
required: false
description: |
The transfer function of the output.
Transform:
@ -2762,3 +2772,25 @@ ColorManagement:
The default is `false`.
kind: boolean
required: false
ColorSpace:
description: |
The color space of an output.
kind: string
values:
- value: default
description: The default color space (usually sRGB).
- value: bt2020
description: The BT.2020 color space.
TransferFunction:
description: |
The transfer function of an output.
kind: string
values:
- value: default
description: The default transfer function (usually sRGB).
- value: pq
description: The PQ transfer function.

View file

@ -80,6 +80,12 @@ request set_flip_margin (since = 10) {
margin_ns: pod(u64),
}
request set_colors (since = 15) {
output: str,
color_space: str,
transfer_function: str,
}
# events
event global {
@ -160,3 +166,19 @@ event fb_format (since = 8) {
event flip_margin (since = 10) {
margin_ns: pod(u64),
}
event supported_color_space (since = 15) {
color_space: str,
}
event current_color_space (since = 15) {
color_space: str,
}
event supported_transfer_function (since = 15) {
transfer_function: str,
}
event current_transfer_function (since = 15) {
transfer_function: str,
}