diff options
| author | Merlijn <32853531+ToxicMushroom@users.noreply.github.com> | 2025-10-29 07:10:38 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-29 09:10:38 +0300 |
| commit | 6a2c6261df130cccb5262eddf71d40b2fffcf8f9 (patch) | |
| tree | 48639aef4ebddbc315234b925954c5cc768d0f1c /src | |
| parent | e6f3c538da0c646bda43fcde7ef7dc3b771e0c8b (diff) | |
| download | niri-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.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 |
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 }); |
