From a3226a3c624fd174216ffa6d32f0ba15fe6612b3 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sat, 1 Jun 2024 20:39:01 -0500 Subject: [PATCH 01/15] Impl wlr-output-management --- src/backend/udev.rs | 8 + src/handlers.rs | 71 ++- src/layout.rs | 14 +- src/output.rs | 3 + src/protocol.rs | 1 + src/protocol/output_management.rs | 918 ++++++++++++++++++++++++++++++ src/state.rs | 19 +- 7 files changed, 1017 insertions(+), 17 deletions(-) create mode 100644 src/protocol/output_management.rs diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 464b13a..6d9fe26 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -1055,6 +1055,10 @@ impl Udev { ); let global = output.create_global::(&self.display_handle); + pinnacle + .output_management_manager_state + .add_head::(&output); + output.with_state_mut(|state| state.serial = serial); output.set_preferred(wl_mode); @@ -1215,6 +1219,10 @@ impl Udev { output_name: Some(output.name()), }) }); + + pinnacle + .output_management_manager_state + .remove_head(&output); } } diff --git a/src/handlers.rs b/src/handlers.rs index 84a554c..2194fdd 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1,17 +1,18 @@ // SPDX-License-Identifier: GPL-3.0-or-later +pub mod idle; pub mod session_lock; pub mod window; mod xdg_shell; mod xwayland; -use std::{mem, os::fd::OwnedFd, sync::Arc}; +use std::{collections::HashMap, mem, os::fd::OwnedFd, sync::Arc}; use smithay::{ backend::renderer::utils::{self, with_renderer_surface_state}, delegate_compositor, delegate_data_control, delegate_data_device, delegate_fractional_scale, - delegate_idle_notify, delegate_layer_shell, delegate_output, delegate_pointer_constraints, - delegate_presentation, delegate_primary_selection, delegate_relative_pointer, delegate_seat, + delegate_layer_shell, delegate_output, delegate_pointer_constraints, delegate_presentation, + delegate_primary_selection, delegate_relative_pointer, delegate_seat, delegate_security_context, delegate_shm, delegate_viewporter, delegate_xwayland_shell, desktop::{ self, find_popup_root_surface, get_popup_toplevel_coords, layer_map_for_output, PopupKind, @@ -21,7 +22,7 @@ use smithay::{ pointer::{CursorImageStatus, PointerHandle}, Seat, SeatHandler, SeatState, }, - output::Output, + output::{Output, Scale}, reexports::{ calloop::Interest, wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment, @@ -42,7 +43,6 @@ use smithay::{ }, dmabuf, fractional_scale::{self, FractionalScaleHandler}, - idle_notify::{IdleNotifierHandler, IdleNotifierState}, output::OutputHandler, pointer_constraints::{with_pointer_constraint, PointerConstraintsHandler}, seat::WaylandFocus, @@ -69,16 +69,20 @@ use smithay::{ }, xwayland::{X11Wm, XWaylandClientData}, }; -use tracing::{error, trace, warn}; +use tracing::{debug, error, trace, warn}; use crate::{ backend::Backend, - delegate_foreign_toplevel, delegate_gamma_control, delegate_screencopy, + delegate_foreign_toplevel, delegate_gamma_control, delegate_output_management, + delegate_screencopy, focus::{keyboard::KeyboardFocusTarget, pointer::PointerFocusTarget}, handlers::xdg_shell::snapshot_pre_commit_hook, protocol::{ foreign_toplevel::{self, ForeignToplevelHandler, ForeignToplevelManagerState}, gamma_control::{GammaControlHandler, GammaControlManagerState}, + output_management::{ + OutputConfiguration, OutputManagementHandler, OutputManagementManagerState, + }, screencopy::{Screencopy, ScreencopyHandler}, }, render::util::snapshot::capture_snapshots_on_output, @@ -918,12 +922,57 @@ impl XWaylandShellHandler for State { } delegate_xwayland_shell!(State); -impl IdleNotifierHandler for State { - fn idle_notifier_state(&mut self) -> &mut IdleNotifierState { - &mut self.pinnacle.idle_notifier_state +impl OutputManagementHandler for State { + fn output_management_manager_state(&mut self) -> &mut OutputManagementManagerState { + &mut self.pinnacle.output_management_manager_state + } + + fn apply_configuration(&mut self, config: HashMap) -> bool { + for (output, config) in config { + match config { + OutputConfiguration::Disabled => todo!(), + OutputConfiguration::Enabled { + mode, + position, + transform, + scale, + adaptive_sync, + } => { + let snapshots = self.backend.with_renderer(|renderer| { + capture_snapshots_on_output(&mut self.pinnacle, renderer, &output, []) + }); + + self.pinnacle.change_output_state( + &output, + None, + transform, + scale.map(Scale::Fractional), + position, + ); + + if let Some((a, b)) = snapshots { + output.with_state_mut(|state| { + state.new_wait_layout_transaction( + self.pinnacle.loop_handle.clone(), + a, + b, + ) + }); + } + + self.pinnacle.request_layout(&output); + } + } + } + true + } + + fn test_configuration(&mut self, config: HashMap) -> bool { + debug!(?config); + true } } -delegate_idle_notify!(State); +delegate_output_management!(State); impl Pinnacle { fn position_popup(&self, popup: &PopupSurface) { diff --git a/src/layout.rs b/src/layout.rs index 7e9f8fe..3dbb0a5 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -32,20 +32,24 @@ impl Pinnacle { output: &Output, geometries: Vec>, ) -> Vec<(WindowElement, Serial)> { - let windows_on_foc_tags = output.with_state(|state| { + let (windows_on_foc_tags, to_unmap) = output.with_state(|state| { let focused_tags = state.focused_tags().collect::>(); self.windows .iter() - .filter(|win| !win.is_x11_override_redirect()) - .filter(|win| { + .filter(|win| win.output(self).as_ref() == Some(output)) + .cloned() + .partition::, _>(|win| { win.with_state(|state| state.tags.iter().any(|tg| focused_tags.contains(&tg))) }) - .cloned() - .collect::>() }); + for win in to_unmap { + self.space.unmap_elem(&win); + } + let tiled_windows = windows_on_foc_tags .iter() + .filter(|win| !win.is_x11_override_redirect()) .filter(|win| { win.with_state(|state| { state.floating_or_tiled.is_tiled() && state.fullscreen_or_maximized.is_neither() diff --git a/src/output.rs b/src/output.rs index f965a93..66e5503 100644 --- a/src/output.rs +++ b/src/output.rs @@ -219,5 +219,8 @@ impl Pinnacle { lock_surface.send_configure(); } + + self.output_management_manager_state + .update_head::(output); } } diff --git a/src/protocol.rs b/src/protocol.rs index d1779b7..a58b48b 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,3 +1,4 @@ pub mod foreign_toplevel; pub mod gamma_control; +pub mod output_management; pub mod screencopy; diff --git a/src/protocol/output_management.rs b/src/protocol/output_management.rs new file mode 100644 index 0000000..e3eb3b4 --- /dev/null +++ b/src/protocol/output_management.rs @@ -0,0 +1,918 @@ +use smithay::{ + output::Output, + reexports::{ + wayland_protocols_wlr::output_management::v1::server::{ + zwlr_output_configuration_head_v1::{self, ZwlrOutputConfigurationHeadV1}, + zwlr_output_configuration_v1, + zwlr_output_head_v1::{self, AdaptiveSyncState}, + zwlr_output_mode_v1::{self, ZwlrOutputModeV1}, + }, + wayland_server::{Resource, WEnum}, + }, + utils::{Logical, Physical, Point, Size, Transform, SERIAL_COUNTER}, +}; +use std::{collections::HashMap, num::NonZeroU32, sync::Mutex}; + +use smithay::{ + output::Mode, + reexports::{ + wayland_protocols_wlr::output_management::v1::server::{ + zwlr_output_configuration_v1::ZwlrOutputConfigurationV1, + zwlr_output_head_v1::ZwlrOutputHeadV1, + zwlr_output_manager_v1::{self, ZwlrOutputManagerV1}, + }, + wayland_server::{ + self, backend::ClientId, Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, + }, + }, +}; + +use crate::state::WithState; + +const VERSION: u32 = 4; + +pub struct OutputManagementManagerState { + display_handle: DisplayHandle, + managers: HashMap, + outputs: HashMap, +} + +struct OutputManagerData { + serial: u32, + configurations: Vec, + heads: HashMap>, +} + +pub struct OutputManagementGlobalData { + filter: Box bool + Send + Sync>, +} + +#[derive(Debug)] +enum PendingHead { + NotConfigured, + Enabled(ZwlrOutputConfigurationHeadV1), + Disabled, +} + +#[derive(Debug)] +pub struct PendingOutputConfiguration { + serial: u32, + inner: Mutex, +} + +#[derive(Default, Debug)] +struct PendingOutputConfigurationInner { + cancelled: bool, + pending_heads: HashMap, +} + +#[derive(Debug, Copy, Clone, Default)] +pub struct PendingOutputHeadConfiguration { + pub mode: Option<(Size, Option)>, + pub position: Option>, + pub transform: Option, + pub scale: Option, + pub adaptive_sync: Option, +} + +#[derive(Debug)] +pub enum OutputConfiguration { + Disabled, + Enabled { + mode: Option<(Size, Option)>, + position: Option>, + transform: Option, + scale: Option, + adaptive_sync: Option, + }, +} + +pub trait OutputManagementHandler { + fn output_management_manager_state(&mut self) -> &mut OutputManagementManagerState; + fn apply_configuration(&mut self, config: HashMap) -> bool; + fn test_configuration(&mut self, config: HashMap) -> bool; +} + +#[derive(Debug, Clone)] +pub struct OutputData { + // modes: Vec, + enabled: bool, + current_mode: Option, + position: Point, + transform: Transform, + scale: f64, + adaptive_sync: bool, +} + +impl OutputManagementManagerState { + pub fn add_head(&mut self, output: &Output) + where + D: Dispatch + + Dispatch + + OutputManagementHandler + + 'static, + { + if self.outputs.contains_key(output) { + return; + } + + for (manager, manager_data) in self.managers.iter_mut() { + let (head, modes) = advertise_output::(&self.display_handle, manager, output); + manager_data.heads.insert(head, modes); + } + + let output_data = OutputData { + enabled: true, + current_mode: output.current_mode(), + position: output.current_location(), + transform: output.current_transform(), + scale: output.current_scale().fractional_scale(), + adaptive_sync: false, // TODO: + }; + + self.outputs.insert(output.clone(), output_data); + } + + pub fn remove_head(&mut self, output: &Output) { + self.outputs.remove(output); + + for data in self.managers.values_mut() { + let heads = data.heads.keys().cloned().collect::>(); + for head in heads { + if head.data::() == Some(output) { + let modes = data.heads.remove(&head); + if let Some(modes) = modes { + for mode in modes { + mode.finished(); + } + } + head.finished(); + } + } + } + } + + pub fn set_head_enabled(&mut self, output: &Output, enabled: bool) { + let Some(output_data) = self.outputs.get_mut(output) else { + return; + }; + + output_data.enabled = enabled; + + for manager_data in self.managers.values() { + for (head, wlr_modes) in manager_data.heads.iter() { + if head.data::() == Some(output) { + head.enabled(enabled as i32); + + if enabled { + if let Some(current_mode) = output.current_mode() { + let wlr_current_mode = wlr_modes + .iter() + .find(|wlr_mode| wlr_mode.data::() == Some(¤t_mode)); + if let Some(wlr_current_mode) = wlr_current_mode { + head.current_mode(wlr_current_mode); + } + } + head.position(output.current_location().x, output.current_location().y); + head.transform(output.current_transform().into()); + head.scale(output.current_scale().fractional_scale()); + } + } + } + } + } + + pub fn update_head(&mut self, output: &Output) + where + D: Dispatch + + Dispatch + + OutputManagementHandler + + 'static, + { + let Some(output_data) = self.outputs.get_mut(output) else { + tracing::error!("Called `update_head` without `advertise_output`"); + return; + }; + + for (manager, manager_data) in self.managers.iter_mut() { + for (head, wlr_modes) in manager_data.heads.iter_mut() { + if head.data::() != Some(output) { + continue; + } + + // TODO: modes + let modes = output.with_state(|state| state.modes.clone()); + + wlr_modes.retain(|wlr_mode| { + if !modes.contains(wlr_mode.data::().unwrap()) { + wlr_mode.finished(); + false + } else { + true + } + }); + + for mode in modes { + if !wlr_modes + .iter() + .any(|wlr_mode| wlr_mode.data::().unwrap() == &mode) + { + if let Some(client) = head.client() { + let new_wlr_mode = client + .create_resource::( + &self.display_handle, + head.version(), + mode, + ) + .expect("TODO"); + + new_wlr_mode.size(mode.size.w, mode.size.h); + new_wlr_mode.refresh(mode.refresh); + + if Some(mode) == output.preferred_mode() { + new_wlr_mode.preferred(); + } + + head.mode(&new_wlr_mode); + + wlr_modes.push(new_wlr_mode); + } + } + } + + // enabled handled in `set_head_enabled` + + if output.current_mode() != output_data.current_mode { + if let Some(new_cur_mode) = output.current_mode() { + let new_cur_wlr_mode = wlr_modes + .iter() + .find(|wlr_mode| wlr_mode.data::() == Some(&new_cur_mode)); + + match new_cur_wlr_mode { + Some(new_cur_wlr_mode) => { + head.current_mode(new_cur_wlr_mode); + } + // TODO: don't do this branch + None => { + if let Some(client) = head.client() { + let new_cur_wlr_mode = client + .create_resource::( + &self.display_handle, + head.version(), + new_cur_mode, + ) + .expect("TODO"); + + new_cur_wlr_mode.size(new_cur_mode.size.w, new_cur_mode.size.h); + new_cur_wlr_mode.refresh(new_cur_mode.refresh); + + if Some(new_cur_mode) == output.preferred_mode() { + new_cur_wlr_mode.preferred(); + } + + head.mode(&new_cur_wlr_mode); + head.current_mode(&new_cur_wlr_mode); + wlr_modes.push(new_cur_wlr_mode); + } + } + } + + output_data.current_mode = Some(new_cur_mode); + } + } + + if output.current_location() != output_data.position { + let new_loc = output.current_location(); + head.position(new_loc.x, new_loc.y); + output_data.position = new_loc; + } + + if output.current_transform() != output_data.transform { + let new_transform = output.current_transform(); + head.transform(new_transform.into()); + output_data.transform = new_transform; + } + + if output.current_scale().fractional_scale() != output_data.scale { + let new_scale = output.current_scale().fractional_scale(); + head.scale(new_scale); + output_data.scale = new_scale; + } + + // TODO: adaptive sync + } + + let serial = u32::from(SERIAL_COUNTER.next_serial()); + + manager_data.serial = serial; + manager.done(serial); + } + } +} + +fn advertise_output( + display: &DisplayHandle, + manager: &ZwlrOutputManagerV1, + output: &Output, +) -> (ZwlrOutputHeadV1, Vec) +where + D: Dispatch + + Dispatch + + OutputManagementHandler + + 'static, +{ + let client = manager.client().expect("TODO"); + + let head = client + .create_resource::(display, manager.version(), output.clone()) + .unwrap(); + + manager.head(&head); + + head.name(output.name()); + head.description(output.description()); + + let physical_props = output.physical_properties(); + head.physical_size(physical_props.size.w, physical_props.size.h); + + let mut wlr_modes = Vec::new(); + for mode in output.modes() { + let wlr_mode = client + .create_resource::(display, manager.version(), mode) + .unwrap(); + head.mode(&wlr_mode); + wlr_mode.size(mode.size.w, mode.size.h); + wlr_mode.refresh(mode.refresh); + if Some(mode) == output.preferred_mode() { + wlr_mode.preferred(); + } + wlr_modes.push(wlr_mode); + } + + if head.version() >= zwlr_output_head_v1::EVT_MAKE_SINCE { + head.make(physical_props.make); + head.model(physical_props.model); + + if let Some(serial_number) = output.with_state(|state| state.serial) { + head.serial_number(serial_number.to_string()); + } + } + + // TODO: + // SINCE FOUR + // head.adaptive_sync(match data.adaptive_sync { + // true => AdaptiveSyncState::Enabled, + // false => AdaptiveSyncState::Disabled, + // }); + + // TODO: + // head.enabled(data.enabled as i32); + head.enabled(true as i32); + if true + /* data.enabled */ + { + if let Some(current_mode) = output.current_mode() { + let wlr_current_mode = wlr_modes + .iter() + .find(|wlr_mode| wlr_mode.data::() == Some(¤t_mode)); + if let Some(wlr_current_mode) = wlr_current_mode { + head.current_mode(wlr_current_mode); + } + } + head.position(output.current_location().x, output.current_location().y); + head.transform(output.current_transform().into()); + head.scale(output.current_scale().fractional_scale()); + } + + (head, wlr_modes) +} + +fn manager_for_configuration<'a, D>( + state: &'a mut D, + configuration: &ZwlrOutputConfigurationV1, +) -> Option<(&'a ZwlrOutputManagerV1, &'a mut OutputManagerData)> +where + D: OutputManagementHandler, +{ + state + .output_management_manager_state() + .managers + .iter_mut() + .find(|(_, manager_data)| manager_data.configurations.contains(configuration)) +} + +impl OutputManagementManagerState { + pub fn new(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch + + Dispatch + + 'static, + F: Fn(&Client) -> bool + Send + Sync + 'static, + { + let global_data = OutputManagementGlobalData { + filter: Box::new(filter), + }; + + display.create_global::(VERSION, global_data); + + Self { + display_handle: display.clone(), + managers: HashMap::new(), + outputs: HashMap::new(), + } + } +} + +impl GlobalDispatch + for OutputManagementManagerState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + OutputManagementHandler, +{ + fn bind( + state: &mut D, + handle: &DisplayHandle, + _client: &Client, + resource: wayland_server::New, + _global_data: &OutputManagementGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + let manager = data_init.init(resource, ()); + + let heads = state + .output_management_manager_state() + .outputs + .keys() + .map(|output| advertise_output::(handle, &manager, output)) + .collect(); + + let serial = u32::from(SERIAL_COUNTER.next_serial()); + + manager.done(serial); + + let state = state.output_management_manager_state(); + + let data = OutputManagerData { + serial, + configurations: Vec::new(), + heads, + }; + + state.managers.insert(manager, data); + } + + fn can_view(client: Client, global_data: &OutputManagementGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for OutputManagementManagerState +where + D: Dispatch + OutputManagementHandler, + D: Dispatch + OutputManagementHandler, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &ZwlrOutputManagerV1, + request: ::Request, + _data: &(), + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_manager_v1::Request::CreateConfiguration { id, serial } => { + let Some(manager_data) = state + .output_management_manager_state() + .managers + .get_mut(resource) + else { + let config = PendingOutputConfiguration { + serial, + inner: Mutex::new(PendingOutputConfigurationInner { + cancelled: false, + pending_heads: HashMap::new(), + }), + }; + + let config = data_init.init(id, config); + + config.cancelled(); + return; + }; + + let pending_heads = manager_data + .heads + .keys() + .map(|head| (head.clone(), PendingHead::NotConfigured)) + .collect::>(); + + let config = PendingOutputConfiguration { + serial, + inner: Mutex::new(PendingOutputConfigurationInner { + cancelled: false, + pending_heads, + }), + }; + + let config = data_init.init(id, config); + + let correct_serial = manager_data.serial == serial; + + if !correct_serial { + config.cancelled(); + return; + } + + manager_data.configurations.push(config); + } + zwlr_output_manager_v1::Request::Stop => { + resource.finished(); + + state + .output_management_manager_state() + .managers + .remove(resource); + } + _ => unreachable!(), + } + } + + fn destroyed(state: &mut D, _client: ClientId, resource: &ZwlrOutputManagerV1, _data: &()) { + state + .output_management_manager_state() + .managers + .remove(resource); + } +} + +impl Dispatch for OutputManagementManagerState { + fn request( + state: &mut D, + client: &Client, + resource: &ZwlrOutputHeadV1, + request: ::Request, + data: &Output, + dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_head_v1::Request::Release => { + // TODO: + } + _ => unreachable!(), + } + } +} + +impl Dispatch for OutputManagementManagerState { + fn request( + state: &mut D, + client: &Client, + resource: &ZwlrOutputModeV1, + request: ::Request, + data: &Mode, + dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_mode_v1::Request::Release => { + // TODO: + } + _ => unreachable!(), + } + } +} + +impl Dispatch + for OutputManagementManagerState +where + D: Dispatch + + Dispatch> + + OutputManagementHandler, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &ZwlrOutputConfigurationV1, + request: ::Request, + pending_data: &PendingOutputConfiguration, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_configuration_v1::Request::EnableHead { id, head } => { + let config_head = + data_init.init(id, Mutex::new(PendingOutputHeadConfiguration::default())); + + let mut data = pending_data.inner.lock().unwrap(); + + let manager_serial = + manager_for_configuration(state, resource).map(|(_, data)| data.serial); + + if manager_serial != Some(pending_data.serial) { + resource.cancelled(); + data.cancelled = true; + } + + if data.cancelled { + return; + } + + if let Some(pending_data) = data.pending_heads.get_mut(&head) { + if !matches!(pending_data, PendingHead::NotConfigured) { + head.post_error( + zwlr_output_configuration_v1::Error::AlreadyConfiguredHead, + "head has already been configured", + ); + return; + } + + *pending_data = PendingHead::Enabled(config_head); + } + } + zwlr_output_configuration_v1::Request::DisableHead { head } => { + let mut data = pending_data.inner.lock().unwrap(); + + let manager_serial = + manager_for_configuration(state, resource).map(|(_, data)| data.serial); + + if manager_serial != Some(pending_data.serial) { + resource.cancelled(); + data.cancelled = true; + } + + if data.cancelled { + return; + } + + if let Some(pending_data) = data.pending_heads.get_mut(&head) { + if !matches!(pending_data, PendingHead::NotConfigured) { + head.post_error( + zwlr_output_configuration_v1::Error::AlreadyConfiguredHead, + "head has already been configured", + ); + return; + } + + *pending_data = PendingHead::Disabled; + } + } + req @ (zwlr_output_configuration_v1::Request::Apply + | zwlr_output_configuration_v1::Request::Test) => { + let mut data = pending_data.inner.lock().unwrap(); + + let manager_serial = + manager_for_configuration(state, resource).map(|(_, data)| data.serial); + + if manager_serial != Some(pending_data.serial) { + resource.cancelled(); + data.cancelled = true; + } + + if data.cancelled { + return; + } + + if data + .pending_heads + .values() + .any(|cfg| matches!(cfg, PendingHead::NotConfigured)) + { + resource.post_error( + zwlr_output_configuration_v1::Error::UnconfiguredHead, + "a head was unconfigured", + ); + return; + } + + let config = data + .pending_heads + .iter() + .map(|(head, head_cfg)| { + let output = head.data::().unwrap().clone(); + + let cfg = match head_cfg { + PendingHead::NotConfigured => unreachable!(), + PendingHead::Enabled(cfg_head) => { + let pending = cfg_head + .data::>() + .unwrap() + .lock() + .unwrap(); + OutputConfiguration::Enabled { + mode: pending.mode, + position: pending.position, + transform: pending.transform, + scale: pending.scale, + adaptive_sync: pending.adaptive_sync, + } + } + PendingHead::Disabled => OutputConfiguration::Disabled, + }; + + (output, cfg) + }) + .collect(); + + let apply = matches!(req, zwlr_output_configuration_v1::Request::Apply); + let success = if apply { + state.apply_configuration(config) + } else { + state.test_configuration(config) + }; + + if success { + resource.succeeded(); + } else { + resource.failed(); + } + } + zwlr_output_configuration_v1::Request::Destroy => (), + _ => unreachable!(), + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + resource: &ZwlrOutputConfigurationV1, + _data: &PendingOutputConfiguration, + ) { + for output_manager_data in state + .output_management_manager_state() + .managers + .values_mut() + { + output_manager_data + .configurations + .retain(|config| config != resource); + } + } +} + +impl Dispatch, D> + for OutputManagementManagerState +where + D: Dispatch + 'static, +{ + fn request( + _state: &mut D, + _client: &Client, + resource: &ZwlrOutputConfigurationHeadV1, + request: ::Request, + data: &Mutex, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_configuration_head_v1::Request::SetMode { mode } => { + let mut data = data.lock().unwrap(); + if data.mode.is_some() { + resource.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + "mode has already been set", + ); + return; + } + + let mode = mode.data::().unwrap(); + + let mode = (mode.size, NonZeroU32::new(mode.refresh as u32)); + + data.mode = Some(mode); + } + zwlr_output_configuration_head_v1::Request::SetCustomMode { + width, + height, + refresh, + } => { + let mut data = data.lock().unwrap(); + if data.mode.is_some() { + resource.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + "mode has already been set", + ); + return; + } + + if width <= 0 || height <= 0 || refresh < 0 { + resource.post_error( + zwlr_output_configuration_head_v1::Error::InvalidCustomMode, + "invalid custom mode", + ); + return; + } + + data.mode = Some(((width, height).into(), NonZeroU32::new(refresh as u32))); + } + zwlr_output_configuration_head_v1::Request::SetPosition { x, y } => { + let mut data = data.lock().unwrap(); + if data.position.is_some() { + resource.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + "position has already been set", + ); + return; + } + + data.position = Some((x, y).into()); + } + zwlr_output_configuration_head_v1::Request::SetTransform { transform } => { + let mut data = data.lock().unwrap(); + if data.transform.is_some() { + resource.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + "transform has already been set", + ); + return; + } + + let transform = match transform { + WEnum::Value(transform) => transform, + WEnum::Unknown(val) => { + resource.post_error( + zwlr_output_configuration_head_v1::Error::InvalidTransform, + format!("transform has an invalid value of {val}"), + ); + return; + } + }; + + data.transform = Some(transform.into()); + } + zwlr_output_configuration_head_v1::Request::SetScale { scale } => { + let mut data = data.lock().unwrap(); + if data.scale.is_some() { + resource.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + "scale has already been set", + ); + return; + } + + data.scale = Some(scale); + } + zwlr_output_configuration_head_v1::Request::SetAdaptiveSync { state } => { + let mut data = data.lock().unwrap(); + if data.adaptive_sync.is_some() { + resource.post_error( + zwlr_output_configuration_head_v1::Error::AlreadySet, + "adaptive sync has already been set", + ); + return; + } + + let adaptive_sync = match state { + WEnum::Value(adaptive_sync) => match adaptive_sync { + AdaptiveSyncState::Disabled => false, + AdaptiveSyncState::Enabled => true, + _ => unreachable!(), + }, + WEnum::Unknown(val) => { + resource.post_error( + zwlr_output_configuration_head_v1::Error::InvalidAdaptiveSyncState, + format!("adaptive sync has an invalid value of {val}"), + ); + return; + } + }; + + data.adaptive_sync = Some(adaptive_sync); + } + _ => unreachable!(), + } + } +} + +#[macro_export] +macro_rules! delegate_output_management { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: $crate::protocol::output_management::OutputManagementGlobalData + ] => $crate::protocol::output_management::OutputManagementManagerState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_manager_v1::ZwlrOutputManagerV1: () + ] => $crate::protocol::output_management::OutputManagementManagerState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_head_v1::ZwlrOutputHeadV1: smithay::output::Output + ] => $crate::protocol::output_management::OutputManagementManagerState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_mode_v1::ZwlrOutputModeV1: smithay::output::Mode + ] => $crate::protocol::output_management::OutputManagementManagerState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_v1::ZwlrOutputConfigurationV1: $crate::protocol::output_management::PendingOutputConfiguration + ] => $crate::protocol::output_management::OutputManagementManagerState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_management::v1::server::zwlr_output_configuration_head_v1::ZwlrOutputConfigurationHeadV1: std::sync::Mutex<$crate::protocol::output_management::PendingOutputHeadConfiguration> + ] => $crate::protocol::output_management::OutputManagementManagerState); + }; +} diff --git a/src/state.rs b/src/state.rs index 3dc6d32..3608a86 100644 --- a/src/state.rs +++ b/src/state.rs @@ -12,6 +12,7 @@ use crate::{ protocol::{ foreign_toplevel::{self, ForeignToplevelManagerState}, gamma_control::GammaControlManagerState, + output_management::OutputManagementManagerState, screencopy::ScreencopyManagerState, }, window::WindowElement, @@ -52,7 +53,12 @@ use smithay::{ }, xwayland::{X11Wm, XWaylandClientData}, }; -use std::{cell::RefCell, collections::HashMap, path::PathBuf, sync::Arc}; +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + path::PathBuf, + sync::Arc, +}; use sysinfo::{ProcessRefreshKind, RefreshKind}; use tracing::{info, warn}; use xdg::BaseDirectories; @@ -101,6 +107,7 @@ pub struct Pinnacle { pub session_lock_manager_state: SessionLockManagerState, pub xwayland_shell_state: XWaylandShellState, pub idle_notifier_state: IdleNotifierState, + pub output_management_manager_state: OutputManagementManagerState, pub lock_state: LockState, @@ -139,6 +146,9 @@ pub struct Pinnacle { /// A cache of surfaces to their root surface. pub root_surface_cache: HashMap, + + /// WlSurfaces with an attached idle inhibitor. + pub idle_inhibiting_surfaces: HashSet, } impl State { @@ -148,6 +158,7 @@ impl State { self.pinnacle.popup_manager.cleanup(); self.update_pointer_focus(); foreign_toplevel::refresh(self); + self.pinnacle.refresh_idle_inhibit(); if let Backend::Winit(winit) = &mut self.backend { winit.render_if_scheduled(&mut self.pinnacle); @@ -290,6 +301,10 @@ impl Pinnacle { ), xwayland_shell_state: XWaylandShellState::new::(&display_handle), idle_notifier_state: IdleNotifierState::new(&display_handle, loop_handle), + output_management_manager_state: OutputManagementManagerState::new::( + &display_handle, + filter_restricted_client, + ), lock_state: LockState::default(), @@ -326,6 +341,8 @@ impl Pinnacle { layout_state: LayoutState::default(), root_surface_cache: HashMap::new(), + + idle_inhibiting_surfaces: HashSet::new(), }; Ok(pinnacle) From 4b3fbd716f008965d71fe67d2dba479f4abb0f80 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sun, 2 Jun 2024 18:52:35 -0500 Subject: [PATCH 02/15] Add output disabling Still needs an API call --- src/api.rs | 33 +++++-- src/backend.rs | 11 +++ src/backend/dummy.rs | 18 +--- src/backend/udev.rs | 151 +++++++++++++++--------------- src/backend/winit.rs | 5 + src/handlers.rs | 42 ++++++++- src/output.rs | 113 +++++++++++++++++----- src/protocol/output_management.rs | 30 +++--- src/state.rs | 9 +- 9 files changed, 274 insertions(+), 138 deletions(-) diff --git a/src/api.rs b/src/api.rs index 852b506..d501f1d 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1035,9 +1035,14 @@ impl output_service_server::OutputService for OutputService { if let Some(y) = y { loc.y = y; } - state - .pinnacle - .change_output_state(&output, None, None, None, Some(loc)); + state.pinnacle.change_output_state( + &mut state.backend, + &output, + None, + None, + None, + Some(loc), + ); debug!("Mapping output {} to {loc:?}", output.name()); state.pinnacle.request_layout(&output); }) @@ -1067,7 +1072,15 @@ impl output_service_server::OutputService for OutputService { return; }; - state.resize_output(&output, mode); + state.pinnacle.change_output_state( + &mut state.backend, + &output, + Some(mode), + None, + None, + None, + ); + state.pinnacle.request_layout(&output); }) .await } @@ -1102,6 +1115,7 @@ impl output_service_server::OutputService for OutputService { }); state.pinnacle.change_output_state( + &mut state.backend, &output, None, None, @@ -1154,9 +1168,14 @@ impl output_service_server::OutputService for OutputService { return; }; - state - .pinnacle - .change_output_state(&output, None, Some(smithay_transform), None, None); + state.pinnacle.change_output_state( + &mut state.backend, + &output, + None, + Some(smithay_transform), + None, + None, + ); state.pinnacle.request_layout(&output); state.schedule_render(&output); }) diff --git a/src/backend.rs b/src/backend.rs index cbd8490..2a0b8c8 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -151,6 +151,8 @@ 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); } impl BackendData for Backend { @@ -180,6 +182,15 @@ impl BackendData for Backend { Backend::Dummy(dummy) => dummy.early_import(surface), } } + + fn set_output_mode(&mut self, output: &Output, mode: smithay::output::Mode) { + match self { + Backend::Winit(winit) => winit.set_output_mode(output, mode), + Backend::Udev(udev) => udev.set_output_mode(output, mode), + #[cfg(feature = "testing")] + Backend::Dummy(dummy) => dummy.set_output_mode(output, mode), + } + } } /// Update surface primary scanout outputs and send frames and dmabuf feedback to visible windows diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs index 274757f..2170976 100644 --- a/src/backend/dummy.rs +++ b/src/backend/dummy.rs @@ -1,6 +1,4 @@ -use pinnacle_api_defs::pinnacle::signal::v0alpha1::{ - OutputConnectResponse, OutputDisconnectResponse, -}; +use pinnacle_api_defs::pinnacle::signal::v0alpha1::OutputConnectResponse; use smithay::backend::renderer::test::DummyRenderer; use smithay::backend::renderer::ImportMemWl; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; @@ -50,6 +48,10 @@ impl BackendData for Dummy { fn reset_buffers(&mut self, _output: &Output) {} fn early_import(&mut self, _surface: &WlSurface) {} + + fn set_output_mode(&mut self, _output: &Output, _mode: smithay::output::Mode) { + // TODO: + } } impl Dummy { @@ -141,14 +143,4 @@ impl Pinnacle { }); }); } - - pub fn remove_output(&mut self, output: &Output) { - self.space.unmap_output(output); - - self.signal_state.output_disconnect.signal(|buffer| { - buffer.push_back(OutputDisconnectResponse { - output_name: Some(output.name()), - }) - }); - } } diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 6d9fe26..519a9a9 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -71,7 +71,6 @@ use smithay::{ presentation_time::server::wp_presentation_feedback, }, wayland_server::{ - backend::GlobalId, protocol::{wl_shm, wl_surface::WlSurface}, DisplayHandle, }, @@ -527,9 +526,12 @@ impl Udev { /// Schedule a new render that will cause the compositor to redraw everything. pub fn schedule_render(&mut self, loop_handle: &LoopHandle, output: &Output) { let Some(surface) = render_surface_for_output(output, &mut self.backends) else { + tracing::info!("no render surface on output {}", output.name()); return; }; + // tracing::info!(state = ?surface.render_state, name = output.name()); + match &surface.render_state { RenderState::Idle => { let output = output.clone(); @@ -577,6 +579,12 @@ impl Udev { { warn!("Failed to reset compositor state on crtc {crtc:?}: {err}"); } + + if let Some(surface) = render_surface_for_output(output, &mut self.backends) { + if let RenderState::Scheduled(idle) = std::mem::take(&mut surface.render_state) { + idle.cancel(); + } + } } } } @@ -638,51 +646,6 @@ impl State { // ); } } - - /// Resize the output with the given mode. - /// - /// TODO: This is in udev.rs but is also used in winit.rs. - /// | I've got no clue how to make things public without making a mess. - pub fn resize_output(&mut self, output: &Output, mode: smithay::output::Mode) { - if let Backend::Udev(udev) = &mut self.backend { - let drm_mode = udev.backends.iter().find_map(|(_, backend)| { - backend - .drm_scanner - .crtcs() - .find(|(_, handle)| { - output - .user_data() - .get::() - .is_some_and(|data| &data.crtc == handle) - }) - .and_then(|(info, _)| { - info.modes() - .iter() - .find(|m| smithay::output::Mode::from(**m) == mode) - }) - .copied() - }); - - if let Some(drm_mode) = drm_mode { - if let Some(render_surface) = render_surface_for_output(output, &mut udev.backends) - { - match render_surface.compositor.use_mode(drm_mode) { - Ok(()) => { - self.pinnacle - .change_output_state(output, Some(mode), None, None, None); - } - Err(err) => error!("Failed to resize output: {err}"), - } - } - } - } else { - self.pinnacle - .change_output_state(output, Some(mode), None, None, None); - } - - self.pinnacle.request_layout(output); - self.schedule_render(output); - } } impl BackendData for Udev { @@ -705,6 +668,45 @@ impl BackendData for Udev { warn!("early buffer import failed: {}", err); } } + + fn set_output_mode(&mut self, output: &Output, mode: smithay::output::Mode) { + let drm_mode = self.backends.iter().find_map(|(_, backend)| { + backend + .drm_scanner + .crtcs() + .find(|(_, handle)| { + output + .user_data() + .get::() + .is_some_and(|data| &data.crtc == handle) + }) + .and_then(|(info, _)| { + info.modes() + .iter() + .find(|m| smithay::output::Mode::from(**m) == mode) + }) + .copied() + }); + + if let Some(drm_mode) = drm_mode { + if let Some(render_surface) = render_surface_for_output(output, &mut self.backends) { + match render_surface.compositor.use_mode(drm_mode) { + Ok(()) => { + output.change_current_state(Some(mode), None, None, None); + output.with_state_mut(|state| { + if !state.modes.contains(&mode) { + state.modes.push(mode); + } + }); + } + Err(err) => warn!("Failed to resize output: {err}"), + } + } + } else { + // TODO: create new drm mode with cvt + tracing::info!("no drm mode for mode"); + } + } } // TODO: document desperately @@ -810,7 +812,6 @@ enum RenderState { /// The idle token from a render being scheduled. /// This is used to cancel renders if, for example, /// the output being rendered is removed. - #[allow(dead_code)] // TODO: Idle<'static>, ), /// A frame was rendered and scheduled and we are waiting for vblank. @@ -823,10 +824,6 @@ enum RenderState { /// Render surface for an output. struct RenderSurface { - /// The output global id. - global: Option, - /// A display handle used to remove the global on drop. - display_handle: DisplayHandle, /// The node from `connector_connected`. device_id: DrmNode, /// The node rendering to the screen? idk @@ -862,15 +859,6 @@ struct ScreencopyCommitState { _cursor: CommitCounter, } -impl Drop for RenderSurface { - // Stop advertising this output to clients on drop. - fn drop(&mut self) { - if let Some(global) = self.global.take() { - self.display_handle.remove_global::(global); - } - } -} - type GbmDrmCompositor = DrmCompositor< GbmAllocator, GbmDevice, @@ -1055,11 +1043,12 @@ impl Udev { ); let global = output.create_global::(&self.display_handle); - pinnacle - .output_management_manager_state - .add_head::(&output); + pinnacle.outputs.insert(output.clone(), global); - output.with_state_mut(|state| state.serial = serial); + output.with_state_mut(|state| { + state.serial = serial; + state.powered = true; + }); output.set_preferred(wl_mode); @@ -1071,6 +1060,10 @@ impl Udev { .collect::>(); output.with_state_mut(|state| state.modes = modes); + pinnacle + .output_management_manager_state + .add_head::(&output); + let x = pinnacle.space.outputs().fold(0, |acc, o| { let Some(geo) = pinnacle.space.output_geometry(o) else { unreachable!() @@ -1131,10 +1124,8 @@ impl Udev { ); let surface = RenderSurface { - display_handle: self.display_handle.clone(), device_id: node, render_node: device.render_node, - global: Some(global), compositor, dmabuf_feedback, render_state: RenderState::Idle, @@ -1145,7 +1136,7 @@ impl Udev { device.surfaces.insert(crtc, surface); - pinnacle.change_output_state(&output, Some(wl_mode), None, None, Some(position)); + pinnacle.change_output_state(self, &output, Some(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. @@ -1157,7 +1148,7 @@ impl Udev { { let ConnectorSavedState { loc, tags, scale } = saved_state; output.with_state_mut(|state| state.tags.clone_from(tags)); - pinnacle.change_output_state(&output, None, None, *scale, Some(*loc)); + pinnacle.change_output_state(self, &output, None, None, *scale, Some(*loc)); } else { pinnacle.signal_state.output_connect.signal(|buffer| { buffer.push_back(OutputConnectResponse { @@ -1223,6 +1214,11 @@ impl Udev { pinnacle .output_management_manager_state .remove_head(&output); + + if let Some(global) = pinnacle.outputs.remove(&output) { + // TODO: disable ahead of time + pinnacle.display_handle.remove_global::(global); + } } } @@ -1298,11 +1294,15 @@ impl Udev { return; }; - let output = if let Some(output) = pinnacle.space.outputs().find(|o| { - let udev_op_data = o.user_data().get::(); - udev_op_data - .is_some_and(|data| data.device_id == surface.device_id && data.crtc == crtc) - }) { + let output = if let Some(output) = pinnacle + .outputs + .keys() + .chain(pinnacle.unmapped_outputs.iter()) + .find(|o| { + let udev_op_data = o.user_data().get::(); + udev_op_data + .is_some_and(|data| data.device_id == surface.device_id && data.crtc == crtc) + }) { output.clone() } else { // somehow we got called with an invalid output @@ -1394,6 +1394,11 @@ impl Udev { assert!(matches!(surface.render_state, RenderState::Scheduled(_))); + if !pinnacle.outputs.contains_key(output) { + surface.render_state = RenderState::Idle; + return; + } + // TODO: possibly lift this out and make it so that scheduling a render // does nothing on powered off outputs if output.with_state(|state| !state.powered) { diff --git a/src/backend/winit.rs b/src/backend/winit.rs index f02ec8f..c3f2261 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -67,6 +67,10 @@ 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); + } } impl Backend { @@ -201,6 +205,7 @@ impl Winit { refresh: 144_000, }; state.pinnacle.change_output_state( + &mut state.backend, &output, Some(mode), None, diff --git a/src/handlers.rs b/src/handlers.rs index 2194fdd..5a0d3dd 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -22,7 +22,7 @@ use smithay::{ pointer::{CursorImageStatus, PointerHandle}, Seat, SeatHandler, SeatState, }, - output::{Output, Scale}, + output::{Mode, Output, Scale}, reexports::{ calloop::Interest, wayland_protocols::xdg::shell::server::xdg_positioner::ConstraintAdjustment, @@ -930,21 +930,55 @@ impl OutputManagementHandler for State { fn apply_configuration(&mut self, config: HashMap) -> bool { for (output, config) in config { match config { - OutputConfiguration::Disabled => todo!(), + OutputConfiguration::Disabled => { + self.pinnacle.set_output_enabled(&output, false); + // TODO: split + self.backend.set_output_powered(&output, false); + } OutputConfiguration::Enabled { mode, position, transform, scale, - adaptive_sync, + adaptive_sync: _, } => { + self.pinnacle.set_output_enabled(&output, true); + // TODO: split + self.backend.set_output_powered(&output, true); + + self.schedule_render(&output); + let snapshots = self.backend.with_renderer(|renderer| { capture_snapshots_on_output(&mut self.pinnacle, renderer, &output, []) }); + let mode = mode.map(|(size, refresh)| { + if let Some(refresh) = refresh { + Mode { + size, + refresh: refresh.get() as i32, + } + } else { + output + .with_state(|state| { + state + .modes + .iter() + .filter(|mode| mode.size == size) + .max_by_key(|mode| mode.refresh) + .copied() + }) + .unwrap_or(Mode { + size, + refresh: 60_000, + }) + } + }); + self.pinnacle.change_output_state( + &mut self.backend, &output, - None, + mode, transform, scale.map(Scale::Fractional), position, diff --git a/src/output.rs b/src/output.rs index 66e5503..f00ae21 100644 --- a/src/output.rs +++ b/src/output.rs @@ -2,7 +2,9 @@ use std::{cell::RefCell, num::NonZeroU32}; -use pinnacle_api_defs::pinnacle::signal::v0alpha1::{OutputMoveResponse, OutputResizeResponse}; +use pinnacle_api_defs::pinnacle::signal::v0alpha1::{ + OutputDisconnectResponse, OutputMoveResponse, OutputResizeResponse, +}; use smithay::{ desktop::layer_map_for_output, output::{Mode, Output, Scale}, @@ -12,6 +14,8 @@ use smithay::{ }; use crate::{ + backend::BackendData, + config::ConnectorSavedState, focus::WindowKeyboardFocusStack, layout::transaction::{LayoutTransaction, SnapshotTarget}, protocol::screencopy::Screencopy, @@ -51,7 +55,7 @@ pub enum BlankingState { } /// The state of an output -#[derive(Debug)] +#[derive(Debug, Default)] pub struct OutputState { pub tags: Vec, pub focus_stack: WindowKeyboardFocusStack, @@ -69,22 +73,6 @@ pub struct OutputState { pub powered: bool, } -impl Default for OutputState { - fn default() -> Self { - Self { - tags: Default::default(), - focus_stack: Default::default(), - screencopy: Default::default(), - serial: Default::default(), - modes: Default::default(), - lock_surface: Default::default(), - blanking_state: Default::default(), - layout_transaction: Default::default(), - powered: true, - } - } -} - impl WithState for Output { type State = OutputState; @@ -135,10 +123,9 @@ impl OutputState { } impl Pinnacle { - /// A wrapper around [`Output::change_current_state`] that additionally sends an output - /// geometry signal. pub fn change_output_state( &mut self, + backend: &mut impl BackendData, output: &Output, mode: Option, transform: Option, @@ -147,7 +134,7 @@ impl Pinnacle { ) { let old_scale = output.current_scale().fractional_scale(); - output.change_current_state(mode, transform, scale, location); + output.change_current_state(None, transform, scale, location); if let Some(location) = location { self.space.map_output(output, location); self.signal_state.output_move.signal(|buf| { @@ -158,6 +145,11 @@ impl Pinnacle { }); }); } + + if let Some(mode) = mode { + backend.set_output_mode(output, mode); + } + if mode.is_some() || transform.is_some() || scale.is_some() { layer_map_for_output(output).arrange(); self.signal_state.output_resize.signal(|buf| { @@ -169,10 +161,6 @@ impl Pinnacle { }); }); } - if let Some(mode) = mode { - output.set_preferred(mode); - output.with_state_mut(|state| state.modes.push(mode)); - } if let Some(scale) = scale { let pos_multiplier = old_scale / scale.fractional_scale(); @@ -223,4 +211,79 @@ impl Pinnacle { self.output_management_manager_state .update_head::(output); } + + pub fn set_output_enabled(&mut self, output: &Output, enabled: bool) { + if enabled { + self.unmapped_outputs.remove(output); + if !self.outputs.contains_key(output) { + let global = output.create_global::(&self.display_handle); + self.outputs.insert(output.clone(), global); + } + self.space.map_output(output, output.current_location()); + + // TODO: output connect? + } else { + let global = self.outputs.remove(output); + if let Some(global) = global { + self.display_handle.remove_global::(global); + } + self.space.unmap_output(output); + self.unmapped_outputs.insert(output.clone()); + + // TODO: should this trigger the signal? + self.signal_state.output_disconnect.signal(|buffer| { + buffer.push_back(OutputDisconnectResponse { + output_name: Some(output.name()), + }) + }); + + self.gamma_control_manager_state.output_removed(output); + + self.config.connector_saved_states.insert( + OutputName(output.name()), + ConnectorSavedState { + loc: output.current_location(), + tags: output.with_state(|state| state.tags.clone()), + scale: Some(output.current_scale()), + }, + ); + + for layer in layer_map_for_output(output).layers() { + layer.layer_surface().send_close(); + } + } + } + + pub fn remove_output(&mut self, output: &Output) { + let global = self.outputs.remove(output); + if let Some(global) = global { + self.display_handle.remove_global::(global); + } + self.unmapped_outputs.remove(output); + + self.space.unmap_output(output); + + self.gamma_control_manager_state.output_removed(output); + + self.output_management_manager_state.remove_head(output); + + self.signal_state.output_disconnect.signal(|buffer| { + buffer.push_back(OutputDisconnectResponse { + output_name: Some(output.name()), + }) + }); + + self.config.connector_saved_states.insert( + OutputName(output.name()), + ConnectorSavedState { + loc: output.current_location(), + tags: output.with_state(|state| state.tags.clone()), + scale: Some(output.current_scale()), + }, + ); + + for layer in layer_map_for_output(output).layers() { + layer.layer_surface().send_close(); + } + } } diff --git a/src/protocol/output_management.rs b/src/protocol/output_management.rs index e3eb3b4..d975b58 100644 --- a/src/protocol/output_management.rs +++ b/src/protocol/output_management.rs @@ -101,7 +101,7 @@ pub struct OutputData { position: Point, transform: Transform, scale: f64, - adaptive_sync: bool, + _adaptive_sync: bool, } impl OutputManagementManagerState { @@ -127,7 +127,7 @@ impl OutputManagementManagerState { position: output.current_location(), transform: output.current_transform(), scale: output.current_scale().fractional_scale(), - adaptive_sync: false, // TODO: + _adaptive_sync: false, // TODO: }; self.outputs.insert(output.clone(), output_data); @@ -336,7 +336,7 @@ where head.physical_size(physical_props.size.w, physical_props.size.h); let mut wlr_modes = Vec::new(); - for mode in output.modes() { + for mode in output.with_state(|state| state.modes.clone()) { let wlr_mode = client .create_resource::(display, manager.version(), mode) .unwrap(); @@ -551,13 +551,13 @@ where impl Dispatch for OutputManagementManagerState { fn request( - state: &mut D, - client: &Client, - resource: &ZwlrOutputHeadV1, + _state: &mut D, + _client: &Client, + _resource: &ZwlrOutputHeadV1, request: ::Request, - data: &Output, - dhandle: &DisplayHandle, - data_init: &mut DataInit<'_, D>, + _data: &Output, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, ) { match request { zwlr_output_head_v1::Request::Release => { @@ -570,13 +570,13 @@ impl Dispatch for OutputManagementManagerState { impl Dispatch for OutputManagementManagerState { fn request( - state: &mut D, - client: &Client, - resource: &ZwlrOutputModeV1, + _state: &mut D, + _client: &Client, + _resource: &ZwlrOutputModeV1, request: ::Request, - data: &Mode, - dhandle: &DisplayHandle, - data_init: &mut DataInit<'_, D>, + _data: &Mode, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, ) { match request { zwlr_output_mode_v1::Request::Release => { diff --git a/src/state.rs b/src/state.rs index 3608a86..4c19287 100644 --- a/src/state.rs +++ b/src/state.rs @@ -22,10 +22,11 @@ use pinnacle_api_defs::pinnacle::v0alpha1::ShutdownWatchResponse; use smithay::{ desktop::{PopupManager, Space}, input::{keyboard::XkbConfig, pointer::CursorImageStatus, Seat, SeatState}, + output::Output, reexports::{ calloop::{generic::Generic, Interest, LoopHandle, LoopSignal, Mode, PostAction}, wayland_server::{ - backend::{ClientData, ClientId, DisconnectReason}, + backend::{ClientData, ClientId, DisconnectReason, GlobalId}, protocol::wl_surface::WlSurface, Client, Display, DisplayHandle, }, @@ -149,6 +150,9 @@ pub struct Pinnacle { /// WlSurfaces with an attached idle inhibitor. pub idle_inhibiting_surfaces: HashSet, + + pub outputs: HashMap, + pub unmapped_outputs: HashSet, } impl State { @@ -343,6 +347,9 @@ impl Pinnacle { root_surface_cache: HashMap::new(), idle_inhibiting_surfaces: HashSet::new(), + + outputs: HashMap::new(), + unmapped_outputs: HashSet::new(), }; Ok(pinnacle) From 34517fd111137ecc2dbec599dd54ce1eba00e06a Mon Sep 17 00:00:00 2001 From: Ottatop Date: Sun, 2 Jun 2024 21:28:44 -0500 Subject: [PATCH 03/15] Add custom modes Custom modes from wlr-randr should work now. Still need to do custom mode*lines*. And add those API calls --- Cargo.lock | 8 +- Cargo.toml | 8 +- src/backend/dummy.rs | 6 +- src/backend/udev.rs | 19 +++- src/backend/udev/drm/util.rs | 167 ++++++++++++++++++++++++++++++++++- 5 files changed, 196 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2ca851..8fb6133 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1393,6 +1393,11 @@ version = "0.2.154" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +[[package]] +name = "libdisplay-info-sys" +version = "0.1.0" +source = "git+https://github.com/Smithay/libdisplay-info-rs?rev=a482d0d#a482d0d4b71762c13d40fa394efe04473916f31c" + [[package]] name = "libloading" version = "0.7.4" @@ -1851,8 +1856,10 @@ dependencies = [ "clap", "cliclack", "dircpy", + "drm-sys", "gag", "image", + "libdisplay-info-sys", "pinnacle", "pinnacle-api", "pinnacle-api-defs", @@ -2381,7 +2388,6 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "smithay" version = "0.3.0" -source = "git+https://github.com/Smithay/smithay?rev=900b938#900b938970081cb525dc94ff083d76aa07c60e54" dependencies = [ "appendlist", "ash", diff --git a/Cargo.toml b/Cargo.toml index bda303a..3782a89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,9 +34,9 @@ dircpy = "0.3.16" tempfile = "3.10.1" [workspace.dependencies.smithay] -git = "https://github.com/Smithay/smithay" -rev = "900b938" -# path = "../../git/smithay" +# git = "https://github.com/Smithay/smithay" +# rev = "900b938" +path = "../../git/smithay" default-features = false features = [ "desktop", @@ -115,6 +115,8 @@ chrono = "0.4.38" bytemuck = "1.16.0" pinnacle-api = { path = "./api/rust" } gag = "1.0.0" +drm-sys = "0.7.0" +libdisplay-info-sys = { git = "https://github.com/Smithay/libdisplay-info-rs", rev = "a482d0d" } [build-dependencies] vergen = { version = "8.3.1", features = ["git", "gitcl", "rustc", "cargo", "si"] } diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs index 2170976..98d662a 100644 --- a/src/backend/dummy.rs +++ b/src/backend/dummy.rs @@ -28,6 +28,7 @@ pub struct Dummy { // pub dmabuf_state: (DmabufState, DmabufGlobal, Option), #[cfg(feature = "wlcs")] pub wlcs_state: Wlcs, + pub output: Output, } impl Backend { @@ -49,8 +50,8 @@ impl BackendData for Dummy { fn early_import(&mut self, _surface: &WlSurface) {} - fn set_output_mode(&mut self, _output: &Output, _mode: smithay::output::Mode) { - // TODO: + fn set_output_mode(&mut self, output: &Output, mode: smithay::output::Mode) { + output.change_current_state(Some(mode), None, None, None); } } @@ -87,6 +88,7 @@ impl Dummy { // dmabuf_state, #[cfg(feature = "wlcs")] wlcs_state: Wlcs::default(), + output: output.clone(), }; UninitBackend { diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 519a9a9..ad588a1 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -10,7 +10,7 @@ use std::{ }; use anyhow::{anyhow, ensure, Context}; -use drm::set_crtc_active; +use drm::{set_crtc_active, util::create_drm_mode}; use pinnacle_api_defs::pinnacle::signal::v0alpha1::{ OutputConnectResponse, OutputDisconnectResponse, }; @@ -703,8 +703,21 @@ impl BackendData for Udev { } } } else { - // TODO: create new drm mode with cvt - tracing::info!("no drm mode for mode"); + let new_mode = create_drm_mode(mode.size.w, mode.size.h, Some(mode.refresh as u32)); + + if let Some(render_surface) = render_surface_for_output(output, &mut self.backends) { + match render_surface.compositor.use_mode(new_mode) { + Ok(()) => { + output.change_current_state(Some(mode), None, None, None); + output.with_state_mut(|state| { + if !state.modes.contains(&mode) { + state.modes.push(mode); + } + }); + } + Err(err) => warn!("Failed to resize output: {err}"), + } + } } } } diff --git a/src/backend/udev/drm/util.rs b/src/backend/udev/drm/util.rs index 02b9396..25dfd16 100644 --- a/src/backend/udev/drm/util.rs +++ b/src/backend/udev/drm/util.rs @@ -1,7 +1,18 @@ -use std::num::NonZeroU32; +use std::{ffi::CString, io::Write, mem::MaybeUninit, num::NonZeroU32}; -use anyhow::Context; -use smithay::reexports::drm::control::{connector, property, Device, ResourceHandle}; +use anyhow::{bail, 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, +}; +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 smithay::reexports::drm::{ + self, + control::{connector, property, Device, ResourceHandle}, +}; use super::edid_manus::get_manufacturer; @@ -129,3 +140,153 @@ 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(); + + 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 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 args.next().context("no +/-vsync specified")? { + "+vsync" => flags |= DRM_MODE_FLAG_PVSYNC, + "-vsync" => flags |= DRM_MODE_FLAG_NVSYNC, + _ => bail!("invalid vsync specifier"), + }; + + let type_ = DRM_MODE_TYPE_USERDEF; + + let name = CString::new(format!("{}x{}@{}", hdisplay, vdisplay, vrefresh / 1000)).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, + }) +} + +/// Create a new drm mode from a given width, height, and optional refresh rate (defaults to 60Hz). +pub fn create_drm_mode(width: i32, height: i32, refresh_mhz: Option) -> drm::control::Mode { + drm::control::Mode::from(generate_cvt_mode( + width, + height, + refresh_mhz.map(|refresh| refresh as f64 / 1000.0), + )) +} + +// From https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/95ac3e99242b4e7f59f00dd073ede405ff8e9e26/backend/drm/util.c#L247 +fn generate_cvt_mode(hdisplay: i32, vdisplay: i32, vrefresh: Option) -> drm_mode_modeinfo { + let options: di_cvt_options = di_cvt_options { + red_blank_ver: di_cvt_reduced_blanking_version_DI_CVT_REDUCED_BLANKING_NONE, + h_pixels: hdisplay, + v_lines: vdisplay, + ip_freq_rqd: vrefresh.unwrap_or(60.0), + video_opt: false, + vblank: 0.0, + additional_hblank: 0, + early_vsync_rqd: false, + int_rqd: false, + margins_rqd: false, + }; + + let mut timing = MaybeUninit::::zeroed(); + // SAFETY: is an ffi function + unsafe { di_cvt_compute(timing.as_mut_ptr(), &options as *const _) }; + + // SAFETY: Initialized in the function above + let timing = unsafe { timing.assume_init() }; + + let hsync_start = (hdisplay + timing.h_front_porch as i32) as u16; + let vsync_start = (timing.v_lines_rnd + timing.v_front_porch) as u16; + let hsync_end = hsync_start + timing.h_sync as u16; + let vsync_end = vsync_start + timing.v_sync as u16; + + let name = CString::new(format!("{}x{}", hdisplay, vdisplay)).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); + + drm_mode_modeinfo { + clock: f64::round(timing.act_pixel_freq * 1000.0) as u32, + hdisplay: hdisplay as u16, + hsync_start, + hsync_end, + htotal: hsync_end + timing.h_back_porch as u16, + hskew: 0, + vdisplay: timing.v_lines_rnd as u16, + vsync_start, + vsync_end, + vtotal: vsync_end + timing.v_back_porch as u16, + vscan: 0, + vrefresh: f64::round(timing.act_frame_rate) as u32, + flags: DRM_MODE_FLAG_NHSYNC | DRM_MODE_FLAG_PVSYNC, + type_: DRM_MODE_TYPE_USERDEF, + name, + } +} From 1f3a50438734472d0a7ef934926d11f4e54c56be Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 3 Jun 2024 19:50:34 -0500 Subject: [PATCH 04/15] Fix serial handling, remove unwraps --- src/api.rs | 16 + src/backend/udev.rs | 90 +++--- src/handlers.rs | 3 + src/output.rs | 4 +- src/protocol/output_management.rs | 488 +++++++++++++++++++----------- 5 files changed, 371 insertions(+), 230 deletions(-) diff --git a/src/api.rs b/src/api.rs index d501f1d..c7d60da 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1045,6 +1045,10 @@ impl output_service_server::OutputService for OutputService { ); debug!("Mapping output {} to {loc:?}", output.name()); state.pinnacle.request_layout(&output); + state + .pinnacle + .output_management_manager_state + .update::(); }) .await } @@ -1081,6 +1085,10 @@ impl output_service_server::OutputService for OutputService { None, ); state.pinnacle.request_layout(&output); + state + .pinnacle + .output_management_manager_state + .update::(); }) .await } @@ -1135,6 +1143,10 @@ impl output_service_server::OutputService for OutputService { state.pinnacle.request_layout(&output); state.schedule_render(&output); + state + .pinnacle + .output_management_manager_state + .update::(); }) .await } @@ -1178,6 +1190,10 @@ impl output_service_server::OutputService for OutputService { ); state.pinnacle.request_layout(&output); state.schedule_render(&output); + state + .pinnacle + .output_management_manager_state + .update::(); }) .await } diff --git a/src/backend/udev.rs b/src/backend/udev.rs index ad588a1..9c3c6b0 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -670,53 +670,50 @@ impl BackendData for Udev { } fn set_output_mode(&mut self, output: &Output, mode: smithay::output::Mode) { - let drm_mode = self.backends.iter().find_map(|(_, backend)| { - backend - .drm_scanner - .crtcs() - .find(|(_, handle)| { - output - .user_data() - .get::() - .is_some_and(|data| &data.crtc == handle) - }) - .and_then(|(info, _)| { - info.modes() - .iter() - .find(|m| smithay::output::Mode::from(**m) == mode) - }) - .copied() - }); + let drm_mode = self + .backends + .iter() + .find_map(|(_, backend)| { + backend + .drm_scanner + .crtcs() + .find(|(_, handle)| { + output + .user_data() + .get::() + .is_some_and(|data| &data.crtc == handle) + }) + .and_then(|(info, _)| { + info.modes() + .iter() + .find(|m| smithay::output::Mode::from(**m) == mode) + }) + .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)) + }); - if let Some(drm_mode) = drm_mode { - if let Some(render_surface) = render_surface_for_output(output, &mut self.backends) { - match render_surface.compositor.use_mode(drm_mode) { - Ok(()) => { - output.change_current_state(Some(mode), None, None, None); - output.with_state_mut(|state| { - if !state.modes.contains(&mode) { - state.modes.push(mode); - } - }); - } - Err(err) => warn!("Failed to resize output: {err}"), - } - } - } else { - let new_mode = create_drm_mode(mode.size.w, mode.size.h, Some(mode.refresh as u32)); - - if let Some(render_surface) = render_surface_for_output(output, &mut self.backends) { - match render_surface.compositor.use_mode(new_mode) { - Ok(()) => { - output.change_current_state(Some(mode), None, None, None); - output.with_state_mut(|state| { - if !state.modes.contains(&mode) { - state.modes.push(mode); - } - }); - } - Err(err) => warn!("Failed to resize output: {err}"), + if let Some(render_surface) = render_surface_for_output(output, &mut self.backends) { + match render_surface.compositor.use_mode(drm_mode) { + Ok(()) => { + info!( + "Set {}'s mode to {}x{}@{:.3}Hz", + output.name(), + mode.size.w, + mode.size.h, + mode.refresh as f64 / 1000.0 + ); + output.change_current_state(Some(mode), None, None, None); + output.with_state_mut(|state| { + // TODO: push or no? + if !state.modes.contains(&mode) { + state.modes.push(mode); + } + }); } + Err(err) => warn!("Failed to set output mode for {}: {err}", output.name()), } } } @@ -1169,6 +1166,8 @@ impl Udev { }) }); } + + pinnacle.output_management_manager_state.update::(); } /// A display was unplugged. @@ -1227,6 +1226,7 @@ impl Udev { pinnacle .output_management_manager_state .remove_head(&output); + pinnacle.output_management_manager_state.update::(); if let Some(global) = pinnacle.outputs.remove(&output) { // TODO: disable ahead of time diff --git a/src/handlers.rs b/src/handlers.rs index 5a0d3dd..f29a2ab 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -998,6 +998,9 @@ impl OutputManagementHandler for State { } } } + self.pinnacle + .output_management_manager_state + .update::(); true } diff --git a/src/output.rs b/src/output.rs index f00ae21..d9f7bb1 100644 --- a/src/output.rs +++ b/src/output.rs @@ -207,9 +207,6 @@ impl Pinnacle { lock_surface.send_configure(); } - - self.output_management_manager_state - .update_head::(output); } pub fn set_output_enabled(&mut self, output: &Output, enabled: bool) { @@ -266,6 +263,7 @@ impl Pinnacle { self.gamma_control_manager_state.output_removed(output); self.output_management_manager_state.remove_head(output); + self.output_management_manager_state.update::(); self.signal_state.output_disconnect.signal(|buffer| { buffer.push_back(OutputDisconnectResponse { diff --git a/src/protocol/output_management.rs b/src/protocol/output_management.rs index d975b58..c023a9c 100644 --- a/src/protocol/output_management.rs +++ b/src/protocol/output_management.rs @@ -1,3 +1,4 @@ +use anyhow::Context; use smithay::{ output::Output, reexports::{ @@ -11,7 +12,12 @@ use smithay::{ }, utils::{Logical, Physical, Point, Size, Transform, SERIAL_COUNTER}, }; -use std::{collections::HashMap, num::NonZeroU32, sync::Mutex}; +use std::{ + collections::{HashMap, HashSet}, + num::NonZeroU32, + sync::Mutex, +}; +use tracing::error; use smithay::{ output::Mode, @@ -35,6 +41,7 @@ pub struct OutputManagementManagerState { display_handle: DisplayHandle, managers: HashMap, outputs: HashMap, + removed_outputs: HashSet, } struct OutputManagerData { @@ -95,7 +102,6 @@ pub trait OutputManagementHandler { #[derive(Debug, Clone)] pub struct OutputData { - // modes: Vec, enabled: bool, current_mode: Option, position: Point, @@ -117,7 +123,13 @@ impl OutputManagementManagerState { } for (manager, manager_data) in self.managers.iter_mut() { - let (head, modes) = advertise_output::(&self.display_handle, manager, output); + let (head, modes) = match advertise_output::(&self.display_handle, manager, output) { + Ok(ret) => ret, + Err(err) => { + error!("Failed to advertise output to output management: {err}"); + continue; + } + }; manager_data.heads.insert(head, modes); } @@ -134,33 +146,26 @@ impl OutputManagementManagerState { } pub fn remove_head(&mut self, output: &Output) { - self.outputs.remove(output); - - for data in self.managers.values_mut() { - let heads = data.heads.keys().cloned().collect::>(); - for head in heads { - if head.data::() == Some(output) { - let modes = data.heads.remove(&head); - if let Some(modes) = modes { - for mode in modes { - mode.finished(); - } - } - head.finished(); - } - } + if self.outputs.remove(output).is_some() { + self.removed_outputs.insert(output.clone()); } } - pub fn set_head_enabled(&mut self, output: &Output, enabled: bool) { + pub fn set_head_enabled(&mut self, output: &Output, enabled: bool) + where + D: Dispatch + + Dispatch + + OutputManagementHandler + + 'static, + { let Some(output_data) = self.outputs.get_mut(output) else { return; }; output_data.enabled = enabled; - for manager_data in self.managers.values() { - for (head, wlr_modes) in manager_data.heads.iter() { + for manager_data in self.managers.values_mut() { + for (head, wlr_modes) in manager_data.heads.iter_mut() { if head.data::() == Some(output) { head.enabled(enabled as i32); @@ -171,141 +176,163 @@ impl OutputManagementManagerState { .find(|wlr_mode| wlr_mode.data::() == Some(¤t_mode)); if let Some(wlr_current_mode) = wlr_current_mode { head.current_mode(wlr_current_mode); + } else { + let new_wlr_mode = create_mode_for_head::( + head, + &self.display_handle, + current_mode, + output.preferred_mode() == Some(current_mode), + ); + + match new_wlr_mode { + Ok(new_wlr_current_mode) => { + head.current_mode(&new_wlr_current_mode); + wlr_modes.push(new_wlr_current_mode); + } + Err(err) => error!("Failed to create wlr mode: {err}"), + } } } - head.position(output.current_location().x, output.current_location().y); - head.transform(output.current_transform().into()); - head.scale(output.current_scale().fractional_scale()); + let new_loc = output.current_location(); + head.position(new_loc.x, new_loc.y); + output_data.position = new_loc; + + let new_transform = output.current_transform(); + head.transform(new_transform.into()); + output_data.transform = new_transform; + + let new_scale = output.current_scale().fractional_scale(); + head.scale(new_scale); + output_data.scale = new_scale; } } } } } - pub fn update_head(&mut self, output: &Output) + pub fn update(&mut self) where D: Dispatch + Dispatch + OutputManagementHandler + 'static, { - let Some(output_data) = self.outputs.get_mut(output) else { - tracing::error!("Called `update_head` without `advertise_output`"); - return; - }; - - for (manager, manager_data) in self.managers.iter_mut() { - for (head, wlr_modes) in manager_data.heads.iter_mut() { - if head.data::() != Some(output) { - continue; - } - - // TODO: modes - let modes = output.with_state(|state| state.modes.clone()); - - wlr_modes.retain(|wlr_mode| { - if !modes.contains(wlr_mode.data::().unwrap()) { - wlr_mode.finished(); - false - } else { - true - } - }); - - for mode in modes { - if !wlr_modes - .iter() - .any(|wlr_mode| wlr_mode.data::().unwrap() == &mode) - { - if let Some(client) = head.client() { - let new_wlr_mode = client - .create_resource::( - &self.display_handle, - head.version(), - mode, - ) - .expect("TODO"); - - new_wlr_mode.size(mode.size.w, mode.size.h); - new_wlr_mode.refresh(mode.refresh); - - if Some(mode) == output.preferred_mode() { - new_wlr_mode.preferred(); - } - - head.mode(&new_wlr_mode); - - wlr_modes.push(new_wlr_mode); - } - } - } - - // enabled handled in `set_head_enabled` - - if output.current_mode() != output_data.current_mode { - if let Some(new_cur_mode) = output.current_mode() { - let new_cur_wlr_mode = wlr_modes - .iter() - .find(|wlr_mode| wlr_mode.data::() == Some(&new_cur_mode)); - - match new_cur_wlr_mode { - Some(new_cur_wlr_mode) => { - head.current_mode(new_cur_wlr_mode); - } - // TODO: don't do this branch - None => { - if let Some(client) = head.client() { - let new_cur_wlr_mode = client - .create_resource::( - &self.display_handle, - head.version(), - new_cur_mode, - ) - .expect("TODO"); - - new_cur_wlr_mode.size(new_cur_mode.size.w, new_cur_mode.size.h); - new_cur_wlr_mode.refresh(new_cur_mode.refresh); - - if Some(new_cur_mode) == output.preferred_mode() { - new_cur_wlr_mode.preferred(); - } - - head.mode(&new_cur_wlr_mode); - head.current_mode(&new_cur_wlr_mode); - wlr_modes.push(new_cur_wlr_mode); - } + for output in self.removed_outputs.drain() { + for data in self.managers.values_mut() { + let heads = data.heads.keys().cloned().collect::>(); + for head in heads { + if head.data::() == Some(&output) { + let modes = data.heads.remove(&head); + if let Some(modes) = modes { + for mode in modes { + mode.finished(); } } - - output_data.current_mode = Some(new_cur_mode); + head.finished(); } } - - if output.current_location() != output_data.position { - let new_loc = output.current_location(); - head.position(new_loc.x, new_loc.y); - output_data.position = new_loc; - } - - if output.current_transform() != output_data.transform { - let new_transform = output.current_transform(); - head.transform(new_transform.into()); - output_data.transform = new_transform; - } - - if output.current_scale().fractional_scale() != output_data.scale { - let new_scale = output.current_scale().fractional_scale(); - head.scale(new_scale); - output_data.scale = new_scale; - } - - // TODO: adaptive sync } + } - let serial = u32::from(SERIAL_COUNTER.next_serial()); + let serial = u32::from(SERIAL_COUNTER.next_serial()); - manager_data.serial = serial; - manager.done(serial); + for (output, output_data) in self.outputs.iter_mut() { + for (manager, manager_data) in self.managers.iter_mut() { + for (head, wlr_modes) in manager_data.heads.iter_mut() { + if head.data::() != Some(output) { + continue; + } + + let modes = output.with_state(|state| state.modes.clone()); + + wlr_modes.retain(|wlr_mode| { + if !modes.contains(wlr_mode.data::().unwrap()) { + wlr_mode.finished(); + false + } else { + true + } + }); + + for mode in modes { + if !wlr_modes + .iter() + .any(|wlr_mode| wlr_mode.data::().unwrap() == &mode) + { + let new_wlr_mode = create_mode_for_head::( + head, + &self.display_handle, + mode, + output.preferred_mode() == Some(mode), + ); + + match new_wlr_mode { + Ok(new_wlr_current_mode) => wlr_modes.push(new_wlr_current_mode), + Err(err) => error!("Failed to create wlr mode: {err}"), + } + } + } + + // enabled handled in `set_head_enabled` + + if output_data.enabled { + if output.current_mode() != output_data.current_mode { + if let Some(new_cur_mode) = output.current_mode() { + let new_cur_wlr_mode = wlr_modes.iter().find(|wlr_mode| { + wlr_mode.data::() == Some(&new_cur_mode) + }); + + match new_cur_wlr_mode { + Some(new_cur_wlr_mode) => { + head.current_mode(new_cur_wlr_mode); + } + None => { + let new_wlr_current_mode = create_mode_for_head::( + head, + &self.display_handle, + new_cur_mode, + output.preferred_mode() == Some(new_cur_mode), + ); + + match new_wlr_current_mode { + Ok(new_wlr_current_mode) => { + head.current_mode(&new_wlr_current_mode); + wlr_modes.push(new_wlr_current_mode); + } + Err(err) => error!("Failed to create wlr mode: {err}"), + } + } + } + + output_data.current_mode = Some(new_cur_mode); + } + } + + if output.current_location() != output_data.position { + let new_loc = output.current_location(); + head.position(new_loc.x, new_loc.y); + output_data.position = new_loc; + } + + if output.current_transform() != output_data.transform { + let new_transform = output.current_transform(); + head.transform(new_transform.into()); + output_data.transform = new_transform; + } + + if output.current_scale().fractional_scale() != output_data.scale { + let new_scale = output.current_scale().fractional_scale(); + head.scale(new_scale); + output_data.scale = new_scale; + } + } + + // TODO: adaptive sync + } + + manager_data.serial = serial; + manager.done(serial); + } } } } @@ -314,18 +341,22 @@ fn advertise_output( display: &DisplayHandle, manager: &ZwlrOutputManagerV1, output: &Output, -) -> (ZwlrOutputHeadV1, Vec) +) -> anyhow::Result<(ZwlrOutputHeadV1, Vec)> where D: Dispatch + Dispatch + OutputManagementHandler + 'static, { - let client = manager.client().expect("TODO"); + let client = manager + .client() + .context("output manager has no owning client")?; - let head = client - .create_resource::(display, manager.version(), output.clone()) - .unwrap(); + let head = client.create_resource::( + display, + manager.version(), + output.clone(), + )?; manager.head(&head); @@ -337,16 +368,13 @@ where let mut wlr_modes = Vec::new(); for mode in output.with_state(|state| state.modes.clone()) { - let wlr_mode = client - .create_resource::(display, manager.version(), mode) - .unwrap(); - head.mode(&wlr_mode); - wlr_mode.size(mode.size.w, mode.size.h); - wlr_mode.refresh(mode.refresh); - if Some(mode) == output.preferred_mode() { - wlr_mode.preferred(); + let wlr_mode = + create_mode_for_head::(&head, display, mode, output.preferred_mode() == Some(mode)); + + match wlr_mode { + Ok(wlr_mode) => wlr_modes.push(wlr_mode), + Err(err) => error!("Failed to create wlr mode: {err}"), } - wlr_modes.push(wlr_mode); } if head.version() >= zwlr_output_head_v1::EVT_MAKE_SINCE { @@ -365,26 +393,64 @@ where // false => AdaptiveSyncState::Disabled, // }); - // TODO: - // head.enabled(data.enabled as i32); head.enabled(true as i32); - if true - /* data.enabled */ - { - if let Some(current_mode) = output.current_mode() { - let wlr_current_mode = wlr_modes - .iter() - .find(|wlr_mode| wlr_mode.data::() == Some(¤t_mode)); - if let Some(wlr_current_mode) = wlr_current_mode { - head.current_mode(wlr_current_mode); + if let Some(current_mode) = output.current_mode() { + let wlr_current_mode = wlr_modes + .iter() + .find(|wlr_mode| wlr_mode.data::() == Some(¤t_mode)); + if let Some(wlr_current_mode) = wlr_current_mode { + head.current_mode(wlr_current_mode); + } else { + let new_wlr_current_mode = create_mode_for_head::( + &head, + display, + current_mode, + output.preferred_mode() == Some(current_mode), + ); + + match new_wlr_current_mode { + Ok(new_wlr_current_mode) => { + head.current_mode(&new_wlr_current_mode); + wlr_modes.push(new_wlr_current_mode); + } + Err(err) => error!("Failed to create wlr mode: {err}"), } } - head.position(output.current_location().x, output.current_location().y); - head.transform(output.current_transform().into()); - head.scale(output.current_scale().fractional_scale()); + } + head.position(output.current_location().x, output.current_location().y); + head.transform(output.current_transform().into()); + head.scale(output.current_scale().fractional_scale()); + + Ok((head, wlr_modes)) +} + +fn create_mode_for_head( + head: &ZwlrOutputHeadV1, + display_handle: &DisplayHandle, + mode: Mode, + is_preferred: bool, +) -> anyhow::Result +where + D: Dispatch + + Dispatch + + OutputManagementHandler + + 'static, +{ + let client = head.client().context("head has no owning client")?; + let wlr_mode = + client.create_resource::(display_handle, head.version(), mode)?; + + // do not reorder or wlr-randr gets 0x0 modes + head.mode(&wlr_mode); + + wlr_mode.size(mode.size.w, mode.size.h); + wlr_mode.refresh(mode.refresh); + + if is_preferred { + wlr_mode.preferred(); } - (head, wlr_modes) + Ok(wlr_mode) } fn manager_for_configuration<'a, D>( @@ -419,6 +485,7 @@ impl OutputManagementManagerState { display_handle: display.clone(), managers: HashMap::new(), outputs: HashMap::new(), + removed_outputs: HashSet::new(), } } } @@ -446,7 +513,7 @@ where .output_management_manager_state() .outputs .keys() - .map(|output| advertise_output::(handle, &manager, output)) + .flat_map(|output| advertise_output::(handle, &manager, output)) .collect(); let serial = u32::from(SERIAL_COUNTER.next_serial()); @@ -523,7 +590,16 @@ where let correct_serial = manager_data.serial == serial; if !correct_serial { + tracing::info!(mgr_serial = manager_data.serial, cfg_serial = serial); + tracing::info!("cancelled, incorrect serial 527"); config.cancelled(); + config + .data::() + .unwrap() + .inner + .lock() + .unwrap() + .cancelled = true; return; } @@ -549,11 +625,14 @@ where } } -impl Dispatch for OutputManagementManagerState { +impl Dispatch for OutputManagementManagerState +where + D: OutputManagementHandler + 'static, +{ fn request( - _state: &mut D, + state: &mut D, _client: &Client, - _resource: &ZwlrOutputHeadV1, + resource: &ZwlrOutputHeadV1, request: ::Request, _data: &Output, _dhandle: &DisplayHandle, @@ -561,18 +640,37 @@ impl Dispatch for OutputManagementManagerState { ) { match request { zwlr_output_head_v1::Request::Release => { - // TODO: + for manager_data in state + .output_management_manager_state() + .managers + .values_mut() + { + manager_data.heads.remove(resource); + } } _ => unreachable!(), } } + + fn destroyed(state: &mut D, _client: ClientId, resource: &ZwlrOutputHeadV1, _data: &Output) { + for manager_data in state + .output_management_manager_state() + .managers + .values_mut() + { + manager_data.heads.remove(resource); + } + } } -impl Dispatch for OutputManagementManagerState { +impl Dispatch for OutputManagementManagerState +where + D: OutputManagementHandler + 'static, +{ fn request( - _state: &mut D, + state: &mut D, _client: &Client, - _resource: &ZwlrOutputModeV1, + resource: &ZwlrOutputModeV1, request: ::Request, _data: &Mode, _dhandle: &DisplayHandle, @@ -580,11 +678,31 @@ impl Dispatch for OutputManagementManagerState { ) { match request { zwlr_output_mode_v1::Request::Release => { - // TODO: + for manager_data in state + .output_management_manager_state() + .managers + .values_mut() + { + for modes in manager_data.heads.values_mut() { + modes.retain(|mode| mode != resource); + } + } } _ => unreachable!(), } } + + fn destroyed(state: &mut D, _client: ClientId, resource: &ZwlrOutputModeV1, _data: &Mode) { + for manager_data in state + .output_management_manager_state() + .managers + .values_mut() + { + for modes in manager_data.heads.values_mut() { + modes.retain(|mode| mode != resource); + } + } + } } impl Dispatch @@ -610,15 +728,17 @@ where let mut data = pending_data.inner.lock().unwrap(); + if data.cancelled { + return; + } + let manager_serial = manager_for_configuration(state, resource).map(|(_, data)| data.serial); if manager_serial != Some(pending_data.serial) { + tracing::info!("cancelled, incorrect serial 661"); resource.cancelled(); data.cancelled = true; - } - - if data.cancelled { return; } @@ -637,15 +757,17 @@ where zwlr_output_configuration_v1::Request::DisableHead { head } => { let mut data = pending_data.inner.lock().unwrap(); + if data.cancelled { + return; + } + let manager_serial = manager_for_configuration(state, resource).map(|(_, data)| data.serial); if manager_serial != Some(pending_data.serial) { + tracing::info!("cancelled, incorrect serial 689"); resource.cancelled(); data.cancelled = true; - } - - if data.cancelled { return; } @@ -665,15 +787,17 @@ where | zwlr_output_configuration_v1::Request::Test) => { let mut data = pending_data.inner.lock().unwrap(); + if data.cancelled { + return; + } + let manager_serial = manager_for_configuration(state, resource).map(|(_, data)| data.serial); if manager_serial != Some(pending_data.serial) { + tracing::info!("cancelled, incorrect serial 718"); resource.cancelled(); data.cancelled = true; - } - - if data.cancelled { return; } From 07917a82ef0bad39cf141e45d4e79cb7348b2e10 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 3 Jun 2024 20:59:23 -0500 Subject: [PATCH 05/15] Impl wlr-output-power-management --- src/api.rs | 1 + src/backend/udev.rs | 43 +---- src/config.rs | 1 + src/handlers.rs | 18 ++- src/output.rs | 10 +- src/protocol.rs | 2 + src/protocol/gamma_control.rs | 1 - src/protocol/output_management.rs | 5 - src/protocol/output_power_management.rs | 206 ++++++++++++++++++++++++ src/state.rs | 6 + 10 files changed, 242 insertions(+), 51 deletions(-) create mode 100644 src/protocol/output_power_management.rs diff --git a/src/api.rs b/src/api.rs index c7d60da..ce71925 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1070,6 +1070,7 @@ impl output_service_server::OutputService for OutputService { let Some(mode) = Some(request).and_then(|request| { Some(smithay::output::Mode { size: (request.pixel_width? as i32, request.pixel_height? as i32).into(), + // FIXME: this is nullable, pick a mode with highest refresh if None refresh: request.refresh_rate_millihz? as i32, }) }) else { diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 9c3c6b0..f304fea 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -11,9 +11,7 @@ use std::{ use anyhow::{anyhow, ensure, Context}; use drm::{set_crtc_active, util::create_drm_mode}; -use pinnacle_api_defs::pinnacle::signal::v0alpha1::{ - OutputConnectResponse, OutputDisconnectResponse, -}; +use pinnacle_api_defs::pinnacle::signal::v0alpha1::OutputConnectResponse; use smithay::{ backend::{ allocator::{ @@ -51,10 +49,7 @@ use smithay::{ vulkan::{self, version::Version, PhysicalDevice}, SwapBuffersError, }, - desktop::{ - layer_map_for_output, - utils::{send_frames_surface_tree, OutputPresentationFeedback}, - }, + desktop::utils::{send_frames_surface_tree, OutputPresentationFeedback}, input::pointer::CursorImageStatus, output::{Output, PhysicalProperties, Subpixel}, reexports::{ @@ -1199,39 +1194,7 @@ impl Udev { .cloned(); if let Some(output) = output { - // Save this output's state. It will be restored if the monitor gets replugged. - pinnacle.config.connector_saved_states.insert( - OutputName(output.name()), - ConnectorSavedState { - loc: output.current_location(), - tags: output.with_state(|state| state.tags.clone()), - scale: Some(output.current_scale()), - }, - ); - - // TODO: extract into a `remove_output` function and unify with dummy backend - for layer in layer_map_for_output(&output).layers() { - layer.layer_surface().send_close(); - } - - pinnacle.space.unmap_output(&output); - pinnacle.gamma_control_manager_state.output_removed(&output); - - pinnacle.signal_state.output_disconnect.signal(|buffer| { - buffer.push_back(OutputDisconnectResponse { - output_name: Some(output.name()), - }) - }); - - pinnacle - .output_management_manager_state - .remove_head(&output); - pinnacle.output_management_manager_state.update::(); - - if let Some(global) = pinnacle.outputs.remove(&output) { - // TODO: disable ahead of time - pinnacle.display_handle.remove_global::(global); - } + pinnacle.remove_output(&output); } } diff --git a/src/config.rs b/src/config.rs index c65c51c..5d1f5a1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -347,6 +347,7 @@ pub struct ConnectorSavedState { pub tags: Vec, /// The output's previous scale pub scale: Option, + // TODO: transform } /// Parse a metaconfig file in `config_dir`, if any. diff --git a/src/handlers.rs b/src/handlers.rs index f29a2ab..8e53920 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -74,7 +74,7 @@ use tracing::{debug, error, trace, warn}; use crate::{ backend::Backend, delegate_foreign_toplevel, delegate_gamma_control, delegate_output_management, - delegate_screencopy, + delegate_output_power_management, delegate_screencopy, focus::{keyboard::KeyboardFocusTarget, pointer::PointerFocusTarget}, handlers::xdg_shell::snapshot_pre_commit_hook, protocol::{ @@ -83,6 +83,7 @@ use crate::{ output_management::{ OutputConfiguration, OutputManagementHandler, OutputManagementManagerState, }, + output_power_management::{OutputPowerManagementHandler, OutputPowerManagementState}, screencopy::{Screencopy, ScreencopyHandler}, }, render::util::snapshot::capture_snapshots_on_output, @@ -1011,6 +1012,21 @@ impl OutputManagementHandler for State { } delegate_output_management!(State); +impl OutputPowerManagementHandler for State { + fn output_power_management_state(&mut self) -> &mut OutputPowerManagementState { + &mut self.pinnacle.output_power_management_state + } + + fn set_mode(&mut self, output: &Output, powered: bool) { + self.backend.set_output_powered(output, powered); + + if powered { + self.schedule_render(output); + } + } +} +delegate_output_power_management!(State); + impl Pinnacle { fn position_popup(&self, popup: &PopupSurface) { trace!("State::position_popup"); diff --git a/src/output.rs b/src/output.rs index d9f7bb1..7c0afe5 100644 --- a/src/output.rs +++ b/src/output.rs @@ -258,10 +258,16 @@ impl Pinnacle { } self.unmapped_outputs.remove(output); + for layer in layer_map_for_output(output).layers() { + layer.layer_surface().send_close(); + } + self.space.unmap_output(output); self.gamma_control_manager_state.output_removed(output); + self.output_power_management_state.output_removed(output); + self.output_management_manager_state.remove_head(output); self.output_management_manager_state.update::(); @@ -279,9 +285,5 @@ impl Pinnacle { scale: Some(output.current_scale()), }, ); - - for layer in layer_map_for_output(output).layers() { - layer.layer_surface().send_close(); - } } } diff --git a/src/protocol.rs b/src/protocol.rs index a58b48b..4309c87 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,4 +1,6 @@ pub mod foreign_toplevel; pub mod gamma_control; pub mod output_management; +pub mod output_power_management; pub mod screencopy; + diff --git a/src/protocol/gamma_control.rs b/src/protocol/gamma_control.rs index 645dd9e..c65e0c6 100644 --- a/src/protocol/gamma_control.rs +++ b/src/protocol/gamma_control.rs @@ -150,7 +150,6 @@ pub trait GammaControlHandler { fn gamma_control_destroyed(&mut self, output: &Output); } -#[allow(missing_docs)] #[macro_export] macro_rules! delegate_gamma_control { ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { diff --git a/src/protocol/output_management.rs b/src/protocol/output_management.rs index c023a9c..e9a98ed 100644 --- a/src/protocol/output_management.rs +++ b/src/protocol/output_management.rs @@ -590,8 +590,6 @@ where let correct_serial = manager_data.serial == serial; if !correct_serial { - tracing::info!(mgr_serial = manager_data.serial, cfg_serial = serial); - tracing::info!("cancelled, incorrect serial 527"); config.cancelled(); config .data::() @@ -736,7 +734,6 @@ where manager_for_configuration(state, resource).map(|(_, data)| data.serial); if manager_serial != Some(pending_data.serial) { - tracing::info!("cancelled, incorrect serial 661"); resource.cancelled(); data.cancelled = true; return; @@ -765,7 +762,6 @@ where manager_for_configuration(state, resource).map(|(_, data)| data.serial); if manager_serial != Some(pending_data.serial) { - tracing::info!("cancelled, incorrect serial 689"); resource.cancelled(); data.cancelled = true; return; @@ -795,7 +791,6 @@ where manager_for_configuration(state, resource).map(|(_, data)| data.serial); if manager_serial != Some(pending_data.serial) { - tracing::info!("cancelled, incorrect serial 718"); resource.cancelled(); data.cancelled = true; return; diff --git a/src/protocol/output_power_management.rs b/src/protocol/output_power_management.rs new file mode 100644 index 0000000..4f2673f --- /dev/null +++ b/src/protocol/output_power_management.rs @@ -0,0 +1,206 @@ +use std::collections::HashMap; + +use smithay::{ + output::Output, + reexports::{ + wayland_protocols_wlr::output_power_management::v1::server::{ + zwlr_output_power_manager_v1::{self, ZwlrOutputPowerManagerV1}, + zwlr_output_power_v1::{self, ZwlrOutputPowerV1}, + }, + wayland_server::{ + self, backend::ClientId, Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, + Resource, WEnum, + }, + }, +}; +use tracing::warn; + +use crate::state::WithState; + +const VERSION: u32 = 1; + +pub struct OutputPowerManagementState { + clients: HashMap, +} + +pub struct OutputPowerManagementGlobalData { + filter: Box bool + Send + Sync + 'static>, +} + +pub trait OutputPowerManagementHandler { + fn output_power_management_state(&mut self) -> &mut OutputPowerManagementState; + fn set_mode(&mut self, output: &Output, powered: bool); +} + +impl OutputPowerManagementState { + pub fn new(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch + 'static, + F: Fn(&Client) -> bool + Send + Sync + 'static, + { + let data = OutputPowerManagementGlobalData { + filter: Box::new(filter), + }; + + display.create_global::(VERSION, data); + + Self { + clients: HashMap::new(), + } + } + + pub fn output_removed(&mut self, output: &Output) { + if let Some(power) = self.clients.remove(output) { + power.failed(); + } + } +} + +impl GlobalDispatch + for OutputPowerManagementState +where + D: Dispatch + OutputPowerManagementHandler, +{ + fn bind( + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: wayland_server::New, + _global_data: &OutputPowerManagementGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, ()); + } + + fn can_view(client: Client, global_data: &OutputPowerManagementGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for OutputPowerManagementState +where + D: Dispatch + OutputPowerManagementHandler, +{ + fn request( + state: &mut D, + _client: &Client, + _resource: &ZwlrOutputPowerManagerV1, + request: ::Request, + _data: &(), + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_power_manager_v1::Request::GetOutputPower { id, output } => { + let Some(output) = Output::from_resource(&output) else { + warn!("wlr-output-power-management: no output for wl_output {output:?}"); + let power = data_init.init(id, ()); + power.failed(); + return; + }; + + if state + .output_power_management_state() + .clients + .contains_key(&output) + { + warn!( + "wlr-output-power-management: {} already has an active power manager", + output.name() + ); + let power = data_init.init(id, ()); + power.failed(); + return; + } + + let power = data_init.init(id, ()); + let is_powered = output.with_state(|state| state.powered); + power.mode(match is_powered { + true => zwlr_output_power_v1::Mode::On, + false => zwlr_output_power_v1::Mode::Off, + }); + + state + .output_power_management_state() + .clients + .insert(output, power); + } + zwlr_output_power_manager_v1::Request::Destroy => (), + _ => unreachable!(), + } + } +} + +impl Dispatch for OutputPowerManagementState +where + D: Dispatch + OutputPowerManagementHandler, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &ZwlrOutputPowerV1, + request: ::Request, + _data: &(), + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_power_v1::Request::SetMode { mode } => { + let Some(output) = state + .output_power_management_state() + .clients + .iter() + .find_map(|(output, power)| (power == resource).then_some(output.clone())) + else { + return; + }; + + state.set_mode( + &output, + match mode { + WEnum::Value(zwlr_output_power_v1::Mode::On) => true, + WEnum::Value(zwlr_output_power_v1::Mode::Off) => false, + mode => { + resource.post_error( + zwlr_output_power_v1::Error::InvalidMode, + format!("invalid mode {mode:?}"), + ); + return; + } + }, + ); + } + zwlr_output_power_v1::Request::Destroy => { + state + .output_power_management_state() + .clients + .retain(|_, power| power == resource); + } + _ => todo!(), + } + } + + fn destroyed(state: &mut D, _client: ClientId, resource: &ZwlrOutputPowerV1, _data: &()) { + state + .output_power_management_state() + .clients + .retain(|_, power| power == resource); + } +} + +#[macro_export] +macro_rules! delegate_output_power_management { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_power_management::v1::server::zwlr_output_power_manager_v1::ZwlrOutputPowerManagerV1: $crate::protocol::output_power_management::OutputPowerManagementGlobalData + ] => $crate::protocol::output_power_management::OutputPowerManagementState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_power_management::v1::server::zwlr_output_power_manager_v1::ZwlrOutputPowerManagerV1: () + ] => $crate::protocol::output_power_management::OutputPowerManagementState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols_wlr::output_power_management::v1::server::zwlr_output_power_v1::ZwlrOutputPowerV1: () + ] => $crate::protocol::output_power_management::OutputPowerManagementState); + }; +} diff --git a/src/state.rs b/src/state.rs index 4c19287..dde8244 100644 --- a/src/state.rs +++ b/src/state.rs @@ -13,6 +13,7 @@ use crate::{ foreign_toplevel::{self, ForeignToplevelManagerState}, gamma_control::GammaControlManagerState, output_management::OutputManagementManagerState, + output_power_management::OutputPowerManagementState, screencopy::ScreencopyManagerState, }, window::WindowElement, @@ -109,6 +110,7 @@ pub struct Pinnacle { pub xwayland_shell_state: XWaylandShellState, pub idle_notifier_state: IdleNotifierState, pub output_management_manager_state: OutputManagementManagerState, + pub output_power_management_state: OutputPowerManagementState, pub lock_state: LockState, @@ -309,6 +311,10 @@ impl Pinnacle { &display_handle, filter_restricted_client, ), + output_power_management_state: OutputPowerManagementState::new::( + &display_handle, + filter_restricted_client, + ), lock_state: LockState::default(), From bc8ec3d5a66f4b17f204fcce4a68431c0aa9c169 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Mon, 3 Jun 2024 22:27:48 -0500 Subject: [PATCH 06/15] 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>, From ad81b553c8542e0cc5dfba166d48c61b9203b558 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 4 Jun 2024 10:48:34 -0500 Subject: [PATCH 07/15] lua-api: Add custom modelines --- Cargo.lock | 1 + Cargo.toml | 6 +-- api/lua/pinnacle/grpc/defs.lua | 21 +++++++++ api/lua/pinnacle/output.lua | 48 ++++++++++++++++++++ api/lua/pinnacle/util.lua | 83 ++++++++++++++++++++++++++++++++++ 5 files changed, 156 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8fb6133..71171e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2388,6 +2388,7 @@ checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" [[package]] name = "smithay" version = "0.3.0" +source = "git+https://github.com/Smithay/smithay?rev=900b938#900b938970081cb525dc94ff083d76aa07c60e54" dependencies = [ "appendlist", "ash", diff --git a/Cargo.toml b/Cargo.toml index 3782a89..b6d71d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,9 +34,9 @@ dircpy = "0.3.16" tempfile = "3.10.1" [workspace.dependencies.smithay] -# git = "https://github.com/Smithay/smithay" -# rev = "900b938" -path = "../../git/smithay" +git = "https://github.com/Smithay/smithay" +rev = "900b938" +# path = "../../git/smithay" default-features = false features = [ "desktop", diff --git a/api/lua/pinnacle/grpc/defs.lua b/api/lua/pinnacle/grpc/defs.lua index 8010c7f..2f07610 100644 --- a/api/lua/pinnacle/grpc/defs.lua +++ b/api/lua/pinnacle/grpc/defs.lua @@ -65,6 +65,20 @@ local pinnacle_output_v0alpha1_Transform = { ---@field pixel_height integer? ---@field refresh_rate_millihz integer? +---@class pinnacle.output.v0alpha1.SetModelineRequest +---@field output_name string? +---@field clock number? +---@field hdisplay integer? +---@field hsync_start integer? +---@field hsync_end integer? +---@field htotal integer? +---@field vdisplay integer? +---@field vsync_start integer? +---@field vsync_end integer? +---@field vtotal integer? +---@field hsync_pos boolean? +---@field vsync_pos boolean? + ---@class pinnacle.output.v0alpha1.SetScaleRequest ---@field output_name string? ---@field absolute number? @@ -476,6 +490,13 @@ defs.pinnacle = { response = "google.protobuf.Empty", }, ---@type GrpcRequestArgs + SetModeline = { + service = "pinnacle.output.v0alpha1.OutputService", + method = "SetModeline", + request = "pinnacle.output.v0alpha1.SetModelineRequest", + response = "google.protobuf.Empty", + }, + ---@type GrpcRequestArgs SetScale = { service = "pinnacle.output.v0alpha1.OutputService", method = "SetScale", diff --git a/api/lua/pinnacle/output.lua b/api/lua/pinnacle/output.lua index 552daa4..2ba57fb 100644 --- a/api/lua/pinnacle/output.lua +++ b/api/lua/pinnacle/output.lua @@ -813,6 +813,54 @@ function OutputHandle:set_mode(pixel_width, pixel_height, refresh_rate_millihz) }) end +---@class Modeline +---@field clock number +---@field hdisplay integer +---@field hsync_start integer +---@field hsync_end integer +---@field htotal integer +---@field vdisplay integer +---@field vsync_start integer +---@field vsync_end integer +---@field vtotal integer +---@field hsync boolean +---@field vsync boolean + +---Set a custom modeline for this output. +--- +---This accepts a `Modeline` table or a string of the modeline. +--- +---@param modeline string|Modeline +function OutputHandle:set_modeline(modeline) + if type(modeline) == "string" then + local ml, err = require("pinnacle.util").output.parse_modeline(modeline) + if ml then + modeline = ml + else + print("invalid modeline: " .. tostring(err)) + return + end + end + + ---@type pinnacle.output.v0alpha1.SetModelineRequest + local request = { + output_name = self.name, + clock = modeline.clock, + 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, + hsync_pos = modeline.hsync, + vsync_pos = modeline.vsync, + } + + client.unary_request(output_service.SetModeline, request) +end + ---Set this output's scaling factor. --- ---@param scale number diff --git a/api/lua/pinnacle/util.lua b/api/lua/pinnacle/util.lua index 07327a2..88fa8c7 100644 --- a/api/lua/pinnacle/util.lua +++ b/api/lua/pinnacle/util.lua @@ -118,10 +118,93 @@ function rectangle.new(x, y, width, height) return self end +---Parse a modeline string. +--- +---@param modeline string +--- +---@return Modeline|nil modeline A modeline if successful +---@return string|nil error An error message if any +local function parse_modeline(modeline) + local args = modeline:gmatch("[^%s]+") + + local targs = {} + + for arg in args do + table.insert(targs, arg) + end + + local clock = tonumber(targs[1]) + local hdisplay = tonumber(targs[2]) + local hsync_start = tonumber(targs[3]) + local hsync_end = tonumber(targs[4]) + local htotal = tonumber(targs[5]) + local vdisplay = tonumber(targs[6]) + local vsync_start = tonumber(targs[7]) + local vsync_end = tonumber(targs[8]) + local vtotal = tonumber(targs[9]) + local hsync = targs[10] + local vsync = targs[11] + + if + not ( + clock + and hdisplay + and hsync_start + and hsync_end + and htotal + and vdisplay + and vsync_start + and vsync_end + and vtotal + and hsync + and vsync + ) + then + return nil, "one or more fields was missing" + end + + local hsync_lower = string.lower(hsync) + local vsync_lower = string.lower(vsync) + + if hsync_lower == "+hsync" then + hsync = true + elseif hsync_lower == "-hsync" then + hsync = false + else + return nil, "invalid hsync: " .. hsync + end + + if vsync_lower == "+vsync" then + vsync = true + elseif vsync_lower == "-vsync" then + vsync = false + else + return nil, "invalid vsync: " .. vsync + end + + ---@type Modeline + return { + clock = clock, + hdisplay = hdisplay, + hsync_start = hsync_start, + hsync_end = hsync_end, + htotal = htotal, + vdisplay = vdisplay, + vsync_start = vsync_start, + vsync_end = vsync_end, + vtotal = vtotal, + hsync = hsync, + vsync = vsync, + } +end + ---Utility functions. ---@class Util local util = { rectangle = rectangle, + output = { + parse_modeline = parse_modeline, + }, } ---Batch a set of requests that will be sent to the compositor all at once. From a9c565d51d4c83197f2d4249192261097b18e913 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 4 Jun 2024 10:55:06 -0500 Subject: [PATCH 08/15] CI: Add libdisplay-info --- .github/workflows/ci.pinnacle.yml | 6 +++--- README.md | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.pinnacle.yml b/.github/workflows/ci.pinnacle.yml index d8ee96a..127cb23 100644 --- a/.github/workflows/ci.pinnacle.yml +++ b/.github/workflows/ci.pinnacle.yml @@ -22,7 +22,7 @@ jobs: - name: Cache stuff uses: Swatinem/rust-cache@v2 - name: Get dependencies - run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev liblua5.4-dev + run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev liblua5.4-dev libdisplay-info-dev - name: Setup Lua uses: leafo/gh-actions-lua@v10 with: @@ -44,7 +44,7 @@ jobs: - name: Cache stuff uses: Swatinem/rust-cache@v2 - name: Get dependencies - run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev foot liblua5.4-dev + run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev foot liblua5.4-dev libdisplay-info-dev - name: Setup Lua uses: leafo/gh-actions-lua@v10 with: @@ -84,7 +84,7 @@ jobs: - name: Cache stuff uses: Swatinem/rust-cache@v2 - name: Get dependencies - run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev liblua5.4-dev + run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev liblua5.4-dev libdisplay-info-dev - name: Setup Lua uses: leafo/gh-actions-lua@v10 with: diff --git a/README.md b/README.md index cb20f3a..7fcf303 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ You will need: `LD_LIBRARY_PATH` so the dynamically loaded libraries are found. > Luarocks currently doesn't install the Lua library and its dependencies due to openssh directory > shenanigans. Fix soon, hopefully. In the meantime you can use the Rust API. +- `libdisplay-info`, for monitor display information - [protoc](https://grpc.io/docs/protoc-installation/), the Protocol Buffer Compiler, for configuration - Arch: ```sh From 5fe5152b766b69ae5b343ae3eef5735a239d2e94 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 4 Jun 2024 10:59:32 -0500 Subject: [PATCH 09/15] CI: Update ubuntu version --- .github/workflows/ci.pinnacle.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.pinnacle.yml b/.github/workflows/ci.pinnacle.yml index 127cb23..c2f02cf 100644 --- a/.github/workflows/ci.pinnacle.yml +++ b/.github/workflows/ci.pinnacle.yml @@ -12,7 +12,7 @@ env: jobs: build: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: Build steps: - name: Checkout @@ -34,7 +34,7 @@ jobs: - name: Celebratory yahoo run: echo yahoo test: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: Run tests steps: - name: Checkout @@ -60,7 +60,7 @@ jobs: if: ${{ runner.debug == '1' }} run: RUST_LOG=debug RUST_BACKTRACE=1 just install test -- --nocapture --test-threads=1 check-format: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: Check formatting steps: - name: Checkout @@ -72,7 +72,7 @@ jobs: - name: Check formatting run: cargo fmt -- --check clippy-check: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 name: Clippy check steps: - name: Checkout From 1c55296d8f13122e28d678e70ae537b98e6ba36d Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 4 Jun 2024 16:52:12 -0500 Subject: [PATCH 10/15] api: Add output powered and enabled props --- api/lua/pinnacle/grpc/defs.lua | 2 + api/lua/pinnacle/output.lua | 50 ++++++++++++- .../pinnacle/output/v0alpha1/output.proto | 2 + api/rust/src/output.rs | 74 ++++++++++++++++++- src/api.rs | 28 +++++-- src/backend/dummy.rs | 8 +- src/backend/udev.rs | 23 +++--- src/backend/winit.rs | 4 +- src/config.rs | 2 +- src/layout.rs | 14 +++- src/output.rs | 53 +++++++++---- src/protocol.rs | 1 - src/state.rs | 4 +- src/tag.rs | 8 +- src/window.rs | 2 +- 15 files changed, 217 insertions(+), 58 deletions(-) diff --git a/api/lua/pinnacle/grpc/defs.lua b/api/lua/pinnacle/grpc/defs.lua index 2f07610..4adb719 100644 --- a/api/lua/pinnacle/grpc/defs.lua +++ b/api/lua/pinnacle/grpc/defs.lua @@ -118,6 +118,8 @@ local pinnacle_output_v0alpha1_Transform = { ---@field transform pinnacle.output.v0alpha1.Transform? ---@field serial integer? ---@field keyboard_focus_stack_window_ids integer[]? +---@field enabled boolean? +---@field powered boolean? -- Window diff --git a/api/lua/pinnacle/output.lua b/api/lua/pinnacle/output.lua index 2ba57fb..6164cb8 100644 --- a/api/lua/pinnacle/output.lua +++ b/api/lua/pinnacle/output.lua @@ -40,8 +40,6 @@ output.handle = output_handle --- ---@return OutputHandle[] function output.get_all() - -- Not going to batch these because I doubt people would have that many monitors - local response = client.unary_request(output_service.Get, {}) ---@type OutputHandle[] @@ -54,6 +52,27 @@ function output.get_all() return handles end +---Get all enabled outputs. +--- +---### Example +---```lua +---local outputs = Output.get_all_enabled() +---``` +--- +---@return OutputHandle[] +function output.get_all_enabled() + local outputs = output.get_all() + + local enabled_handles = {} + for _, handle in ipairs(outputs) do + if handle:enabled() then + table.insert(enabled_handles, handle) + end + end + + return enabled_handles +end + ---Get an output by its name (the connector it's plugged into). --- ---### Example @@ -421,7 +440,7 @@ function output.setup_locs(update_locs_on, locs) end local function layout_outputs() - local outputs = output.get_all() + local outputs = output.get_all_enabled() ---@type OutputHandle[] local placed_outputs = {} @@ -948,6 +967,8 @@ end ---@field transform Transform? ---@field serial integer? ---@field keyboard_focus_stack WindowHandle[] +---@field enabled boolean? +---@field powered boolean? ---Get all properties of this output. --- @@ -1020,6 +1041,8 @@ end ---Get this output's logical width in pixels. --- +---If the output is disabled, this returns nil. +--- ---Shorthand for `handle:props().logical_width`. --- ---@return integer? @@ -1029,6 +1052,8 @@ end ---Get this output's logical height in pixels. --- +---If the output is disabled, this returns nil. +--- ---Shorthand for `handle:props().y`. --- ---@return integer? @@ -1142,6 +1167,25 @@ function OutputHandle:keyboard_focus_stack() return self:props().keyboard_focus_stack end +---Get whether this output is enabled. +--- +---Disabled outputs are not mapped to the global space and cannot be used. +--- +---@return boolean? +function OutputHandle:enabled() + return self:props().enabled +end + +---Get whether this output is powered. +--- +---Unpowered outputs that are enabled will be off, but they will still be +---mapped to the global space, meaning you can still interact with them. +--- +---@return boolean? +function OutputHandle:powered() + return self:props().powered +end + ---Get this output's keyboard focus stack. --- ---This only includes windows on active tags. diff --git a/api/protocol/pinnacle/output/v0alpha1/output.proto b/api/protocol/pinnacle/output/v0alpha1/output.proto index a14a35f..75c5cca 100644 --- a/api/protocol/pinnacle/output/v0alpha1/output.proto +++ b/api/protocol/pinnacle/output/v0alpha1/output.proto @@ -116,6 +116,8 @@ message GetPropertiesResponse { optional uint32 serial = 16; // Window ids of the keyboard focus stack for this output. repeated uint32 keyboard_focus_stack_window_ids = 17; + optional bool enabled = 18; + optional bool powered = 19; } service OutputService { diff --git a/api/rust/src/output.rs b/api/rust/src/output.rs index 9bbfcc1..45c3092 100644 --- a/api/rust/src/output.rs +++ b/api/rust/src/output.rs @@ -60,7 +60,7 @@ impl Output { } } - /// Get a handle to all connected outputs. + /// Get handles to all connected outputs. /// /// # Examples /// @@ -82,10 +82,35 @@ impl Output { .into_inner() .output_names .into_iter() - .map(move |name| self.new_handle(name)) + .map(|name| self.new_handle(name)) .collect() } + /// Get handles to all outputs that are connected and enabled. + /// + /// # Examples + /// + /// ``` + /// let enabled = output.get_all_enabled(); + /// ``` + pub fn get_all_enabled(&self) -> Vec { + block_on_tokio(self.get_all_enabled_async()) + } + + /// The async version of [`Output::get_all_enabled`]. + pub async fn get_all_enabled_async(&self) -> Vec { + let outputs = self.get_all_async().await; + + let mut enabled_outputs = Vec::new(); + for output in outputs { + if output.enabled_async().await.unwrap_or_default() { + enabled_outputs.push(output); + } + } + + enabled_outputs + } + /// Get a handle to the output with the given name. /// /// By "name", we mean the name of the connector the output is connected to. @@ -272,7 +297,7 @@ impl Output { let api = self.api.get().unwrap().clone(); let layout_outputs = move || { - let outputs = api.output.get_all(); + let outputs = api.output.get_all_enabled(); let mut rightmost_output_and_x: Option<(OutputHandle, i32)> = None; @@ -1001,6 +1026,8 @@ impl OutputHandle { .into_iter() .map(|id| self.api.window.new_handle(id)) .collect(), + enabled: response.enabled, + powered: response.powered, } } @@ -1056,6 +1083,8 @@ impl OutputHandle { /// Get this output's logical width in pixels. /// + /// If the output is disabled, this returns None. + /// /// Shorthand for `self.props().logical_width`. pub fn logical_width(&self) -> Option { self.props().logical_width @@ -1068,6 +1097,8 @@ impl OutputHandle { /// Get this output's logical height in pixels. /// + /// If the output is disabled, this returns None. + /// /// Shorthand for `self.props().logical_height`. pub fn logical_height(&self) -> Option { self.props().logical_height @@ -1228,6 +1259,34 @@ impl OutputHandle { .collect() } + /// Get whether this output is enabled. + /// + /// Disabled outputs act as if you unplugged them. + pub fn enabled(&self) -> Option { + self.props().enabled + } + + /// The async version of [`OutputHandle::enabled`]. + pub async fn enabled_async(&self) -> Option { + self.props_async().await.enabled + } + + /// Get whether this output is powered. + /// + /// Unpowered outputs will be turned off but you can still interact with them. + /// + /// Outputs can be disabled but still powered; this just means + /// they will turn on when powered. Disabled and unpowered outputs + /// will not power on when enabled, but will still be interactable. + pub fn powered(&self) -> Option { + self.props().powered + } + + /// The async version of [`OutputHandle::powered`]. + pub async fn powered_async(&self) -> Option { + self.props_async().await.powered + } + /// Get this output's unique name (the name of its connector). pub fn name(&self) -> String { self.name.to_string() @@ -1292,6 +1351,15 @@ pub struct OutputProperties { pub serial: Option, /// This output's window keyboard focus stack. pub keyboard_focus_stack: Vec, + /// Whether this output is enabled. + /// + /// Enabled outputs are mapped in the global space and usable. + /// Disabled outputs function as if you unplugged them. + pub enabled: Option, + /// Whether this output is powered. + /// + /// Unpowered outputs will be off but you can still interact with them. + pub powered: Option, } /// A custom modeline. diff --git a/src/api.rs b/src/api.rs index dc63274..30c117a 100644 --- a/src/api.rs +++ b/src/api.rs @@ -886,7 +886,7 @@ impl tag_service_server::TagService for TagService { .flat_map(|id| id.tag(&state.pinnacle)) .collect::>(); - for output in state.pinnacle.space.outputs().cloned().collect::>() { + for output in state.pinnacle.outputs.keys().cloned().collect::>() { // TODO: seriously, convert state.tags into a hashset output.with_state_mut(|state| { for tag_to_remove in tags_to_remove.iter() { @@ -916,8 +916,8 @@ impl tag_service_server::TagService for TagService { run_unary(&self.sender, move |state| { let tag_ids = state .pinnacle - .space - .outputs() + .outputs + .keys() .flat_map(|op| op.with_state(|state| state.tags.clone())) .map(|tag| tag.id()) .map(|id| id.0) @@ -1272,8 +1272,8 @@ impl output_service_server::OutputService for OutputService { run_unary(&self.sender, move |state| { let output_names = state .pinnacle - .space - .outputs() + .outputs + .keys() .map(|output| output.name()) .collect::>(); @@ -1400,6 +1400,18 @@ impl output_service_server::OutputService for OutputService { }) .unwrap_or_default(); + let enabled = output.as_ref().map(|output| { + state + .pinnacle + .outputs + .get(output) + .is_some_and(|global| global.is_some()) + }); + + let powered = output + .as_ref() + .map(|output| output.with_state(|state| state.powered)); + output::v0alpha1::GetPropertiesResponse { make, model, @@ -1418,6 +1430,8 @@ impl output_service_server::OutputService for OutputService { transform, serial, keyboard_focus_stack_window_ids, + enabled, + powered, } }) .await @@ -1453,7 +1467,7 @@ impl render_service_server::RenderService for RenderService { run_unary_no_response(&self.sender, move |state| { state.backend.set_upscale_filter(filter); - for output in state.pinnacle.space.outputs().cloned().collect::>() { + for output in state.pinnacle.outputs.keys().cloned().collect::>() { state.backend.reset_buffers(&output); state.schedule_render(&output); } @@ -1478,7 +1492,7 @@ impl render_service_server::RenderService for RenderService { run_unary_no_response(&self.sender, move |state| { state.backend.set_downscale_filter(filter); - for output in state.pinnacle.space.outputs().cloned().collect::>() { + for output in state.pinnacle.outputs.keys().cloned().collect::>() { state.backend.reset_buffers(&output); state.schedule_render(&output); } diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs index 2dbcf4f..4136bb3 100644 --- a/src/backend/dummy.rs +++ b/src/backend/dummy.rs @@ -95,10 +95,12 @@ impl Dummy { UninitBackend { seat_name: dummy.seat_name(), init: Box::new(move |pinnacle| { - output.create_global::(&display_handle); + let global = output.create_global::(&display_handle); pinnacle.output_focus_stack.set_focus(output.clone()); + pinnacle.outputs.insert(output.clone(), Some(global)); + pinnacle .shm_state .update_formats(dummy.renderer.shm_formats()); @@ -136,7 +138,9 @@ impl Pinnacle { output.set_preferred(mode); output.with_state_mut(|state| state.modes = vec![mode]); - output.create_global::(&self.display_handle); + let global = output.create_global::(&self.display_handle); + + self.outputs.insert(output.clone(), Some(global)); self.space.map_output(&output, (0, 0)); diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 7089934..4e2969f 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -1037,7 +1037,7 @@ impl Udev { let (phys_w, phys_h) = connector.size().unwrap_or((0, 0)); - if pinnacle.space.outputs().any(|op| { + if pinnacle.outputs.keys().any(|op| { op.user_data() .get::() .is_some_and(|op_id| op_id.crtc == crtc) @@ -1056,11 +1056,10 @@ impl Udev { ); let global = output.create_global::(&self.display_handle); - pinnacle.outputs.insert(output.clone(), global); + pinnacle.outputs.insert(output.clone(), Some(global)); output.with_state_mut(|state| { state.serial = serial; - state.powered = true; }); output.set_preferred(wl_mode); @@ -1198,8 +1197,8 @@ impl Udev { device.surfaces.remove(&crtc); let output = pinnacle - .space - .outputs() + .outputs + .keys() .find(|o| { o.user_data() .get::() @@ -1285,15 +1284,11 @@ impl Udev { return; }; - let output = if let Some(output) = pinnacle - .outputs - .keys() - .chain(pinnacle.unmapped_outputs.iter()) - .find(|o| { - let udev_op_data = o.user_data().get::(); - udev_op_data - .is_some_and(|data| data.device_id == surface.device_id && data.crtc == crtc) - }) { + let output = if let Some(output) = pinnacle.outputs.keys().find(|o| { + let udev_op_data = o.user_data().get::(); + udev_op_data + .is_some_and(|data| data.device_id == surface.device_id && data.crtc == crtc) + }) { output.clone() } else { // somehow we got called with an invalid output diff --git a/src/backend/winit.rs b/src/backend/winit.rs index fd4ee85..a202130 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -185,10 +185,12 @@ impl Winit { let init = Box::new(move |pinnacle: &mut Pinnacle| { let output = winit.output.clone(); - output.create_global::(&display_handle); + let global = output.create_global::(&display_handle); pinnacle.output_focus_stack.set_focus(output.clone()); + pinnacle.outputs.insert(output.clone(), Some(global)); + pinnacle .shm_state .update_formats(winit.backend.renderer().shm_formats()); diff --git a/src/config.rs b/src/config.rs index 5d1f5a1..37b3412 100644 --- a/src/config.rs +++ b/src/config.rs @@ -381,7 +381,7 @@ impl Pinnacle { // Clear state debug!("Clearing tags"); - for output in self.space.outputs() { + for output in self.outputs.keys() { output.with_state_mut(|state| state.tags.clear()); } diff --git a/src/layout.rs b/src/layout.rs index 3dbb0a5..1988a1f 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -158,11 +158,19 @@ impl LayoutState { } impl Pinnacle { - pub fn request_layout(&mut self, output: &Output) -> Option { + pub fn request_layout(&mut self, output: &Output) { + if self + .outputs + .get(output) + .is_some_and(|global| global.is_none()) + { + return; + } + let id = self.layout_state.next_id(); let Some(sender) = self.layout_state.layout_request_sender.as_ref() else { warn!("Layout requested but no client has connected to the layout service"); - return None; + return; }; let windows_on_foc_tags = output.with_state(|state| { @@ -213,8 +221,6 @@ impl Pinnacle { output_width: Some(output_width as u32), output_height: Some(output_height as u32), })); - - Some(id) } } diff --git a/src/output.rs b/src/output.rs index f648b6d..ab54334 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -use std::{cell::RefCell, num::NonZeroU32}; +use std::{cell::RefCell, collections::hash_map::Entry, num::NonZeroU32}; use pinnacle_api_defs::pinnacle::signal::v0alpha1::{ OutputDisconnectResponse, OutputMoveResponse, OutputResizeResponse, @@ -35,8 +35,8 @@ impl OutputName { /// Get the output with this name. pub fn output(&self, pinnacle: &Pinnacle) -> Option { pinnacle - .space - .outputs() + .outputs + .keys() .find(|output| output.name() == self.0) .cloned() } @@ -55,7 +55,7 @@ pub enum BlankingState { } /// The state of an output -#[derive(Debug, Default)] +#[derive(Debug)] pub struct OutputState { pub tags: Vec, pub focus_stack: WindowKeyboardFocusStack, @@ -73,6 +73,22 @@ pub struct OutputState { pub powered: bool, } +impl Default for OutputState { + fn default() -> Self { + Self { + tags: Default::default(), + focus_stack: Default::default(), + screencopy: Default::default(), + serial: Default::default(), + modes: Default::default(), + lock_surface: Default::default(), + blanking_state: Default::default(), + layout_transaction: Default::default(), + powered: true, + } + } +} + impl WithState for Output { type State = OutputState; @@ -226,21 +242,29 @@ impl Pinnacle { pub fn set_output_enabled(&mut self, output: &Output, enabled: bool) { if enabled { - self.unmapped_outputs.remove(output); - if !self.outputs.contains_key(output) { - let global = output.create_global::(&self.display_handle); - self.outputs.insert(output.clone(), global); + match self.outputs.entry(output.clone()) { + Entry::Occupied(entry) => { + let global = entry.into_mut(); + if global.is_none() { + *global = Some(output.create_global::(&self.display_handle)); + } + } + Entry::Vacant(entry) => { + let global = output.create_global::(&self.display_handle); + entry.insert(Some(global)); + } } self.space.map_output(output, output.current_location()); // TODO: output connect? } else { - let global = self.outputs.remove(output); + let global = self.outputs.get_mut(output); if let Some(global) = global { - self.display_handle.remove_global::(global); + if let Some(global) = global.take() { + self.display_handle.remove_global::(global); + } } self.space.unmap_output(output); - self.unmapped_outputs.insert(output.clone()); // TODO: should this trigger the signal? self.signal_state.output_disconnect.signal(|buffer| { @@ -267,11 +291,12 @@ impl Pinnacle { } pub fn remove_output(&mut self, output: &Output) { - let global = self.outputs.remove(output); + let global = self.outputs.get_mut(output); if let Some(global) = global { - self.display_handle.remove_global::(global); + if let Some(global) = global.take() { + self.display_handle.remove_global::(global); + } } - self.unmapped_outputs.remove(output); for layer in layer_map_for_output(output).layers() { layer.layer_surface().send_close(); diff --git a/src/protocol.rs b/src/protocol.rs index 4309c87..8ece99e 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -3,4 +3,3 @@ pub mod gamma_control; pub mod output_management; pub mod output_power_management; pub mod screencopy; - diff --git a/src/state.rs b/src/state.rs index dde8244..20dcc71 100644 --- a/src/state.rs +++ b/src/state.rs @@ -153,8 +153,7 @@ pub struct Pinnacle { /// WlSurfaces with an attached idle inhibitor. pub idle_inhibiting_surfaces: HashSet, - pub outputs: HashMap, - pub unmapped_outputs: HashSet, + pub outputs: HashMap>, } impl State { @@ -355,7 +354,6 @@ impl Pinnacle { idle_inhibiting_surfaces: HashSet::new(), outputs: HashMap::new(), - unmapped_outputs: HashSet::new(), }; Ok(pinnacle) diff --git a/src/tag.rs b/src/tag.rs index 5ae2347..4398282 100644 --- a/src/tag.rs +++ b/src/tag.rs @@ -26,8 +26,8 @@ impl TagId { /// Get the tag associated with this id. pub fn tag(&self, pinnacle: &Pinnacle) -> Option { pinnacle - .space - .outputs() + .outputs + .keys() .flat_map(|op| op.with_state(|state| state.tags.clone())) .find(|tag| &tag.id() == self) } @@ -118,8 +118,8 @@ impl Tag { /// RefCell Safety: This uses RefCells on every mapped output. pub fn output(&self, pinnacle: &Pinnacle) -> Option { pinnacle - .space - .outputs() + .outputs + .keys() .find(|output| output.with_state(|state| state.tags.iter().any(|tg| tg == self))) .cloned() } diff --git a/src/window.rs b/src/window.rs index 6e5ef31..e2b3ca0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -308,7 +308,7 @@ impl Pinnacle { self.z_index_stack.retain(|win| win != window); - for output in self.space.outputs() { + for output in self.outputs.keys() { output.with_state_mut(|state| state.focus_stack.stack.retain(|win| win != window)); } } From a8562277c1f351d8ce269fea3000af39a5ebb123 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 4 Jun 2024 17:40:06 -0500 Subject: [PATCH 11/15] output-mgmt: Post error on config reuse --- src/protocol/output_management.rs | 55 +++++++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/src/protocol/output_management.rs b/src/protocol/output_management.rs index e9a98ed..fcfa018 100644 --- a/src/protocol/output_management.rs +++ b/src/protocol/output_management.rs @@ -70,6 +70,7 @@ pub struct PendingOutputConfiguration { #[derive(Default, Debug)] struct PendingOutputConfigurationInner { cancelled: bool, + used: bool, pending_heads: HashMap, } @@ -111,6 +112,9 @@ pub struct OutputData { } impl OutputManagementManagerState { + /// Add this head. + /// + /// [`OutputManagementManagerState::update`] needs to be called afterwards to apply the new state. pub fn add_head(&mut self, output: &Output) where D: Dispatch @@ -145,12 +149,18 @@ impl OutputManagementManagerState { self.outputs.insert(output.clone(), output_data); } + /// Mark this head as removed. + /// + /// [`OutputManagementManagerState::update`] needs to be called afterwards to apply the new state. pub fn remove_head(&mut self, output: &Output) { if self.outputs.remove(output).is_some() { self.removed_outputs.insert(output.clone()); } } + /// Mark this head as enabled or not. + /// + /// [`OutputManagementManagerState::update`] needs to be called afterwards to apply the new state. pub fn set_head_enabled(&mut self, output: &Output, enabled: bool) where D: Dispatch @@ -210,6 +220,9 @@ impl OutputManagementManagerState { } } + /// Update output management state. + /// + /// This needs to be called whenever output state changes to notify clients of the new state. pub fn update(&mut self) where D: Dispatch @@ -386,12 +399,14 @@ where } } - // TODO: - // SINCE FOUR - // head.adaptive_sync(match data.adaptive_sync { - // true => AdaptiveSyncState::Enabled, - // false => AdaptiveSyncState::Disabled, - // }); + if head.version() >= zwlr_output_head_v1::EVT_ADAPTIVE_SYNC_SINCE { + // TODO: + // head.adaptive_sync(match data.adaptive_sync { + // true => AdaptiveSyncState::Enabled, + // false => AdaptiveSyncState::Disabled, + // }); + head.adaptive_sync(AdaptiveSyncState::Disabled); + } head.enabled(true as i32); if let Some(current_mode) = output.current_mode() { @@ -561,6 +576,7 @@ where serial, inner: Mutex::new(PendingOutputConfigurationInner { cancelled: false, + used: false, pending_heads: HashMap::new(), }), }; @@ -581,6 +597,7 @@ where serial, inner: Mutex::new(PendingOutputConfigurationInner { cancelled: false, + used: false, pending_heads, }), }; @@ -730,6 +747,14 @@ where return; } + if data.used { + resource.post_error( + zwlr_output_configuration_v1::Error::AlreadyUsed, + "configuration has already been applied or tested", + ); + return; + } + let manager_serial = manager_for_configuration(state, resource).map(|(_, data)| data.serial); @@ -758,6 +783,14 @@ where return; } + if data.used { + resource.post_error( + zwlr_output_configuration_v1::Error::AlreadyUsed, + "configuration has already been applied or tested", + ); + return; + } + let manager_serial = manager_for_configuration(state, resource).map(|(_, data)| data.serial); @@ -787,6 +820,14 @@ where return; } + if data.used { + resource.post_error( + zwlr_output_configuration_v1::Error::AlreadyUsed, + "configuration has already been applied or tested", + ); + return; + } + let manager_serial = manager_for_configuration(state, resource).map(|(_, data)| data.serial); @@ -849,6 +890,8 @@ where } else { resource.failed(); } + + data.used = true; } zwlr_output_configuration_v1::Request::Destroy => (), _ => unreachable!(), From 110dbca675572f36e86dd9ef0d71d384d58111bf Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 4 Jun 2024 17:47:59 -0500 Subject: [PATCH 12/15] CI: Remove `needrestart` Gets 24.04 to work --- .github/workflows/ci.pinnacle.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.pinnacle.yml b/.github/workflows/ci.pinnacle.yml index c2f02cf..cab4467 100644 --- a/.github/workflows/ci.pinnacle.yml +++ b/.github/workflows/ci.pinnacle.yml @@ -22,7 +22,7 @@ jobs: - name: Cache stuff uses: Swatinem/rust-cache@v2 - name: Get dependencies - run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev liblua5.4-dev libdisplay-info-dev + run: sudo apt remove needrestart && sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev liblua5.4-dev libdisplay-info-dev - name: Setup Lua uses: leafo/gh-actions-lua@v10 with: @@ -44,7 +44,7 @@ jobs: - name: Cache stuff uses: Swatinem/rust-cache@v2 - name: Get dependencies - run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev foot liblua5.4-dev libdisplay-info-dev + run: sudo apt remove needrestart && sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev foot liblua5.4-dev libdisplay-info-dev - name: Setup Lua uses: leafo/gh-actions-lua@v10 with: @@ -84,7 +84,7 @@ jobs: - name: Cache stuff uses: Swatinem/rust-cache@v2 - name: Get dependencies - run: sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev liblua5.4-dev libdisplay-info-dev + run: sudo apt remove needrestart && sudo apt update && sudo apt install libwayland-dev libxkbcommon-dev libudev-dev libinput-dev libgbm-dev libseat-dev libsystemd-dev protobuf-compiler xwayland libegl-dev liblua5.4-dev libdisplay-info-dev - name: Setup Lua uses: leafo/gh-actions-lua@v10 with: From f16767240e49aea2e35988ede612f269e21985f4 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 4 Jun 2024 18:11:40 -0500 Subject: [PATCH 13/15] Use indexmap for output storage I need me some determinism for testing --- Cargo.lock | 1 + Cargo.toml | 1 + src/output.rs | 20 ++++++-------------- src/state.rs | 5 +++-- 4 files changed, 11 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 71171e5..ed971a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1859,6 +1859,7 @@ dependencies = [ "drm-sys", "gag", "image", + "indexmap 2.2.6", "libdisplay-info-sys", "pinnacle", "pinnacle-api", diff --git a/Cargo.toml b/Cargo.toml index b6d71d0..cade065 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,6 +117,7 @@ pinnacle-api = { path = "./api/rust" } gag = "1.0.0" drm-sys = "0.7.0" libdisplay-info-sys = { git = "https://github.com/Smithay/libdisplay-info-rs", rev = "a482d0d" } +indexmap = "2.2.6" [build-dependencies] vergen = { version = "8.3.1", features = ["git", "gitcl", "rustc", "cargo", "si"] } diff --git a/src/output.rs b/src/output.rs index ab54334..016d8a6 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -use std::{cell::RefCell, collections::hash_map::Entry, num::NonZeroU32}; +use std::{cell::RefCell, num::NonZeroU32}; use pinnacle_api_defs::pinnacle::signal::v0alpha1::{ OutputDisconnectResponse, OutputMoveResponse, OutputResizeResponse, @@ -243,20 +243,18 @@ impl Pinnacle { pub fn set_output_enabled(&mut self, output: &Output, enabled: bool) { if enabled { match self.outputs.entry(output.clone()) { - Entry::Occupied(entry) => { + indexmap::map::Entry::Occupied(entry) => { let global = entry.into_mut(); if global.is_none() { *global = Some(output.create_global::(&self.display_handle)); } } - Entry::Vacant(entry) => { + indexmap::map::Entry::Vacant(entry) => { let global = output.create_global::(&self.display_handle); entry.insert(Some(global)); } } self.space.map_output(output, output.current_location()); - - // TODO: output connect? } else { let global = self.outputs.get_mut(output); if let Some(global) = global { @@ -266,13 +264,6 @@ impl Pinnacle { } self.space.unmap_output(output); - // TODO: should this trigger the signal? - self.signal_state.output_disconnect.signal(|buffer| { - buffer.push_back(OutputDisconnectResponse { - output_name: Some(output.name()), - }) - }); - self.gamma_control_manager_state.output_removed(output); self.config.connector_saved_states.insert( @@ -290,9 +281,10 @@ impl Pinnacle { } } + /// Completely remove an output, for example when a monitor is unplugged pub fn remove_output(&mut self, output: &Output) { - let global = self.outputs.get_mut(output); - if let Some(global) = global { + let global = self.outputs.shift_remove(output); + if let Some(mut global) = global { if let Some(global) = global.take() { self.display_handle.remove_global::(global); } diff --git a/src/state.rs b/src/state.rs index 20dcc71..d3cc2f7 100644 --- a/src/state.rs +++ b/src/state.rs @@ -19,6 +19,7 @@ use crate::{ window::WindowElement, }; use anyhow::Context; +use indexmap::IndexMap; use pinnacle_api_defs::pinnacle::v0alpha1::ShutdownWatchResponse; use smithay::{ desktop::{PopupManager, Space}, @@ -153,7 +154,7 @@ pub struct Pinnacle { /// WlSurfaces with an attached idle inhibitor. pub idle_inhibiting_surfaces: HashSet, - pub outputs: HashMap>, + pub outputs: IndexMap>, } impl State { @@ -353,7 +354,7 @@ impl Pinnacle { idle_inhibiting_surfaces: HashSet::new(), - outputs: HashMap::new(), + outputs: IndexMap::new(), }; Ok(pinnacle) From 6c21f4917747b48f27055ded7c30bbf3e7e6dacd Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 4 Jun 2024 18:37:45 -0500 Subject: [PATCH 14/15] api: Add modeline to `Output::setup` --- api/lua/pinnacle/output.lua | 5 ++++- api/rust/src/output.rs | 45 +++++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/api/lua/pinnacle/output.lua b/api/lua/pinnacle/output.lua index 6164cb8..5e0d4d5 100644 --- a/api/lua/pinnacle/output.lua +++ b/api/lua/pinnacle/output.lua @@ -163,6 +163,7 @@ end ---@class OutputSetup ---@field filter (fun(output: OutputHandle): boolean)? -- A filter for wildcard matches that should return true if this setup should apply to the passed in output. ---@field mode Mode? -- Makes this setup apply the given mode to outputs. +---@field modeline (string|Modeline)? -- Makes this setup apply the given modeline to outputs. This takes precedence over `mode`. ---@field scale number? -- Makes this setup apply the given scale to outputs. ---@field tags string[]? -- Makes this setup add tags with the given name to outputs. ---@field transform Transform? -- Makes this setup applt the given transform to outputs. @@ -289,7 +290,9 @@ function output.setup(setups) goto continue end - if setup.mode then + if setup.modeline then + op:set_modeline(setup.modeline) + elseif setup.mode then op:set_mode( setup.mode.pixel_width, setup.mode.pixel_height, diff --git a/api/rust/src/output.rs b/api/rust/src/output.rs index 45c3092..1666b6a 100644 --- a/api/rust/src/output.rs +++ b/api/rust/src/output.rs @@ -443,10 +443,15 @@ impl std::fmt::Debug for OutputMatcher { } } +enum OutputMode { + Mode(Mode), + Modeline(Modeline), +} + /// An output setup for use in [`Output::setup`]. pub struct OutputSetup { output: OutputMatcher, - mode: Option, + mode: Option, scale: Option, tag_names: Option>, transform: Option, @@ -478,9 +483,24 @@ impl OutputSetup { } /// Makes this setup apply the given [`Mode`] to its outputs. + /// + /// This will overwrite [`OutputSetup::with_modeline`] if called after it. pub fn with_mode(self, mode: Mode) -> Self { Self { - mode: Some(mode), + mode: Some(OutputMode::Mode(mode)), + ..self + } + } + + /// Makes this setup apply the given [`Modeline`] to its outputs. + /// + /// You can parse a modeline string into a modeline. See [`OutputHandle::set_modeline`] for + /// specifics. + /// + /// This will overwrite [`OutputSetup::with_mode`] if called after it. + pub fn with_modeline(self, modeline: Modeline) -> Self { + Self { + mode: Some(OutputMode::Modeline(modeline)), ..self } } @@ -511,11 +531,18 @@ impl OutputSetup { fn apply(&self, output: &OutputHandle, tag: &Tag) { if let Some(mode) = &self.mode { - output.set_mode( - mode.pixel_width, - mode.pixel_height, - Some(mode.refresh_rate_millihertz), - ); + match mode { + OutputMode::Mode(mode) => { + output.set_mode( + mode.pixel_width, + mode.pixel_height, + Some(mode.refresh_rate_millihertz), + ); + } + OutputMode::Modeline(modeline) => { + output.set_modeline(*modeline); + } + } } if let Some(scale) = self.scale { output.set_scale(scale); @@ -530,7 +557,7 @@ impl OutputSetup { } /// A location for an output. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum OutputLoc { /// A specific point in the global space of the form (x, y). Point(i32, i32), @@ -1294,7 +1321,7 @@ impl OutputHandle { } /// A possible output pixel dimension and refresh rate configuration. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Default)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] pub struct Mode { /// The width of the output, in pixels. pub pixel_width: u32, From d037fa25b2208d5e2d79c926571268e773ff4048 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 4 Jun 2024 18:53:10 -0500 Subject: [PATCH 15/15] Send output (dis)connect signal on enable/disable Temporary until I add output enable/disable signals --- src/output.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/output.rs b/src/output.rs index 016d8a6..9e8fa4e 100644 --- a/src/output.rs +++ b/src/output.rs @@ -3,7 +3,7 @@ use std::{cell::RefCell, num::NonZeroU32}; use pinnacle_api_defs::pinnacle::signal::v0alpha1::{ - OutputDisconnectResponse, OutputMoveResponse, OutputResizeResponse, + OutputConnectResponse, OutputDisconnectResponse, OutputMoveResponse, OutputResizeResponse, }; use smithay::{ desktop::layer_map_for_output, @@ -255,6 +255,15 @@ impl Pinnacle { } } self.space.map_output(output, output.current_location()); + + // Trigger the connect signal here for configs to reposition outputs + // + // TODO: Create a new output_disable/enable signal and trigger it here + self.signal_state.output_connect.signal(|buffer| { + buffer.push_back(OutputConnectResponse { + output_name: Some(output.name()), + }) + }); } else { let global = self.outputs.get_mut(output); if let Some(global) = global { @@ -264,6 +273,15 @@ impl Pinnacle { } self.space.unmap_output(output); + // Trigger the disconnect signal here for configs to reposition outputs + // + // TODO: Create a new output_disable/enable signal and trigger it here + self.signal_state.output_disconnect.signal(|buffer| { + buffer.push_back(OutputDisconnectResponse { + output_name: Some(output.name()), + }) + }); + self.gamma_control_manager_state.output_removed(output); self.config.connector_saved_states.insert(