aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/wiki/Configuration:-Outputs.md40
-rw-r--r--niri-config/src/lib.rs79
-rw-r--r--niri-config/src/output.rs286
-rw-r--r--niri-ipc/src/lib.rs172
-rw-r--r--src/backend/headless.rs1
-rw-r--r--src/backend/tty.rs394
-rw-r--r--src/backend/winit.rs1
-rw-r--r--src/dbus/mutter_display_config.rs11
-rw-r--r--src/ipc/client.rs34
-rw-r--r--src/ipc/server.rs2
-rw-r--r--src/niri.rs40
-rw-r--r--src/protocols/output_management.rs88
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);
+