From bc8ec3d5a66f4b17f204fcce4a68431c0aa9c169 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 3 Jun 2024 22:27:48 -0500 Subject: [PATCH] rust-api: Add custom modelines Still need to do the Lua side --- .../pinnacle/output/v0alpha1/output.proto | 16 ++ api/rust/src/output.rs | 183 +++++++++++++++++- src/api.rs | 47 ++++- src/backend.rs | 5 +- src/backend/dummy.rs | 5 +- src/backend/udev.rs | 25 ++- src/backend/udev/drm/util.rs | 133 ++++++------- src/backend/winit.rs | 8 +- src/handlers.rs | 3 +- src/output.rs | 19 +- 10 files changed, 345 insertions(+), 99 deletions(-) diff --git a/api/protocol/pinnacle/output/v0alpha1/output.proto b/api/protocol/pinnacle/output/v0alpha1/output.proto index 470cbf0..a14a35f 100644 --- a/api/protocol/pinnacle/output/v0alpha1/output.proto +++ b/api/protocol/pinnacle/output/v0alpha1/output.proto @@ -36,6 +36,21 @@ message SetModeRequest { optional uint32 refresh_rate_millihz = 4; } +message SetModelineRequest { + optional string output_name = 1; + optional float clock = 2; + optional uint32 hdisplay = 3; + optional uint32 hsync_start = 4; + optional uint32 hsync_end = 5; + optional uint32 htotal = 6; + optional uint32 vdisplay = 7; + optional uint32 vsync_start = 8; + optional uint32 vsync_end = 9; + optional uint32 vtotal = 10; + optional bool hsync_pos = 11; + optional bool vsync_pos = 12; +} + message SetScaleRequest { optional string output_name = 1; oneof absolute_or_relative { @@ -106,6 +121,7 @@ message GetPropertiesResponse { service OutputService { rpc SetLocation(SetLocationRequest) returns (google.protobuf.Empty); rpc SetMode(SetModeRequest) returns (google.protobuf.Empty); + rpc SetModeline(SetModelineRequest) returns (google.protobuf.Empty); rpc SetScale(SetScaleRequest) returns (google.protobuf.Empty); rpc SetTransform(SetTransformRequest) returns (google.protobuf.Empty); rpc SetPowered(SetPoweredRequest) returns (google.protobuf.Empty); diff --git a/api/rust/src/output.rs b/api/rust/src/output.rs index 7685c32..9bbfcc1 100644 --- a/api/rust/src/output.rs +++ b/api/rust/src/output.rs @@ -9,14 +9,14 @@ //! This module provides [`Output`], which allows you to get [`OutputHandle`]s for different //! connected monitors and set them up. -use std::{num::NonZeroU32, sync::OnceLock}; +use std::{num::NonZeroU32, str::FromStr, sync::OnceLock}; use futures::FutureExt; use pinnacle_api_defs::pinnacle::output::{ self, v0alpha1::{ output_service_client::OutputServiceClient, set_scale_request::AbsoluteOrRelative, - SetLocationRequest, SetModeRequest, SetPoweredRequest, SetScaleRequest, + SetLocationRequest, SetModeRequest, SetModelineRequest, SetPoweredRequest, SetScaleRequest, SetTransformRequest, }, }; @@ -812,6 +812,37 @@ impl OutputHandle { .unwrap(); } + /// Set a custom modeline for this output. + /// + /// See `xorg.conf(5)` for more information. + /// + /// You can parse a modeline from a string of the form + /// ` `. + /// + /// # Examples + /// + /// ``` + /// output.set_modeline("173.00 1920 2048 2248 2576 1080 1083 1088 1120 -hsync +vsync".parse()?); + /// ``` + pub fn set_modeline(&self, modeline: Modeline) { + let mut client = self.output_client.clone(); + block_on_tokio(client.set_modeline(SetModelineRequest { + output_name: Some(self.name.clone()), + clock: Some(modeline.clock), + hdisplay: Some(modeline.hdisplay), + hsync_start: Some(modeline.hsync_start), + hsync_end: Some(modeline.hsync_end), + htotal: Some(modeline.htotal), + vdisplay: Some(modeline.vdisplay), + vsync_start: Some(modeline.vsync_start), + vsync_end: Some(modeline.vsync_end), + vtotal: Some(modeline.vtotal), + hsync_pos: Some(modeline.hsync), + vsync_pos: Some(modeline.vsync), + })) + .unwrap(); + } + /// Set this output's scaling factor. /// /// # Examples @@ -1262,3 +1293,151 @@ pub struct OutputProperties { /// This output's window keyboard focus stack. pub keyboard_focus_stack: Vec, } + +/// A custom modeline. +#[allow(missing_docs)] +#[derive(Copy, Clone, Debug, PartialEq, Default)] +pub struct Modeline { + pub clock: f32, + pub hdisplay: u32, + pub hsync_start: u32, + pub hsync_end: u32, + pub htotal: u32, + pub vdisplay: u32, + pub vsync_start: u32, + pub vsync_end: u32, + pub vtotal: u32, + pub hsync: bool, + pub vsync: bool, +} + +/// Error for the `FromStr` implementation for [`Modeline`]. +#[derive(Debug)] +pub struct ParseModelineError(ParseModelineErrorKind); + +#[derive(Debug)] +enum ParseModelineErrorKind { + NoClock, + InvalidClock, + NoHdisplay, + InvalidHdisplay, + NoHsyncStart, + InvalidHsyncStart, + NoHsyncEnd, + InvalidHsyncEnd, + NoHtotal, + InvalidHtotal, + NoVdisplay, + InvalidVdisplay, + NoVsyncStart, + InvalidVsyncStart, + NoVsyncEnd, + InvalidVsyncEnd, + NoVtotal, + InvalidVtotal, + NoHsync, + InvalidHsync, + NoVsync, + InvalidVsync, +} + +impl std::fmt::Display for ParseModelineError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Debug::fmt(&self.0, f) + } +} + +impl From for ParseModelineError { + fn from(value: ParseModelineErrorKind) -> Self { + Self(value) + } +} + +impl FromStr for Modeline { + type Err = ParseModelineError; + + fn from_str(s: &str) -> Result { + let mut args = s.split_whitespace(); + + let clock = args + .next() + .ok_or(ParseModelineErrorKind::NoClock)? + .parse() + .map_err(|_| ParseModelineErrorKind::InvalidClock)?; + let hdisplay = args + .next() + .ok_or(ParseModelineErrorKind::NoHdisplay)? + .parse() + .map_err(|_| ParseModelineErrorKind::InvalidHdisplay)?; + let hsync_start = args + .next() + .ok_or(ParseModelineErrorKind::NoHsyncStart)? + .parse() + .map_err(|_| ParseModelineErrorKind::InvalidHsyncStart)?; + let hsync_end = args + .next() + .ok_or(ParseModelineErrorKind::NoHsyncEnd)? + .parse() + .map_err(|_| ParseModelineErrorKind::InvalidHsyncEnd)?; + let htotal = args + .next() + .ok_or(ParseModelineErrorKind::NoHtotal)? + .parse() + .map_err(|_| ParseModelineErrorKind::InvalidHtotal)?; + let vdisplay = args + .next() + .ok_or(ParseModelineErrorKind::NoVdisplay)? + .parse() + .map_err(|_| ParseModelineErrorKind::InvalidVdisplay)?; + let vsync_start = args + .next() + .ok_or(ParseModelineErrorKind::NoVsyncStart)? + .parse() + .map_err(|_| ParseModelineErrorKind::InvalidVsyncStart)?; + let vsync_end = args + .next() + .ok_or(ParseModelineErrorKind::NoVsyncEnd)? + .parse() + .map_err(|_| ParseModelineErrorKind::InvalidVsyncEnd)?; + let vtotal = args + .next() + .ok_or(ParseModelineErrorKind::NoVtotal)? + .parse() + .map_err(|_| ParseModelineErrorKind::InvalidVtotal)?; + + let hsync = match args + .next() + .ok_or(ParseModelineErrorKind::NoHsync)? + .to_lowercase() + .as_str() + { + "+hsync" => true, + "-hsync" => false, + _ => Err(ParseModelineErrorKind::InvalidHsync)?, + }; + let vsync = match args + .next() + .ok_or(ParseModelineErrorKind::NoVsync)? + .to_lowercase() + .as_str() + { + "+vsync" => true, + "-vsync" => false, + _ => Err(ParseModelineErrorKind::InvalidVsync)?, + }; + + Ok(Modeline { + clock, + hdisplay, + hsync_start, + hsync_end, + htotal, + vdisplay, + vsync_start, + vsync_end, + vtotal, + hsync, + vsync, + }) + } +} diff --git a/src/api.rs b/src/api.rs index ce71925..dc63274 100644 --- a/src/api.rs +++ b/src/api.rs @@ -16,7 +16,8 @@ use pinnacle_api_defs::pinnacle::{ self, v0alpha1::{ output_service_server, set_scale_request::AbsoluteOrRelative, SetLocationRequest, - SetModeRequest, SetPoweredRequest, SetScaleRequest, SetTransformRequest, + SetModeRequest, SetModelineRequest, SetPoweredRequest, SetScaleRequest, + SetTransformRequest, }, }, process::v0alpha1::{process_service_server, SetEnvRequest, SpawnRequest, SpawnResponse}, @@ -52,10 +53,10 @@ use tonic::{Request, Response, Status, Streaming}; use tracing::{debug, error, info, trace, warn}; use crate::{ - backend::BackendData, + backend::{udev::drm_mode_from_api_modeline, BackendData}, config::ConnectorSavedState, input::ModifierMask, - output::OutputName, + output::{OutputMode, OutputName}, render::util::snapshot::capture_snapshots_on_output, state::{State, WithState}, tag::{Tag, TagId}, @@ -1080,7 +1081,45 @@ impl output_service_server::OutputService for OutputService { state.pinnacle.change_output_state( &mut state.backend, &output, - Some(mode), + Some(OutputMode::Smithay(mode)), + None, + None, + None, + ); + state.pinnacle.request_layout(&output); + state + .pinnacle + .output_management_manager_state + .update::(); + }) + .await + } + + async fn set_modeline( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + run_unary_no_response(&self.sender, |state| { + let Some(output) = request + .output_name + .clone() + .map(OutputName) + .and_then(|name| name.output(&state.pinnacle)) + else { + return; + }; + + let Some(mode) = drm_mode_from_api_modeline(request) else { + // TODO: raise error + return; + }; + + state.pinnacle.change_output_state( + &mut state.backend, + &output, + Some(OutputMode::Drm(mode)), None, None, None, diff --git a/src/backend.rs b/src/backend.rs index 2a0b8c8..8dfc97b 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -34,6 +34,7 @@ use smithay::{ use tracing::error; use crate::{ + output::OutputMode, state::{Pinnacle, State, SurfaceDmabufFeedback, WithState}, window::WindowElement, }; @@ -152,7 +153,7 @@ pub trait BackendData: 'static { // INFO: only for udev in anvil, maybe shouldn't be a trait fn? fn early_import(&mut self, surface: &WlSurface); - fn set_output_mode(&mut self, output: &Output, mode: smithay::output::Mode); + fn set_output_mode(&mut self, output: &Output, mode: OutputMode); } impl BackendData for Backend { @@ -183,7 +184,7 @@ impl BackendData for Backend { } } - fn set_output_mode(&mut self, output: &Output, mode: smithay::output::Mode) { + fn set_output_mode(&mut self, output: &Output, mode: OutputMode) { match self { Backend::Winit(winit) => winit.set_output_mode(output, mode), Backend::Udev(udev) => udev.set_output_mode(output, mode), diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs index 98d662a..2dbcf4f 100644 --- a/src/backend/dummy.rs +++ b/src/backend/dummy.rs @@ -10,6 +10,7 @@ use smithay::{ utils::Transform, }; +use crate::output::OutputMode; use crate::state::{Pinnacle, State, WithState}; use super::BackendData; @@ -50,8 +51,8 @@ impl BackendData for Dummy { fn early_import(&mut self, _surface: &WlSurface) {} - fn set_output_mode(&mut self, output: &Output, mode: smithay::output::Mode) { - output.change_current_state(Some(mode), None, None, None); + fn set_output_mode(&mut self, output: &Output, mode: OutputMode) { + output.change_current_state(Some(mode.into()), None, None, None); } } diff --git a/src/backend/udev.rs b/src/backend/udev.rs index f304fea..7089934 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -3,6 +3,8 @@ mod drm; mod gamma; +pub use drm::util::drm_mode_from_api_modeline; + use std::{ collections::{HashMap, HashSet}, path::Path, @@ -82,7 +84,7 @@ use tracing::{debug, error, info, trace, warn}; use crate::{ backend::Backend, config::ConnectorSavedState, - output::{BlankingState, OutputName}, + output::{BlankingState, OutputMode, OutputName}, render::{ pointer::PointerElement, pointer_render_elements, take_presentation_feedback, OutputRenderElement, CLEAR_COLOR, CLEAR_COLOR_LOCKED, @@ -664,7 +666,7 @@ impl BackendData for Udev { } } - fn set_output_mode(&mut self, output: &Output, mode: smithay::output::Mode) { + fn set_output_mode(&mut self, output: &Output, mode: OutputMode) { let drm_mode = self .backends .iter() @@ -681,18 +683,24 @@ impl BackendData for Udev { .and_then(|(info, _)| { info.modes() .iter() - .find(|m| smithay::output::Mode::from(**m) == mode) + .find(|m| smithay::output::Mode::from(**m) == mode.into()) }) .copied() }) .unwrap_or_else(|| { info!("Unknown mode for {}, creating new one", output.name()); - create_drm_mode(mode.size.w, mode.size.h, Some(mode.refresh as u32)) + match mode { + OutputMode::Smithay(mode) => { + create_drm_mode(mode.size.w, mode.size.h, Some(mode.refresh as u32)) + } + OutputMode::Drm(mode) => mode, + } }); if let Some(render_surface) = render_surface_for_output(output, &mut self.backends) { match render_surface.compositor.use_mode(drm_mode) { Ok(()) => { + let mode = smithay::output::Mode::from(mode); info!( "Set {}'s mode to {}x{}@{:.3}Hz", output.name(), @@ -1141,7 +1149,14 @@ impl Udev { device.surfaces.insert(crtc, surface); - pinnacle.change_output_state(self, &output, Some(wl_mode), None, None, Some(position)); + pinnacle.change_output_state( + self, + &output, + Some(OutputMode::Smithay(wl_mode)), + None, + None, + Some(position), + ); // If there is saved connector state, the connector was previously plugged in. // In this case, restore its tags and location. diff --git a/src/backend/udev/drm/util.rs b/src/backend/udev/drm/util.rs index 25dfd16..56c43d6 100644 --- a/src/backend/udev/drm/util.rs +++ b/src/backend/udev/drm/util.rs @@ -1,6 +1,6 @@ use std::{ffi::CString, io::Write, mem::MaybeUninit, num::NonZeroU32}; -use anyhow::{bail, Context}; +use anyhow::Context; use drm_sys::{ drm_mode_modeinfo, DRM_MODE_FLAG_NHSYNC, DRM_MODE_FLAG_NVSYNC, DRM_MODE_FLAG_PHSYNC, DRM_MODE_FLAG_PVSYNC, DRM_MODE_TYPE_USERDEF, @@ -9,6 +9,7 @@ use libdisplay_info_sys::cvt::{ di_cvt_compute, di_cvt_options, di_cvt_reduced_blanking_version_DI_CVT_REDUCED_BLANKING_NONE, di_cvt_timing, }; +use pinnacle_api_defs::pinnacle::output::v0alpha1::SetModelineRequest; use smithay::reexports::drm::{ self, control::{connector, property, Device, ResourceHandle}, @@ -141,94 +142,72 @@ pub(super) fn get_drm_property( anyhow::bail!("No prop found for {}", name) } -// From https://github.com/swaywm/sway/blob/2e9139df664f1e2dbe14b5df4a9646411b924c66/sway/commands/output/mode.c#L64 -fn parse_modeline_string(modeline: &str) -> anyhow::Result { - let mut args = modeline.split_whitespace(); +pub fn drm_mode_from_api_modeline(modeline: SetModelineRequest) -> Option { + let SetModelineRequest { + output_name: _, + clock: Some(clock), + hdisplay: Some(hdisplay), + hsync_start: Some(hsync_start), + hsync_end: Some(hsync_end), + htotal: Some(htotal), + vdisplay: Some(vdisplay), + vsync_start: Some(vsync_start), + vsync_end: Some(vsync_end), + vtotal: Some(vtotal), + hsync_pos: Some(hsync_pos), + vsync_pos: Some(vsync_pos), + } = modeline + else { + return None; + }; - let clock = args - .next() - .context("no clock specified")? - .parse::() - .context("failed to parse clock")? - * 1000; - let hdisplay = args - .next() - .context("no hdisplay specified")? - .parse() - .context("failed to parse hdisplay")?; - let hsync_start = args - .next() - .context("no hsync_start specified")? - .parse() - .context("failed to parse hsync_start")?; - let hsync_end = args - .next() - .context("no hsync_end specified")? - .parse() - .context("failed to parse hsync_end")?; - let htotal = args - .next() - .context("no htotal specified")? - .parse() - .context("failed to parse htotal")?; - let vdisplay = args - .next() - .context("no vdisplay specified")? - .parse() - .context("failed to parse vdisplay")?; - let vsync_start = args - .next() - .context("no vsync_start specified")? - .parse() - .context("failed to parse vsync_start")?; - let vsync_end = args - .next() - .context("no vsync_end specified")? - .parse() - .context("failed to parse vsync_end")?; - let vtotal = args - .next() - .context("no vtotal specified")? - .parse() - .context("failed to parse vtotal")?; - let vrefresh = clock * 1000 * 1000 / htotal as u32 / vtotal as u32; + let clock = clock * 1000.0; + + let vrefresh = (clock * 1000.0 * 1000.0 / htotal as f32 / vtotal as f32) as u32; let mut flags = 0; - match args.next().context("no +/-hsync specified")? { - "+hsync" => flags |= DRM_MODE_FLAG_PHSYNC, - "-hsync" => flags |= DRM_MODE_FLAG_NHSYNC, - _ => bail!("invalid hsync specifier"), + match hsync_pos { + true => flags |= DRM_MODE_FLAG_PHSYNC, + false => flags |= DRM_MODE_FLAG_NHSYNC, }; - match args.next().context("no +/-vsync specified")? { - "+vsync" => flags |= DRM_MODE_FLAG_PVSYNC, - "-vsync" => flags |= DRM_MODE_FLAG_NVSYNC, - _ => bail!("invalid vsync specifier"), + match vsync_pos { + true => flags |= DRM_MODE_FLAG_PVSYNC, + false => flags |= DRM_MODE_FLAG_NVSYNC, }; let type_ = DRM_MODE_TYPE_USERDEF; - let name = CString::new(format!("{}x{}@{}", hdisplay, vdisplay, vrefresh / 1000)).unwrap(); + let name = CString::new(format!( + "{}x{}@{:.3}", + hdisplay, + vdisplay, + vrefresh as f64 / 1000.0 + )) + .unwrap(); let mut name_buf = [0u8; 32]; let _ = name_buf.as_mut_slice().write_all(name.as_bytes_with_nul()); let name: [i8; 32] = bytemuck::cast(name_buf); - Ok(drm_mode_modeinfo { - clock, - hdisplay, - hsync_start, - hsync_end, - htotal, - hskew: 0, - vdisplay, - vsync_start, - vsync_end, - vtotal, - vscan: 0, - vrefresh, - flags, - type_, - name, - }) + Some( + drm_mode_modeinfo { + clock: clock as u32, + hdisplay: hdisplay as u16, + hsync_start: hsync_start as u16, + hsync_end: hsync_end as u16, + htotal: htotal as u16, + hskew: 0, + vdisplay: vdisplay as u16, + vsync_start: vsync_start as u16, + vsync_end: vsync_end as u16, + vtotal: vtotal as u16, + vscan: 0, + vrefresh, + flags, + type_, + name, + } + .into(), + ) } /// Create a new drm mode from a given width, height, and optional refresh rate (defaults to 60Hz). diff --git a/src/backend/winit.rs b/src/backend/winit.rs index c3f2261..fd4ee85 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -36,7 +36,7 @@ use smithay::{ use tracing::{debug, error, trace, warn}; use crate::{ - output::BlankingState, + output::{BlankingState, OutputMode}, render::{ pointer::PointerElement, pointer_render_elements, take_presentation_feedback, CLEAR_COLOR, CLEAR_COLOR_LOCKED, @@ -68,8 +68,8 @@ impl BackendData for Winit { fn early_import(&mut self, _surface: &WlSurface) {} - fn set_output_mode(&mut self, output: &Output, mode: smithay::output::Mode) { - output.change_current_state(Some(mode), None, None, None); + fn set_output_mode(&mut self, output: &Output, mode: OutputMode) { + output.change_current_state(Some(mode.into()), None, None, None); } } @@ -207,7 +207,7 @@ impl Winit { state.pinnacle.change_output_state( &mut state.backend, &output, - Some(mode), + Some(OutputMode::Smithay(mode)), None, Some(Scale::Fractional(scale_factor)), // None, diff --git a/src/handlers.rs b/src/handlers.rs index 8e53920..488b7e4 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -77,6 +77,7 @@ use crate::{ delegate_output_power_management, delegate_screencopy, focus::{keyboard::KeyboardFocusTarget, pointer::PointerFocusTarget}, handlers::xdg_shell::snapshot_pre_commit_hook, + output::OutputMode, protocol::{ foreign_toplevel::{self, ForeignToplevelHandler, ForeignToplevelManagerState}, gamma_control::{GammaControlHandler, GammaControlManagerState}, @@ -979,7 +980,7 @@ impl OutputManagementHandler for State { self.pinnacle.change_output_state( &mut self.backend, &output, - mode, + mode.map(OutputMode::Smithay), transform, scale.map(Scale::Fractional), position, diff --git a/src/output.rs b/src/output.rs index 7c0afe5..f648b6d 100644 --- a/src/output.rs +++ b/src/output.rs @@ -8,7 +8,7 @@ use pinnacle_api_defs::pinnacle::signal::v0alpha1::{ use smithay::{ desktop::layer_map_for_output, output::{Mode, Output, Scale}, - reexports::calloop::LoopHandle, + reexports::{calloop::LoopHandle, drm}, utils::{Logical, Point, Transform}, wayland::session_lock::LockSurface, }; @@ -122,12 +122,27 @@ impl OutputState { } } +#[derive(Debug, Clone, Copy)] +pub enum OutputMode { + Smithay(Mode), + Drm(drm::control::Mode), +} + +impl From for Mode { + fn from(value: OutputMode) -> Self { + match value { + OutputMode::Smithay(mode) => mode, + OutputMode::Drm(mode) => Mode::from(mode), + } + } +} + impl Pinnacle { pub fn change_output_state( &mut self, backend: &mut impl BackendData, output: &Output, - mode: Option, + mode: Option, transform: Option, scale: Option, location: Option>,