aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMerlijn <32853531+ToxicMushroom@users.noreply.github.com>2025-10-29 07:10:38 +0100
committerGitHub <noreply@github.com>2025-10-29 09:10:38 +0300
commit6a2c6261df130cccb5262eddf71d40b2fffcf8f9 (patch)
tree48639aef4ebddbc315234b925954c5cc768d0f1c /src
parente6f3c538da0c646bda43fcde7ef7dc3b771e0c8b (diff)
downloadniri-6a2c6261df130cccb5262eddf71d40b2fffcf8f9.tar.gz
niri-6a2c6261df130cccb5262eddf71d40b2fffcf8f9.tar.bz2
niri-6a2c6261df130cccb5262eddf71d40b2fffcf8f9.zip
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>
Diffstat (limited to 'src')
-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
8 files changed, 512 insertions, 59 deletions
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);
+
+ 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,
+ ),
+}");
+ }
+}
diff --git a/src/backend/winit.rs b/src/backend/winit.rs
index ebec64c9..1fb4019a 100644
--- a/src/backend/winit.rs
+++ b/src/backend/winit.rs
@@ -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)),
diff --git a/src/dbus/mutter_display_config.rs b/src/dbus/mutter_display_config.rs
index fe445f64..91429d9a 100644
--- a/src/dbus/mutter_display_config.rs
+++ b/src/dbus/mutter_display_config.rs
@@ -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()
}),
diff --git a/src/ipc/client.rs b/src/ipc/client.rs
index 3c7b27a9..8fea1d2b 100644
--- a/src/ipc/client.rs
+++ b/src/ipc/client.rs
@@ -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}");
}
diff --git a/src/ipc/server.rs b/src/ipc/server.rs
index 7fdc81f5..ade57de1 100644
--- a/src/ipc/server.rs
+++ b/src/ipc/server.rs
@@ -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()
diff --git a/src/niri.rs b/src/niri.rs
index 5bc69c2e..551439c3 100644
--- a/src/niri.rs
+++ b/src/niri.rs
@@ -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 {
diff --git a/src/protocols/output_management.rs b/src/protocols/output_management.rs
index 4f1ac8c0..2b45588f 100644
--- a/src/protocols/output_management.rs
+++ b/src/protocols/output_management.rs
@@ -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");
- 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");
+ if refresh == 0 {
+ warn!("SetCustomMode: refresh 0 requested, ignoring");
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 });