mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Add support for custom modes and modelines. (#2479)
* Implement custom modes and modelines Co-authored-by: ToxicMushroom <32853531+ToxicMushroom@users.noreply.github.com> * fixes * refactor mode and modeline kdl parsers. * add IPC parse checks * refactor: address feedback * fix: add missing > 0 refresh rate check * move things around * fixes * wiki fixes --------- Co-authored-by: Christian Meissl <meissl.christian@gmail.com> Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
+73
-6
@@ -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: [
|
||||
|
||||
+283
-3
@@ -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;
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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)),
|
||||
|
||||
+379
-15
@@ -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);
|
||||
|
||||
let hsync_start = width + cvt_timing.h_front_porch as u16;
|
||||
let vsync_start = (cvt_timing.v_lines_rnd + cvt_timing.v_front_porch) as u16;
|
||||
let hsync_end = hsync_start + cvt_timing.h_sync as u16;
|
||||
let vsync_end = vsync_start + cvt_timing.v_sync as u16;
|
||||
|
||||
let htotal = hsync_end + cvt_timing.h_back_porch as u16;
|
||||
let vtotal = vsync_end + cvt_timing.v_back_porch as u16;
|
||||
|
||||
let clock = f64::round(cvt_timing.act_pixel_freq * 1000f64) as u32;
|
||||
let vrefresh = f64::round(cvt_timing.act_frame_rate) as u32;
|
||||
|
||||
let flags = drm_ffi::DRM_MODE_FLAG_NHSYNC | drm_ffi::DRM_MODE_FLAG_PVSYNC;
|
||||
|
||||
let mode_name = format!("{width}x{height}@{:.2}", cvt_timing.act_frame_rate);
|
||||
let name = modeinfo_name_slice_from_string(&mode_name);
|
||||
|
||||
let drm_ffi_mode = drm_ffi::drm_sys::drm_mode_modeinfo {
|
||||
clock,
|
||||
|
||||
hdisplay: width,
|
||||
hsync_start,
|
||||
hsync_end,
|
||||
htotal,
|
||||
|
||||
vdisplay: height,
|
||||
vsync_start,
|
||||
vsync_end,
|
||||
vtotal,
|
||||
|
||||
vrefresh,
|
||||
|
||||
flags,
|
||||
type_: drm_ffi::DRM_MODE_TYPE_USERDEF,
|
||||
name,
|
||||
|
||||
// Defaults
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
};
|
||||
|
||||
DrmMode::from(drm_ffi_mode)
|
||||
}
|
||||
|
||||
// Returns a c-string of maximally 31 Rust string chars + null terminator. Excess characters are
|
||||
// dropped.
|
||||
fn modeinfo_name_slice_from_string(mode_name: &str) -> [core::ffi::c_char; 32] {
|
||||
let mut name: [core::ffi::c_char; 32] = [0; 32];
|
||||
|
||||
for (a, b) in zip(&mut name[..31], mode_name.as_bytes()) {
|
||||
*a = *b as i8;
|
||||
}
|
||||
|
||||
name
|
||||
}
|
||||
|
||||
fn pick_mode(
|
||||
connector: &connector::Info,
|
||||
target: Option<niri_ipc::ConfiguredMode>,
|
||||
target: Option<niri_config::output::Mode>,
|
||||
) -> Option<(control::Mode, bool)> {
|
||||
let mut mode = None;
|
||||
let mut fallback = false;
|
||||
|
||||
if let Some(target) = target {
|
||||
let refresh = target.refresh.map(|r| (r * 1000.).round() as i32);
|
||||
let target_mode = target.mode;
|
||||
|
||||
if target.custom {
|
||||
if let Some(refresh) = target_mode.refresh {
|
||||
let custom_mode =
|
||||
calculate_mode_cvt(target_mode.width, target_mode.height, refresh);
|
||||
return Some((custom_mode, false));
|
||||
} else {
|
||||
warn!("ignoring custom mode without refresh rate");
|
||||
}
|
||||
}
|
||||
|
||||
let refresh = target_mode.refresh.map(|r| (r * 1000.).round() as i32);
|
||||
for m in connector.modes() {
|
||||
if m.size() != (target.width, target.height) {
|
||||
if m.size() != (target.mode.width, target.mode.height) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -2760,3 +2984,143 @@ fn make_output_name(
|
||||
serial: info.as_ref().and_then(|info| info.serial()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_debug_snapshot;
|
||||
use niri_config::output::Modeline;
|
||||
use niri_ipc::{HSyncPolarity, VSyncPolarity};
|
||||
|
||||
use crate::backend::tty::{calculate_drm_mode_from_modeline, calculate_mode_cvt};
|
||||
|
||||
#[test]
|
||||
fn test_calculate_drmmode_from_modeline() {
|
||||
let modeline1 = Modeline {
|
||||
clock: 173.0,
|
||||
hdisplay: 1920,
|
||||
vdisplay: 1080,
|
||||
hsync_start: 2048,
|
||||
hsync_end: 2248,
|
||||
htotal: 2576,
|
||||
vsync_start: 1083,
|
||||
vsync_end: 1088,
|
||||
vtotal: 1120,
|
||||
hsync_polarity: HSyncPolarity::NHSync,
|
||||
vsync_polarity: VSyncPolarity::PVSync,
|
||||
};
|
||||
assert_debug_snapshot!(calculate_drm_mode_from_modeline(&modeline1).unwrap(), @"Mode {
|
||||
name: \"1920x1080@59.96\",
|
||||
clock: 173000,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2048,
|
||||
2248,
|
||||
2576,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1120,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 60,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}");
|
||||
let modeline2 = Modeline {
|
||||
clock: 452.5,
|
||||
hdisplay: 1920,
|
||||
vdisplay: 1080,
|
||||
hsync_start: 2088,
|
||||
hsync_end: 2296,
|
||||
htotal: 2672,
|
||||
vsync_start: 1083,
|
||||
vsync_end: 1088,
|
||||
vtotal: 1177,
|
||||
hsync_polarity: HSyncPolarity::NHSync,
|
||||
vsync_polarity: VSyncPolarity::PVSync,
|
||||
};
|
||||
assert_debug_snapshot!(calculate_drm_mode_from_modeline(&modeline2).unwrap(), @"Mode {
|
||||
name: \"1920x1080@143.88\",
|
||||
clock: 452500,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2088,
|
||||
2296,
|
||||
2672,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1177,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 144,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calc_cvt() {
|
||||
// Crosschecked with other calculators like the cvt commandline utility.
|
||||
assert_debug_snapshot!(calculate_mode_cvt(1920, 1080, 60.0), @"Mode {
|
||||
name: \"1920x1080@59.96\",
|
||||
clock: 173000,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2048,
|
||||
2248,
|
||||
2576,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1120,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 60,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}");
|
||||
assert_debug_snapshot!(calculate_mode_cvt(1920, 1080, 144.0), @"Mode {
|
||||
name: \"1920x1080@143.88\",
|
||||
clock: 452500,
|
||||
size: (
|
||||
1920,
|
||||
1080,
|
||||
),
|
||||
hsync: (
|
||||
2088,
|
||||
2296,
|
||||
2672,
|
||||
),
|
||||
vsync: (
|
||||
1083,
|
||||
1088,
|
||||
1177,
|
||||
),
|
||||
hskew: 0,
|
||||
vscan: 0,
|
||||
vrefresh: 144,
|
||||
mode_type: ModeTypeFlags(
|
||||
USERDEF,
|
||||
),
|
||||
}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,7 @@ impl Winit {
|
||||
is_preferred: true,
|
||||
}],
|
||||
current_mode: Some(0),
|
||||
is_custom_mode: true,
|
||||
vrr_supported: false,
|
||||
vrr_enabled: false,
|
||||
logical: Some(logical_output(&output)),
|
||||
|
||||
@@ -217,9 +217,14 @@ impl DisplayConfig {
|
||||
x: requested_config.x,
|
||||
y: requested_config.y,
|
||||
}),
|
||||
mode: Some(niri_ipc::ConfiguredMode::from_str(&mode).map_err(|e| {
|
||||
zbus::fdo::Error::Failed(format!("Could not parse mode '{mode}': {e}"))
|
||||
})?),
|
||||
mode: Some(niri_config::output::Mode {
|
||||
custom: false,
|
||||
mode: niri_ipc::ConfiguredMode::from_str(&mode).map_err(|e| {
|
||||
zbus::fdo::Error::Failed(format!(
|
||||
"Could not parse mode '{mode}': {e}"
|
||||
))
|
||||
})?,
|
||||
}),
|
||||
// FIXME: VRR
|
||||
..Default::default()
|
||||
}),
|
||||
|
||||
+26
-8
@@ -526,6 +526,7 @@ fn print_output(output: Output) -> anyhow::Result<()> {
|
||||
physical_size,
|
||||
modes,
|
||||
current_mode,
|
||||
is_custom_mode,
|
||||
vrr_supported,
|
||||
vrr_enabled,
|
||||
logical,
|
||||
@@ -534,6 +535,26 @@ fn print_output(output: Output) -> anyhow::Result<()> {
|
||||
let serial = serial.as_deref().unwrap_or("Unknown");
|
||||
println!(r#"Output "{make} {model} {serial}" ({name})"#);
|
||||
|
||||
let print_qualifier = |is_preferred: bool, is_current: bool, is_custom_mode: bool| {
|
||||
let mut qualifier = Vec::new();
|
||||
if is_current {
|
||||
qualifier.push("current");
|
||||
if is_custom_mode {
|
||||
qualifier.push("custom");
|
||||
};
|
||||
};
|
||||
|
||||
if is_preferred {
|
||||
qualifier.push("preferred");
|
||||
};
|
||||
|
||||
if qualifier.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" ({})", qualifier.join(", "))
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(current) = current_mode {
|
||||
let mode = *modes
|
||||
.get(current)
|
||||
@@ -545,8 +566,10 @@ fn print_output(output: Output) -> anyhow::Result<()> {
|
||||
is_preferred,
|
||||
} = mode;
|
||||
let refresh = refresh_rate as f64 / 1000.;
|
||||
let preferred = if is_preferred { " (preferred)" } else { "" };
|
||||
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz{preferred}");
|
||||
|
||||
// This is technically the current mode, but the println below already specifies that.
|
||||
let qualifier = print_qualifier(is_preferred, false, is_custom_mode);
|
||||
println!(" Current mode: {width}x{height} @ {refresh:.3} Hz{qualifier}");
|
||||
} else {
|
||||
println!(" Disabled");
|
||||
}
|
||||
@@ -601,12 +624,7 @@ fn print_output(output: Output) -> anyhow::Result<()> {
|
||||
let refresh = refresh_rate as f64 / 1000.;
|
||||
|
||||
let is_current = Some(idx) == current_mode;
|
||||
let qualifier = match (is_current, is_preferred) {
|
||||
(true, true) => " (current, preferred)",
|
||||
(true, false) => " (current)",
|
||||
(false, true) => " (preferred)",
|
||||
(false, false) => "",
|
||||
};
|
||||
let qualifier = print_qualifier(is_preferred, is_current, is_custom_mode);
|
||||
|
||||
println!(" {width}x{height}@{refresh:.3}{qualifier}");
|
||||
}
|
||||
|
||||
@@ -399,6 +399,8 @@ async fn process(ctx: &ClientCtx, request: Request) -> Reply {
|
||||
Response::Handled
|
||||
}
|
||||
Request::Output { output, action } => {
|
||||
action.validate()?;
|
||||
|
||||
let ipc_outputs = ctx.ipc_outputs.lock().unwrap();
|
||||
let found = ipc_outputs
|
||||
.values()
|
||||
|
||||
+38
-2
@@ -1777,8 +1777,44 @@ impl State {
|
||||
niri_ipc::OutputAction::Mode { mode } => {
|
||||
config.mode = match mode {
|
||||
niri_ipc::ModeToSet::Automatic => None,
|
||||
niri_ipc::ModeToSet::Specific(mode) => Some(mode),
|
||||
}
|
||||
niri_ipc::ModeToSet::Specific(mode) => Some(niri_config::output::Mode {
|
||||
custom: false,
|
||||
mode,
|
||||
}),
|
||||
};
|
||||
config.modeline = None;
|
||||
}
|
||||
niri_ipc::OutputAction::CustomMode { mode } => {
|
||||
config.mode = Some(niri_config::output::Mode { custom: true, mode });
|
||||
config.modeline = None;
|
||||
}
|
||||
niri_ipc::OutputAction::Modeline {
|
||||
clock,
|
||||
hdisplay,
|
||||
hsync_start,
|
||||
hsync_end,
|
||||
htotal,
|
||||
vdisplay,
|
||||
vsync_start,
|
||||
vsync_end,
|
||||
vtotal,
|
||||
hsync_polarity,
|
||||
vsync_polarity,
|
||||
} => {
|
||||
// Do not reset config.mode to None since it's used as a fallback.
|
||||
config.modeline = Some(niri_config::output::Modeline {
|
||||
clock,
|
||||
hdisplay,
|
||||
hsync_start,
|
||||
hsync_end,
|
||||
htotal,
|
||||
vdisplay,
|
||||
vsync_start,
|
||||
vsync_end,
|
||||
vtotal,
|
||||
hsync_polarity,
|
||||
vsync_polarity,
|
||||
})
|
||||
}
|
||||
niri_ipc::OutputAction::Scale { scale } => {
|
||||
config.scale = match scale {
|
||||
|
||||
@@ -110,19 +110,47 @@ impl OutputManagementManagerState {
|
||||
}
|
||||
}
|
||||
|
||||
// TTY outputs can't change modes I think, however, winit and virtual outputs can.
|
||||
// Winit and virtual outputs can change modes; on a TTY custom modes can add/remove
|
||||
// a mode.
|
||||
let modes_changed = old.modes != conf.modes;
|
||||
if modes_changed {
|
||||
changed = true;
|
||||
if old.modes.len() != conf.modes.len() {
|
||||
error!("output's old mode count doesn't match new modes");
|
||||
} else {
|
||||
for client in self.clients.values() {
|
||||
if let Some((_, modes)) = client.heads.get(output) {
|
||||
for (wl_mode, mode) in zip(modes, &conf.modes) {
|
||||
wl_mode.size(i32::from(mode.width), i32::from(mode.height));
|
||||
if let Ok(refresh_rate) = mode.refresh_rate.try_into() {
|
||||
wl_mode.refresh(refresh_rate);
|
||||
for client in self.clients.values_mut() {
|
||||
if let Some((head, modes)) = client.heads.get_mut(output) {
|
||||
// Ends on the shortest iterator.
|
||||
let zwlr_modes_with_modes = zip(modes.iter(), &conf.modes);
|
||||
let least_modes_len = zwlr_modes_with_modes.len();
|
||||
|
||||
for (wl_mode, mode) in zwlr_modes_with_modes {
|
||||
wl_mode.size(i32::from(mode.width), i32::from(mode.height));
|
||||
if let Ok(refresh_rate) = mode.refresh_rate.try_into() {
|
||||
wl_mode.refresh(refresh_rate);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(client) = client.manager.client() {
|
||||
if conf.modes.len() > least_modes_len {
|
||||
for mode in &conf.modes[least_modes_len..] {
|
||||
// One or more modes were added.
|
||||
let new_mode = client
|
||||
.create_resource::<ZwlrOutputModeV1, _, State>(
|
||||
&self.display,
|
||||
head.version(),
|
||||
(),
|
||||
)
|
||||
.unwrap();
|
||||
head.mode(&new_mode);
|
||||
new_mode
|
||||
.size(i32::from(mode.width), i32::from(mode.height));
|
||||
if let Ok(refresh_rate) = mode.refresh_rate.try_into() {
|
||||
new_mode.refresh(refresh_rate)
|
||||
}
|
||||
modes.push(new_mode);
|
||||
}
|
||||
} else if modes.len() > least_modes_len {
|
||||
// One or more modes were removed.
|
||||
for mode in modes.drain(least_modes_len..) {
|
||||
mode.finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -619,18 +647,21 @@ where
|
||||
return;
|
||||
};
|
||||
|
||||
new_config.mode = Some(niri_ipc::ConfiguredMode {
|
||||
width: mode.width,
|
||||
height: mode.height,
|
||||
refresh: Some(mode.refresh_rate as f64 / 1000.),
|
||||
new_config.mode = Some(niri_config::output::Mode {
|
||||
custom: false,
|
||||
mode: niri_ipc::ConfiguredMode {
|
||||
width: mode.width,
|
||||
height: mode.height,
|
||||
refresh: Some(mode.refresh_rate as f64 / 1000.),
|
||||
},
|
||||
});
|
||||
new_config.modeline = None;
|
||||
}
|
||||
zwlr_output_configuration_head_v1::Request::SetCustomMode {
|
||||
width,
|
||||
height,
|
||||
refresh,
|
||||
} => {
|
||||
// FIXME: Support custom mode
|
||||
let (width, height, refresh): (u16, u16, u32) =
|
||||
match (width.try_into(), height.try_into(), refresh.try_into()) {
|
||||
(Ok(width), Ok(height), Ok(refresh)) => (width, height, refresh),
|
||||
@@ -640,25 +671,20 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
let Some(current_config) = g_state.current_state.get(output_id) else {
|
||||
warn!("SetMode: output missing from the current config");
|
||||
if refresh == 0 {
|
||||
warn!("SetCustomMode: refresh 0 requested, ignoring");
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
let Some(mode) = current_config.modes.iter().find(|m| {
|
||||
m.width == width
|
||||
&& m.height == height
|
||||
&& (refresh == 0 || m.refresh_rate == refresh)
|
||||
}) else {
|
||||
warn!("SetCustomMode: no matching mode");
|
||||
return;
|
||||
};
|
||||
|
||||
new_config.mode = Some(niri_ipc::ConfiguredMode {
|
||||
width: mode.width,
|
||||
height: mode.height,
|
||||
refresh: Some(mode.refresh_rate as f64 / 1000.),
|
||||
new_config.mode = Some(niri_config::output::Mode {
|
||||
custom: true,
|
||||
mode: niri_ipc::ConfiguredMode {
|
||||
width,
|
||||
height,
|
||||
refresh: Some(refresh as f64 / 1000.),
|
||||
},
|
||||
});
|
||||
new_config.modeline = None;
|
||||
}
|
||||
zwlr_output_configuration_head_v1::Request::SetPosition { x, y } => {
|
||||
new_config.position = Some(niri_config::Position { x, y });
|
||||
|
||||
Reference in New Issue
Block a user