diff options
| -rw-r--r-- | docs/wiki/Configuration:-Outputs.md | 40 | ||||
| -rw-r--r-- | niri-config/src/lib.rs | 79 | ||||
| -rw-r--r-- | niri-config/src/output.rs | 286 | ||||
| -rw-r--r-- | niri-ipc/src/lib.rs | 172 | ||||
| -rw-r--r-- | src/backend/headless.rs | 1 | ||||
| -rw-r--r-- | src/backend/tty.rs | 394 | ||||
| -rw-r--r-- | src/backend/winit.rs | 1 | ||||
| -rw-r--r-- | src/dbus/mutter_display_config.rs | 11 | ||||
| -rw-r--r-- | src/ipc/client.rs | 34 | ||||
| -rw-r--r-- | src/ipc/server.rs | 2 | ||||
| -rw-r--r-- | src/niri.rs | 40 | ||||
| -rw-r--r-- | src/protocols/output_management.rs | 88 |
12 files changed, 1080 insertions, 68 deletions
diff --git a/docs/wiki/Configuration:-Outputs.md b/docs/wiki/Configuration:-Outputs.md index b2b394bd..ecdee105 100644 --- a/docs/wiki/Configuration:-Outputs.md +++ b/docs/wiki/Configuration:-Outputs.md @@ -27,6 +27,10 @@ output "eDP-1" { layout { // ...layout settings for eDP-1... } + + // Custom modes. Caution: may damage your display. + // mode custom=true "1920x1080@100" + // modeline 173.00 1920 2048 2248 2576 1080 1083 1088 1120 "-hsync" "+vsync" } output "HDMI-A-1" { @@ -86,6 +90,42 @@ output "eDP-1" { } ``` +#### `mode custom=true` + +<sup>Since: next release</sup> + +You can configure a custom mode (not offered by the monitor) by setting `custom=true`. +In this case, the refresh rate is mandatory. + +> [!CAUTION] +> Custom modes may damage your monitor, especially if it's a CRT. +> Follow the maximum supported limits in your monitor's instructions. + +```kdl +// Use a custom mode for this display. +output "HDMI-A-1" { + mode custom=true "2560x1440@143.912" +} +``` + +### `modeline` + +<sup>Since: next release</sup> + +Directly configures the monitor's mode via a modeline, overriding any configured `mode`. +The modeline can be calculated via utilities such as [cvt](https://man.archlinux.org/man/cvt.1.en) or [gtf](https://man.archlinux.org/man/gtf.1.en). + +> [!CAUTION] +> Out of spec modelines may damage your monitor, especially if it's a CRT. +> Follow the maximum supported limits in your monitor's instructions. + +```kdl +// Use a modeline for this display. +output "eDP-3" { + modeline 173.00 1920 2048 2248 2576 1080 1083 1088 1120 "-hsync" "+vsync" +} +``` + ### `scale` Set the scale of the monitor. diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index e939f0c3..dda7dfd6 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -663,6 +663,14 @@ mod tests { } } + output "eDP-2" { + mode custom=true "1920x1080@144" + } + + output "eDP-3" { + modeline 173.00 1920 2048 2248 2576 1080 1083 1088 1120 "-hsync" "+vsync" + } + layout { focus-ring { width 5 @@ -1035,14 +1043,18 @@ mod tests { }, ), mode: Some( - ConfiguredMode { - width: 1920, - height: 1080, - refresh: Some( - 144.0, - ), + Mode { + custom: false, + mode: ConfiguredMode { + width: 1920, + height: 1080, + refresh: Some( + 144.0, + ), + }, }, ), + modeline: None, variable_refresh_rate: Some( Vrr { on_demand: true, @@ -1069,6 +1081,61 @@ mod tests { ), layout: None, }, + Output { + off: false, + name: "eDP-2", + scale: None, + transform: Normal, + position: None, + mode: Some( + Mode { + custom: true, + mode: ConfiguredMode { + width: 1920, + height: 1080, + refresh: Some( + 144.0, + ), + }, + }, + ), + modeline: None, + variable_refresh_rate: None, + focus_at_startup: false, + background_color: None, + backdrop_color: None, + hot_corners: None, + layout: None, + }, + Output { + off: false, + name: "eDP-3", + scale: None, + transform: Normal, + position: None, + mode: None, + modeline: Some( + Modeline { + clock: 173.0, + hdisplay: 1920, + hsync_start: 2048, + hsync_end: 2248, + htotal: 2576, + vdisplay: 1080, + vsync_start: 1083, + vsync_end: 1088, + vtotal: 1120, + hsync_polarity: NHSync, + vsync_polarity: PVSync, + }, + ), + variable_refresh_rate: None, + focus_at_startup: false, + background_color: None, + backdrop_color: None, + hot_corners: None, + layout: None, + }, ], ), spawn_at_startup: [ diff --git a/niri-config/src/output.rs b/niri-config/src/output.rs index 62c14705..5d6565f5 100644 --- a/niri-config/src/output.rs +++ b/niri-config/src/output.rs @@ -1,4 +1,11 @@ -use niri_ipc::{ConfiguredMode, Transform}; +use std::str::FromStr; + +use knuffel::ast::SpannedNode; +use knuffel::decode::Context; +use knuffel::errors::DecodeError; +use knuffel::traits::ErrorSpan; +use knuffel::Decode; +use niri_ipc::{ConfiguredMode, HSyncPolarity, Transform, VSyncPolarity}; use crate::gestures::HotCorners; use crate::{Color, FloatOrInt, LayoutPart}; @@ -6,6 +13,40 @@ use crate::{Color, FloatOrInt, LayoutPart}; #[derive(Debug, Default, Clone, PartialEq)] pub struct Outputs(pub Vec<Output>); +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Mode { + pub custom: bool, + pub mode: ConfiguredMode, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Modeline { + /// The rate at which pixels are drawn in MHz. + pub clock: f64, + /// Horizontal active pixels. + pub hdisplay: u16, + /// Horizontal sync pulse start position in pixels. + pub hsync_start: u16, + /// Horizontal sync pulse end position in pixels. + pub hsync_end: u16, + /// Total horizontal number of pixels before resetting the horizontal drawing position to + /// zero. + pub htotal: u16, + + /// Vertical active pixels. + pub vdisplay: u16, + /// Vertical sync pulse start position in pixels. + pub vsync_start: u16, + /// Vertical sync pulse end position in pixels. + pub vsync_end: u16, + /// Total vertical number of pixels before resetting the vertical drawing position to zero. + pub vtotal: u16, + /// Horizontal sync polarity: "+hsync" or "-hsync". + pub hsync_polarity: niri_ipc::HSyncPolarity, + /// Vertical sync polarity: "+vsync" or "-vsync". + pub vsync_polarity: niri_ipc::VSyncPolarity, +} + #[derive(knuffel::Decode, Debug, Clone, PartialEq)] pub struct Output { #[knuffel(child)] @@ -18,8 +59,10 @@ pub struct Output { pub transform: Transform, #[knuffel(child)] pub position: Option<Position>, - #[knuffel(child, unwrap(argument, str))] - pub mode: Option<ConfiguredMode>, + #[knuffel(child)] + pub mode: Option<Mode>, + #[knuffel(child)] + pub modeline: Option<Modeline>, #[knuffel(child)] pub variable_refresh_rate: Option<Vrr>, #[knuffel(child)] @@ -59,6 +102,7 @@ impl Default for Output { transform: Transform::Normal, position: None, mode: None, + modeline: None, variable_refresh_rate: None, background_color: None, backdrop_color: None, @@ -213,6 +257,242 @@ impl OutputName { } } +impl<S: ErrorSpan> knuffel::Decode<S> for Mode { + fn decode_node(node: &SpannedNode<S>, ctx: &mut Context<S>) -> Result<Self, DecodeError<S>> { + if let Some(type_name) = &node.type_name { + ctx.emit_error(DecodeError::unexpected( + type_name, + "type name", + "no type name expected for this node", + )); + } + + for child in node.children() { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + format!("unexpected node `{}`", child.node_name.escape_default()), + )); + } + + let mut custom: Option<bool> = None; + for (name, val) in &node.properties { + match &***name { + "custom" => { + if custom.is_some() { + ctx.emit_error(DecodeError::unexpected( + name, + "property", + "unexpected duplicate property `custom`", + )) + } + custom = Some(knuffel::traits::DecodeScalar::decode(val, ctx)?) + } + name_str => ctx.emit_error(DecodeError::unexpected( + node, + "property", + format!("unexpected property `{}`", name_str.escape_default()), + )), + } + } + let custom = custom.unwrap_or(false); + + let mut arguments = node.arguments.iter(); + let mode = if let Some(mode_str) = arguments.next() { + let temp_mode: String = knuffel::traits::DecodeScalar::decode(mode_str, ctx)?; + + let res = ConfiguredMode::from_str(temp_mode.as_str()).and_then(|mode| { + if custom { + if mode.refresh.is_none() { + return Err("no refresh rate found; required for custom mode"); + } else if let Some(refresh) = mode.refresh { + if refresh <= 0. { + return Err("custom mode refresh rate must be > 0"); + } + } + } + Ok(mode) + }); + res.map_err(|err_msg| DecodeError::conversion(&mode_str.literal, err_msg))? + } else { + return Err(DecodeError::missing(node, "argument `mode` is required")); + }; + + if let Some(surplus) = arguments.next() { + ctx.emit_error(DecodeError::unexpected( + &surplus.literal, + "argument", + "unexpected argument", + )) + } + + Ok(Mode { custom, mode }) + } +} + +macro_rules! ensure { + ($cond:expr, $ctx:expr, $span:expr, $fmt:literal $($arg:tt)* ) => { + if !$cond { + $ctx.emit_error(DecodeError::Conversion { + source: format!($fmt $($arg)*).into(), + span: $span.literal.span().clone() + }); + } + }; +} + +impl<S: ErrorSpan> Decode<S> for Modeline { + fn decode_node(node: &SpannedNode<S>, ctx: &mut Context<S>) -> Result<Self, DecodeError<S>> { + if let Some(type_name) = &node.type_name { + ctx.emit_error(DecodeError::unexpected( + type_name, + "type name", + "no type name expected for this node", + )); + } + + for child in node.children() { + ctx.emit_error(DecodeError::unexpected( + child, + "node", + format!("unexpected node `{}`", child.node_name.escape_default()), + )); + } + + for span in node.properties.keys() { + ctx.emit_error(DecodeError::unexpected( + span, + "node", + format!("unexpected node `{}`", span.escape_default()), + )); + } + + let mut arguments = node.arguments.iter(); + + macro_rules! m_required { + // This could be one identifier if macro_metavar_expr_concat stabilizes + ($field:ident, $value_field:ident) => { + let $value_field = arguments.next().ok_or_else(|| { + DecodeError::missing(node, format!("missing {} argument", stringify!($value))) + })?; + let $field = knuffel::traits::DecodeScalar::decode($value_field, ctx)?; + }; + } + + m_required!(clock, clock_value); + m_required!(hdisplay, hdisplay_value); + m_required!(hsync_start, hsync_start_value); + m_required!(hsync_end, hsync_end_value); + m_required!(htotal, htotal_value); + m_required!(vdisplay, vdisplay_value); + m_required!(vsync_start, vsync_start_value); + m_required!(vsync_end, vsync_end_value); + m_required!(vtotal, vtotal_value); + m_required!(hsync_polarity, hsync_polarity_value); + let hsync_polarity = + HSyncPolarity::from_str(String::as_str(&hsync_polarity)).map_err(|msg| { + DecodeError::Conversion { + span: hsync_polarity_value.literal.span().clone(), + source: msg.into(), + } + })?; + + m_required!(vsync_polarity, vsync_polarity_value); + let vsync_polarity = + VSyncPolarity::from_str(String::as_str(&vsync_polarity)).map_err(|msg| { + DecodeError::Conversion { + span: vsync_polarity_value.literal.span().clone(), + source: msg.into(), + } + })?; + + ensure!( + hdisplay < hsync_start, + ctx, + hdisplay_value, + "hdisplay {} must be < hsync_start {}", + hdisplay, + hsync_start + ); + ensure!( + hsync_start < hsync_end, + ctx, + hsync_start_value, + "hsync_start {} must be < hsync_end {}", + hsync_start, + hsync_end, + ); + ensure!( + hsync_end < htotal, + ctx, + hsync_end_value, + "hsync_end {} must be < htotal {}", + hsync_end, + htotal, + ); + ensure!( + 0u16 < htotal, + ctx, + htotal_value, + "htotal {} must be > 0", + htotal + ); + ensure!( + vdisplay < vsync_start, + ctx, + vdisplay_value, + "vdisplay {} must be < vsync_start {}", + vdisplay, + vsync_start, + ); + ensure!( + vsync_start < vsync_end, + ctx, + vsync_start_value, + "vsync_start {} must be < vsync_end {}", + vsync_start, + vsync_end, + ); + ensure!( + vsync_end < vtotal, + ctx, + vsync_end_value, + "vsync_end {} must be < vtotal {}", + vsync_end, + vtotal, + ); + ensure!( + 0u16 < vtotal, + ctx, + vtotal_value, + "vtotal {} must be > 0", + vtotal + ); + + if let Some(extra) = arguments.next() { + ctx.emit_error(DecodeError::unexpected( + &extra.literal, + "argument", + "unexpected argument, all possible arguments were already provided", + )) + } + + Ok(Modeline { + clock, + hdisplay, + hsync_start, + hsync_end, + htotal, + vdisplay, + vsync_start, + vsync_end, + vtotal, + hsync_polarity, + vsync_polarity, + }) + } +} + #[cfg(test)] mod tests { use insta::assert_debug_snapshot; diff --git a/niri-ipc/src/lib.rs b/niri-ipc/src/lib.rs index afe9a6d2..4a2e8996 100644 --- a/niri-ipc/src/lib.rs +++ b/niri-ipc/src/lib.rs @@ -1000,6 +1000,51 @@ pub enum OutputAction { #[cfg_attr(feature = "clap", arg())] mode: ModeToSet, }, + /// Set a custom output mode. + CustomMode { + /// Custom mode to set. + #[cfg_attr(feature = "clap", arg())] + mode: ConfiguredMode, + }, + /// Set a custom VESA CVT modeline. + #[cfg_attr(feature = "clap", arg())] + Modeline { + /// The rate at which pixels are drawn in MHz. + #[cfg_attr(feature = "clap", arg())] + clock: f64, + /// Horizontal active pixels. + #[cfg_attr(feature = "clap", arg())] + hdisplay: u16, + /// Horizontal sync pulse start position in pixels. + #[cfg_attr(feature = "clap", arg())] + hsync_start: u16, + /// Horizontal sync pulse end position in pixels. + #[cfg_attr(feature = "clap", arg())] + hsync_end: u16, + /// Total horizontal number of pixels before resetting the horizontal drawing position to + /// zero. + #[cfg_attr(feature = "clap", arg())] + htotal: u16, + + /// Vertical active pixels. + #[cfg_attr(feature = "clap", arg())] + vdisplay: u16, + /// Vertical sync pulse start position in pixels. + #[cfg_attr(feature = "clap", arg())] + vsync_start: u16, + /// Vertical sync pulse end position in pixels. + #[cfg_attr(feature = "clap", arg())] + vsync_end: u16, + /// Total vertical number of pixels before resetting the vertical drawing position to zero. + #[cfg_attr(feature = "clap", arg())] + vtotal: u16, + /// Horizontal sync polarity: "+hsync" or "-hsync". + #[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))] + hsync_polarity: HSyncPolarity, + /// Vertical sync polarity: "+vsync" or "-vsync". + #[cfg_attr(feature = "clap", arg(allow_hyphen_values = true))] + vsync_polarity: VSyncPolarity, + }, /// Set the output scale. Scale { /// Scale factor to set, or "auto" for automatic selection. @@ -1048,6 +1093,26 @@ pub struct ConfiguredMode { pub refresh: Option<f64>, } +/// Modeline horizontal syncing polarity. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum HSyncPolarity { + /// Positive polarity. + PHSync, + /// Negative polarity. + NHSync, +} + +/// Modeline vertical syncing polarity. +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] +pub enum VSyncPolarity { + /// Positive polarity. + PVSync, + /// Negative polarity. + NVSync, +} + /// Output scale to set. #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))] @@ -1125,6 +1190,8 @@ pub struct Output { /// /// `None` if the output is disabled. pub current_mode: Option<usize>, + /// Whether the current_mode is a custom mode. + pub is_custom_mode: bool, /// Whether the output supports variable refresh rate. pub vrr_supported: bool, /// Whether variable refresh rate is enabled on the output. @@ -1680,6 +1747,30 @@ impl FromStr for ConfiguredMode { } } +impl FromStr for HSyncPolarity { + type Err = &'static str; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "+hsync" => Ok(Self::PHSync), + "-hsync" => Ok(Self::NHSync), + _ => Err(r#"invalid horizontal sync polarity, can be "+hsync" or "-hsync"#), + } + } +} + +impl FromStr for VSyncPolarity { + type Err = &'static str; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "+vsync" => Ok(Self::PVSync), + "-vsync" => Ok(Self::NVSync), + _ => Err(r#"invalid vertical sync polarity, can be "+vsync" or "-vsync"#), + } + } +} + impl FromStr for ScaleToSet { type Err = &'static str; @@ -1693,6 +1784,87 @@ impl FromStr for ScaleToSet { } } +macro_rules! ensure { + ($cond:expr, $fmt:literal $($arg:tt)* ) => { + if !$cond { + return Err(format!($fmt $($arg)*)); + } + }; +} + +impl OutputAction { + /// Validates some required constraints on the modeline and custom mode. + pub fn validate(&self) -> Result<(), String> { + match self { + OutputAction::Modeline { + hdisplay, + hsync_start, + hsync_end, + htotal, + vdisplay, + vsync_start, + vsync_end, + vtotal, + .. + } => { + ensure!( + hdisplay < hsync_start, + "hdisplay {} must be < hsync_start {}", + hdisplay, + hsync_start + ); + ensure!( + hsync_start < hsync_end, + "hsync_start {} must be < hsync_end {}", + hsync_start, + hsync_end + ); + ensure!( + hsync_end < htotal, + "hsync_end {} must be < htotal {}", + hsync_end, + htotal + ); + ensure!(0 < *htotal, "htotal {} must be > 0", htotal); + ensure!( + vdisplay < vsync_start, + "vdisplay {} must be < vsync_start {}", + vdisplay, + vsync_start + ); + ensure!( + vsync_start < vsync_end, + "vsync_start {} must be < vsync_end {}", + vsync_start, + vsync_end + ); + ensure!( + vsync_end < vtotal, + "vsync_end {} must be < vtotal {}", + vsync_end, + vtotal + ); + ensure!(0 < *vtotal, "vtotal {} must be > 0", vtotal); + Ok(()) + } + OutputAction::CustomMode { + mode: ConfiguredMode { refresh, .. }, + } => { + if refresh.is_none() { + return Err("refresh rate is required for custom modes".to_string()); + } + if let Some(refresh) = refresh { + if *refresh <= 0. { + return Err(format!("custom mode refresh rate {refresh} must be > 0")); + } + } + Ok(()) + } + _ => Ok(()), + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/backend/headless.rs b/src/backend/headless.rs index 51381168..8b5ee827 100644 --- a/src/backend/headless.rs +++ b/src/backend/headless.rs @@ -105,6 +105,7 @@ impl Headless { is_preferred: true, }], current_mode: Some(0), + is_custom_mode: true, vrr_supported: false, vrr_enabled: false, logical: Some(logical_output(&output)), diff --git a/src/backend/tty.rs b/src/backend/tty.rs index 530280ab..bc638005 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -12,8 +12,11 @@ use std::{io, mem}; use anyhow::{anyhow, bail, ensure, Context}; use bytemuck::cast_slice_mut; +use drm_ffi::drm_mode_modeinfo; use libc::dev_t; +use niri_config::output::Modeline; use niri_config::{Config, OutputName}; +use niri_ipc::{HSyncPolarity, VSyncPolarity}; use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::allocator::format::FormatSet; use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}; @@ -907,21 +910,35 @@ impl Tty { trace!("{m:?}"); } - let (mode, fallback) = - pick_mode(&connector, config.mode).ok_or_else(|| anyhow!("no mode"))?; + let mut mode = None; + if let Some(modeline) = &config.modeline { + match calculate_drm_mode_from_modeline(modeline) { + Ok(x) => mode = Some(x), + Err(err) => { + warn!("invalid custom modeline; falling back to advertised modes: {err:?}"); + } + } + } + + let (mode, fallback) = match mode { + Some(x) => (x, false), + None => pick_mode(&connector, config.mode).ok_or_else(|| anyhow!("no mode"))?, + }; + if fallback { let target = config.mode.unwrap(); warn!( "configured mode {}x{}{} could not be found, falling back to preferred", - target.width, - target.height, - if let Some(refresh) = target.refresh { + target.mode.width, + target.mode.height, + if let Some(refresh) = target.mode.refresh { format!("@{refresh}") } else { String::new() }, ); } + debug!("picking mode: {mode:?}"); if let Ok(props) = ConnectorProperties::try_new(&device.drm, connector.handle()) { @@ -1711,8 +1728,9 @@ impl Tty { let surface = device.surfaces.get(&crtc); let current_crtc_mode = surface.map(|surface| surface.compositor.pending_mode()); let mut current_mode = None; + let mut is_custom_mode = false; - let modes = connector + let mut modes: Vec<niri_ipc::Mode> = connector .modes() .iter() .filter(|m| !m.flags().contains(ModeFlags::INTERLACE)) @@ -1732,6 +1750,21 @@ impl Tty { .collect(); if let Some(crtc_mode) = current_crtc_mode { + // Custom mode + if crtc_mode.mode_type().contains(ModeTypeFlags::USERDEF) { + modes.insert( + 0, + niri_ipc::Mode { + width: crtc_mode.size().0, + height: crtc_mode.size().1, + refresh_rate: Mode::from(crtc_mode).refresh as u32, + is_preferred: false, + }, + ); + current_mode = Some(0); + is_custom_mode = true; + } + if current_mode.is_none() { if crtc_mode.flags().contains(ModeFlags::INTERLACE) { warn!("connector mode list missing current mode (interlaced)"); @@ -1776,6 +1809,7 @@ impl Tty { physical_size, modes, current_mode, + is_custom_mode, vrr_supported, vrr_enabled, logical, @@ -1962,9 +1996,29 @@ impl Tty { continue; }; - let Some((mode, fallback)) = pick_mode(connector, config.mode) else { - warn!("couldn't pick mode for enabled connector"); - continue; + let mut mode = None; + if let Some(modeline) = &config.modeline { + match calculate_drm_mode_from_modeline(modeline) { + Ok(x) => mode = Some(x), + Err(err) => { + warn!( + "output {:?}: invalid custom modeline; \ + falling back to advertised modes: {err:?}", + surface.name.connector + ); + } + } + } + + let (mode, fallback) = match mode { + Some(x) => (x, false), + None => match pick_mode(connector, config.mode) { + Some(result) => result, + None => { + warn!("couldn't pick mode for enabled connector"); + continue; + } + }, }; let change_mode = surface.compositor.pending_mode() != mode; @@ -2017,9 +2071,9 @@ impl Tty { "output {:?}: configured mode {}x{}{} could not be found, \ falling back to preferred", surface.name.connector, - target.width, - target.height, - if let Some(refresh) = target.refresh { + target.mode.width, + target.mode.height, + if let Some(refresh) = target.mode.refresh { format!("@{refresh}") } else { String::new() @@ -2517,18 +2571,188 @@ fn queue_estimated_vblank_timer( output_state.redraw_state = RedrawState::WaitingForEstimatedVBlank(token); } +pub fn calculate_drm_mode_from_modeline(modeline: &Modeline) -> anyhow::Result<DrmMode> { + ensure!( + modeline.hdisplay < modeline.hsync_start, + "hdisplay {} must be < hsync_start {}", + modeline.hdisplay, + modeline.hsync_start + ); + ensure!( + modeline.hsync_start < modeline.hsync_end, + "hsync_start {} must be < hsync_end {}", + modeline.hsync_start, + modeline.hsync_end + ); + ensure!( + modeline.hsync_end < modeline.htotal, + "hsync_end {} must be < htotal {}", + modeline.hsync_end, + modeline.htotal + ); + ensure!( + modeline.vdisplay < modeline.vsync_start, + "vdisplay {} must be < vsync_start {}", + modeline.vdisplay, + modeline.vsync_start + ); + ensure!( + modeline.vsync_start < modeline.vsync_end, + "vsync_start {} must be < vsync_end {}", + modeline.vsync_start, + modeline.vsync_end + ); + ensure!( + modeline.vsync_end < modeline.vtotal, + "vsync_end {} must be < vtotal {}", + modeline.vsync_end, + modeline.vtotal + ); + + let pixel_clock_kilo_hertz = modeline.clock * 1000.0; + // Calculated as documented in the CVT 1.2 standard: + // https://app.box.com/s/vcocw3z73ta09txiskj7cnk6289j356b/file/93518784646 + let vrefresh_hertz = (pixel_clock_kilo_hertz * 1000.0) + / (modeline.htotal as u64 * modeline.vtotal as u64) as f64; + ensure!( + vrefresh_hertz.is_finite(), + "calculated refresh rate is not finite" + ); + let vrefresh_rounded = vrefresh_hertz.round() as u32; + + let flags = match modeline.hsync_polarity { + HSyncPolarity::PHSync => ModeFlags::PHSYNC, + HSyncPolarity::NHSync => ModeFlags::NHSYNC, + } | match modeline.vsync_polarity { + VSyncPolarity::PVSync => ModeFlags::PVSYNC, + VSyncPolarity::NVSync => ModeFlags::NVSYNC, + }; + + let mode_name = format!( + "{}x{}@{:.2}", + modeline.hdisplay, modeline.vdisplay, vrefresh_hertz + ); + let name = modeinfo_name_slice_from_string(&mode_name); + + // https://www.kernel.org/doc/html/v6.17/gpu/drm-uapi.html#c.drm_mode_modeinfo + Ok(DrmMode::from(drm_mode_modeinfo { + clock: pixel_clock_kilo_hertz.round() as u32, + hdisplay: modeline.hdisplay, + hsync_start: modeline.hsync_start, + hsync_end: modeline.hsync_end, + htotal: modeline.htotal, + vdisplay: modeline.vdisplay, + vsync_start: modeline.vsync_start, + vsync_end: modeline.vsync_end, + vtotal: modeline.vtotal, + vrefresh: vrefresh_rounded, + flags: flags.bits(), + name, + // Defaults + type_: drm_ffi::DRM_MODE_TYPE_USERDEF, + hskew: 0, + vscan: 0, + })) +} + +pub fn calculate_mode_cvt(width: u16, height: u16, refresh: f64) -> DrmMode { + // Cross-checked with sway's implementation: + // https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/22528542970687720556035790212df8d9bb30bb/backend/drm/util.c#L251 + + let options = libdisplay_info::cvt::Options { + red_blank_ver: libdisplay_info::cvt::ReducedBlankingVersion::None, + h_pixels: width as i32, + v_lines: height as i32, + ip_freq_rqd: refresh, + + // Defaults + video_opt: false, + vblank: 0f64, + additional_hblank: 0, + early_vsync_rqd: false, + int_rqd: false, + margins_rqd: false, + }; + let cvt_timing = libdisplay_info::cvt::Timing::compute(options); + |
