From ea27ec5e2823216e8f60f146ea3dd36a6e7baa22 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 16 Aug 2024 20:49:59 -0700 Subject: [PATCH] DPMS with `wlr-output-power-management-unstable-v1` protocol --- Cargo.lock | 23 ++- Cargo.toml | 3 +- src/backend/kms/mod.rs | 1 + src/backend/kms/surface/mod.rs | 51 +++++- src/input/mod.rs | 2 + src/state.rs | 4 + src/wayland/handlers/mod.rs | 1 + src/wayland/handlers/output_power.rs | 78 +++++++++ src/wayland/protocols/mod.rs | 1 + src/wayland/protocols/output_power.rs | 231 ++++++++++++++++++++++++++ 10 files changed, 390 insertions(+), 5 deletions(-) create mode 100644 src/wayland/handlers/output_power.rs create mode 100644 src/wayland/protocols/output_power.rs diff --git a/Cargo.lock b/Cargo.lock index 9cd1c16c..c55aeee8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -811,6 +811,7 @@ dependencies = [ "cosmic-config", "cosmic-protocols", "cosmic-settings-config", + "drm-ffi 0.8.0", "edid-rs", "egui", "egui_plot", @@ -1269,6 +1270,16 @@ dependencies = [ "rustix", ] +[[package]] +name = "drm-ffi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" +dependencies = [ + "drm-sys 0.7.0", + "rustix", +] + [[package]] name = "drm-ffi" version = "0.9.0" @@ -1295,6 +1306,16 @@ dependencies = [ "linux-raw-sys 0.6.4", ] +[[package]] +name = "drm-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" +dependencies = [ + "libc", + "linux-raw-sys 0.6.4", +] + [[package]] name = "drm-sys" version = "0.8.0" @@ -4635,7 +4656,7 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smithay" version = "0.3.0" -source = "git+https://github.com/smithay//smithay?rev=df79eeb#df79eeba63a8e9c2d33b9be2418aee6a940135e7" +source = "git+https://github.com/smithay//smithay?rev=05c49f7#05c49f7a193bc89fba12a6484dbac895d5c9f853" dependencies = [ "appendlist", "ash 0.38.0+1.3.281", diff --git a/Cargo.toml b/Cargo.toml index 57adee5b..7f69bca9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ profiling = { version = "1.0" } rustix = { version = "0.38.32", features = ["process"] } smallvec = "1.13.2" rand = "0.8.5" +drm-ffi = "0.8.0" [dependencies.id_tree] branch = "feature/copy_clone" @@ -117,4 +118,4 @@ inherits = "release" lto = "fat" [patch."https://github.com/Smithay/smithay.git"] -smithay = { git = "https://github.com/smithay//smithay", rev = "df79eeb" } +smithay = { git = "https://github.com/smithay//smithay", rev = "05c49f7" } diff --git a/src/backend/kms/mod.rs b/src/backend/kms/mod.rs index f1e19c28..24327509 100644 --- a/src/backend/kms/mod.rs +++ b/src/backend/kms/mod.rs @@ -43,6 +43,7 @@ mod drm_helpers; pub mod render; mod socket; mod surface; +pub(crate) use surface::Surface; use device::*; pub use surface::Timings; diff --git a/src/backend/kms/surface/mod.rs b/src/backend/kms/surface/mod.rs index f03b5c89..c09856c1 100644 --- a/src/backend/kms/surface/mod.rs +++ b/src/backend/kms/surface/mod.rs @@ -102,9 +102,9 @@ static NVIDIA_LOGO: &'static [u8] = include_bytes!("../../../../resources/icons/ #[derive(Debug)] pub struct Surface { - pub(super) connector: connector::Handle, + pub(crate) connector: connector::Handle, pub(super) crtc: crtc::Handle, - pub(super) output: Output, + pub(crate) output: Output, known_nodes: HashSet, active: Arc, @@ -114,6 +114,8 @@ pub struct Surface { loop_handle: LoopHandle<'static, State>, thread_command: Sender, thread_token: RegistrationToken, + + dpms: bool, } pub struct SurfaceThreadState { @@ -238,6 +240,7 @@ pub enum ThreadCommand { ScheduleRender, SetMode(Mode, SyncSender>), End, + DpmsOff, } #[derive(Debug)] @@ -350,6 +353,7 @@ impl Surface { loop_handle: evlh.clone(), thread_command: tx, thread_token, + dpms: true, }) } @@ -380,7 +384,9 @@ impl Surface { } pub fn schedule_render(&self) { - let _ = self.thread_command.send(ThreadCommand::ScheduleRender); + if self.dpms { + let _ = self.thread_command.send(ThreadCommand::ScheduleRender); + } } pub fn set_mirroring(&mut self, output: Option) { @@ -431,6 +437,21 @@ impl Surface { rx.recv().context("Surface thread died")? } + + pub fn get_dpms(&mut self) -> bool { + self.dpms + } + + pub fn set_dpms(&mut self, on: bool) { + if self.dpms != on { + self.dpms = on; + if on { + self.schedule_render(); + } else { + let _ = self.thread_command.send(ThreadCommand::DpmsOff); + } + } + } } impl Drop for Surface { @@ -540,6 +561,30 @@ fn surface_thread( let _ = result.send(Err(anyhow::anyhow!("Set mode with inactive surface"))); } } + Event::Msg(ThreadCommand::DpmsOff) => { + if let Some(compositor) = state.compositor.as_mut() { + if let Err(err) = compositor.clear() { + error!("Failed to set DPMS off: {:?}", err); + } + match std::mem::replace(&mut state.state, QueueState::Idle) { + QueueState::Idle => {} + QueueState::Queued(token) + | QueueState::WaitingForEstimatedVBlank(token) => { + state.loop_handle.remove(token); + } + QueueState::WaitingForVBlank { .. } => { + state.timings.discard_current_frame() + } + QueueState::WaitingForEstimatedVBlankAndQueued { + estimated_vblank, + queued_render, + } => { + state.loop_handle.remove(estimated_vblank); + state.loop_handle.remove(queued_render); + } + }; + } + } Event::Closed | Event::Msg(ThreadCommand::End) => { signal.stop(); signal.wakeup(); diff --git a/src/input/mod.rs b/src/input/mod.rs index 44d7862f..02f97e3b 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -164,6 +164,8 @@ impl State { where ::Device: 'static, { + crate::wayland::handlers::output_power::set_all_surfaces_dpms_on(self); + use smithay::backend::input::Event; match event { InputEvent::DeviceAdded { device } => { diff --git a/src/state.rs b/src/state.rs index 682b0bfd..b3e641ef 100644 --- a/src/state.rs +++ b/src/state.rs @@ -15,6 +15,7 @@ use crate::{ drm::WlDrmState, image_source::ImageSourceState, output_configuration::OutputConfigurationState, + output_power::OutputPowerState, screencopy::ScreencopyState, toplevel_info::ToplevelInfoState, toplevel_management::{ManagementCapabilities, ToplevelManagementState}, @@ -198,6 +199,7 @@ pub struct Common { pub keyboard_shortcuts_inhibit_state: KeyboardShortcutsInhibitState, pub output_state: OutputManagerState, pub output_configuration_state: OutputConfigurationState, + pub output_power_state: OutputPowerState, pub presentation_state: PresentationState, pub primary_selection_state: PrimarySelectionState, pub data_control_state: Option, @@ -487,6 +489,7 @@ impl State { let keyboard_shortcuts_inhibit_state = KeyboardShortcutsInhibitState::new::(dh); let output_state = OutputManagerState::new_with_xdg_output::(dh); let output_configuration_state = OutputConfigurationState::new(dh, client_is_privileged); + let output_power_state = OutputPowerState::new::(dh, client_is_privileged); let presentation_state = PresentationState::new::(dh, clock.id() as u32); let primary_selection_state = PrimarySelectionState::new::(dh); let image_source_state = ImageSourceState::new::(dh, client_is_privileged); @@ -593,6 +596,7 @@ impl State { keyboard_shortcuts_inhibit_state, output_state, output_configuration_state, + output_power_state, presentation_state, primary_selection_state, data_control_state, diff --git a/src/wayland/handlers/mod.rs b/src/wayland/handlers/mod.rs index 82415aa4..78604722 100644 --- a/src/wayland/handlers/mod.rs +++ b/src/wayland/handlers/mod.rs @@ -19,6 +19,7 @@ pub mod keyboard_shortcuts_inhibit; pub mod layer_shell; pub mod output; pub mod output_configuration; +pub mod output_power; pub mod pointer_constraints; pub mod pointer_gestures; pub mod presentation; diff --git a/src/wayland/handlers/output_power.rs b/src/wayland/handlers/output_power.rs new file mode 100644 index 00000000..515c604e --- /dev/null +++ b/src/wayland/handlers/output_power.rs @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use smithay::output::Output; + +use crate::{ + backend::kms::Surface, + state::{BackendData, State}, + utils::prelude::OutputExt, + wayland::protocols::output_power::{ + delegate_output_power, OutputPowerHandler, OutputPowerState, + }, +}; + +pub fn set_all_surfaces_dpms_on(state: &mut State) { + let mut changed = false; + for surface in kms_surfaces(state) { + if !surface.get_dpms() { + surface.set_dpms(true); + changed = true; + } + } + + if changed { + OutputPowerState::refresh(state); + } +} + +fn kms_surfaces(state: &mut State) -> impl Iterator { + if let BackendData::Kms(ref mut kms_state) = &mut state.backend { + Some( + kms_state + .drm_devices + .values_mut() + .flat_map(|device| device.surfaces.values_mut()), + ) + } else { + None + } + .into_iter() + .flatten() +} + +// Get KMS `Surface` for output, and for all outputs mirroring it +fn kms_surfaces_for_output<'a>( + state: &'a mut State, + output: &'a Output, +) -> impl Iterator + 'a { + kms_surfaces(state).filter(move |surface| { + surface.output == *output || surface.output.mirroring().as_ref() == Some(&output) + }) +} + +// Get KMS `Surface` for output +fn primary_kms_surface_for_output<'a>( + state: &'a mut State, + output: &Output, +) -> Option<&'a mut Surface> { + kms_surfaces(state).find(|surface| surface.output == *output) +} + +impl OutputPowerHandler for State { + fn output_power_state(&mut self) -> &mut OutputPowerState { + &mut self.common.output_power_state + } + + fn get_dpms(&mut self, output: &Output) -> Option { + let surface = primary_kms_surface_for_output(self, output)?; + Some(surface.get_dpms()) + } + + fn set_dpms(&mut self, output: &Output, on: bool) { + for surface in kms_surfaces_for_output(self, output) { + surface.set_dpms(on); + } + } +} + +delegate_output_power!(State); diff --git a/src/wayland/protocols/mod.rs b/src/wayland/protocols/mod.rs index 6010d69e..7d18b003 100644 --- a/src/wayland/protocols/mod.rs +++ b/src/wayland/protocols/mod.rs @@ -3,6 +3,7 @@ pub mod drm; pub mod image_source; pub mod output_configuration; +pub mod output_power; pub mod overlap_notify; pub mod screencopy; pub mod toplevel_info; diff --git a/src/wayland/protocols/output_power.rs b/src/wayland/protocols/output_power.rs new file mode 100644 index 00000000..25a98de0 --- /dev/null +++ b/src/wayland/protocols/output_power.rs @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use smithay::{ + output::{Output, WeakOutput}, + reexports::{ + wayland_protocols_wlr::output_power_management::v1::server::{ + zwlr_output_power_manager_v1::{self, ZwlrOutputPowerManagerV1}, + zwlr_output_power_v1::{self, ZwlrOutputPowerV1}, + }, + wayland_server::{ + backend::GlobalId, Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, + Resource, + }, + }, +}; +use std::{collections::HashMap, mem}; +use wayland_backend::{protocol::WEnum, server::ClientId}; + +pub trait OutputPowerHandler { + fn output_power_state(&mut self) -> &mut OutputPowerState; + fn get_dpms(&mut self, output: &Output) -> Option; + fn set_dpms(&mut self, output: &Output, on: bool); +} + +#[derive(Debug)] +pub struct OutputPowerState { + global: GlobalId, + output_powers: HashMap, +} + +impl OutputPowerState { + pub fn new(dh: &DisplayHandle, client_filter: F) -> OutputPowerState + where + D: GlobalDispatch + 'static, + F: for<'a> Fn(&'a Client) -> bool + Clone + Send + Sync + 'static, + { + let global = dh.create_global::( + 1, + OutputPowerManagerGlobalData { + filter: Box::new(client_filter.clone()), + }, + ); + + OutputPowerState { + global, + output_powers: HashMap::new(), + } + } + + pub fn global_id(&self) -> GlobalId { + self.global.clone() + } + + /// Send `mode` events for any output powers where dpms state has changed. + /// + /// This is handled automatically for changes made through the protocol. + pub fn refresh(state: &mut D) { + let mut output_powers = mem::take(&mut state.output_power_state().output_powers); + for (output_power, old_mode) in output_powers.iter_mut() { + let data = output_power.data::().unwrap(); + if let Some(output) = data.output.as_ref().and_then(|o| o.upgrade()) { + if let Some(on) = state.get_dpms(&output) { + let mode = output_power_mode(on); + if mode != *old_mode { + output_power.mode(mode); + *old_mode = mode; + } + } + } + } + state.output_power_state().output_powers = output_powers; + } +} + +pub struct OutputPowerManagerGlobalData { + filter: Box Fn(&'a Client) -> bool + Send + Sync>, +} + +pub struct OutputPowerData { + output: Option, +} + +impl GlobalDispatch + for OutputPowerState +where + D: GlobalDispatch + + Dispatch + + 'static, +{ + fn bind( + _state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &OutputPowerManagerGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, ()); + } + + fn can_view(client: Client, global_data: &OutputPowerManagerGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for OutputPowerState +where + D: GlobalDispatch + + Dispatch + + Dispatch + + OutputPowerHandler + + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + _obj: &ZwlrOutputPowerManagerV1, + request: zwlr_output_power_manager_v1::Request, + _data: &(), + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_power_manager_v1::Request::GetOutputPower { id, output } => { + let output = Output::from_resource(&output); + let output_power = data_init.init( + id, + OutputPowerData { + output: output.as_ref().map(|o| o.downgrade()), + }, + ); + if let Some(on) = output.as_ref().and_then(|o| state.get_dpms(o)) { + let mode = output_power_mode(on); + output_power.mode(mode); + state + .output_power_state() + .output_powers + .insert(output_power, mode); + } else { + output_power.failed(); + } + } + zwlr_output_power_manager_v1::Request::Destroy => {} + _ => unreachable!(), + } + } +} + +impl Dispatch for OutputPowerState +where + D: Dispatch + OutputPowerHandler + 'static, +{ + fn request( + state: &mut D, + _client: &Client, + obj: &ZwlrOutputPowerV1, + request: zwlr_output_power_v1::Request, + data: &OutputPowerData, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + zwlr_output_power_v1::Request::SetMode { mode } => { + if let Some(output) = data.output.as_ref().and_then(|o| o.upgrade()) { + let on = match mode { + WEnum::Value(zwlr_output_power_v1::Mode::On) => true, + WEnum::Value(zwlr_output_power_v1::Mode::Off) => false, + _ => { + return; + } + }; + state.set_dpms(&output, on); + if let Some(on) = state.get_dpms(&output) { + let mode = output_power_mode(on); + for (output_power, old_mode) in + state.output_power_state().output_powers.iter_mut() + { + let data = output_power.data::().unwrap(); + if let Some(o) = data.output.as_ref() { + if o == &output && mode != *old_mode { + output_power.mode(mode); + *old_mode = mode; + } + } + } + } else { + obj.failed(); + state.output_power_state().output_powers.remove(obj); + } + } else { + obj.failed(); + state.output_power_state().output_powers.remove(obj); + } + } + zwlr_output_power_v1::Request::Destroy => {} + _ => unreachable!(), + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + obj: &ZwlrOutputPowerV1, + _data: &OutputPowerData, + ) { + state.output_power_state().output_powers.remove(obj); + } +} + +fn output_power_mode(on: bool) -> zwlr_output_power_v1::Mode { + if on { + zwlr_output_power_v1::Mode::On + } else { + zwlr_output_power_v1::Mode::Off + } +} + +macro_rules! delegate_output_power { + ($(@<$( $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::wayland::protocols::output_power::OutputPowerManagerGlobalData + ] => $crate::wayland::protocols::output_power::OutputPowerState); + 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::wayland::protocols::output_power::OutputPowerState); + 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::wayland::protocols::output_power::OutputPowerData + ] => $crate::wayland::protocols::output_power::OutputPowerState); + }; +} +pub(crate) use delegate_output_power;