diff --git a/release-notes.md b/release-notes.md index 8c6b135a..a8fcb002 100644 --- a/release-notes.md +++ b/release-notes.md @@ -35,6 +35,7 @@ responsiveness under high system load. This is described in detail in [setup.md](docs/setup.md). - Implement wlr-foreign-toplevel-management-v1. +- Implement wlr-output-management-v1. # 1.10.0 (2025-04-22) diff --git a/src/backend.rs b/src/backend.rs index 39fe5a58..49320aff 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -69,7 +69,7 @@ pub trait Backend: Any { } } -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)] pub struct Mode { pub width: i32, pub height: i32, diff --git a/src/client/objects.rs b/src/client/objects.rs index ab625f91..c378e5e5 100644 --- a/src/client/objects.rs +++ b/src/client/objects.rs @@ -29,6 +29,9 @@ use { WlSurface, xdg_surface::{XdgSurface, xdg_popup::XdgPopup, xdg_toplevel::XdgToplevel}, }, + wlr_output_manager::{ + zwlr_output_head_v1::ZwlrOutputHeadV1, zwlr_output_mode_v1::ZwlrOutputModeV1, + }, workspace_manager::ext_workspace_group_handle_v1::ExtWorkspaceGroupHandleV1, wp_drm_lease_connector_v1::WpDrmLeaseConnectorV1, wp_linux_drm_syncobj_timeline_v1::WpLinuxDrmSyncobjTimelineV1, @@ -47,8 +50,8 @@ use { WlDataSourceId, WlOutputId, WlPointerId, WlRegionId, WlRegistryId, WlSeatId, WlSurfaceId, WpDrmLeaseConnectorV1Id, WpImageDescriptionV1Id, WpLinuxDrmSyncobjTimelineV1Id, XdgPopupId, XdgPositionerId, XdgSurfaceId, - XdgToplevelId, XdgWmBaseId, ZwlrDataControlSourceV1Id, ZwpPrimarySelectionSourceV1Id, - ZwpTabletToolV2Id, + XdgToplevelId, XdgWmBaseId, ZwlrDataControlSourceV1Id, ZwlrOutputHeadV1Id, + ZwlrOutputModeV1Id, ZwpPrimarySelectionSourceV1Id, ZwpTabletToolV2Id, }, }, std::{cell::RefCell, rc::Rc}, @@ -76,6 +79,8 @@ pub struct Objects { pub screencasts: CopyHashMap>, pub timelines: CopyHashMap>, pub zwlr_data_sources: CopyHashMap>, + pub zwlr_output_heads: CopyHashMap>, + pub zwlr_output_modes: CopyHashMap>, pub jay_toplevels: CopyHashMap>, pub drm_lease_outputs: CopyHashMap>, pub tablet_tools: CopyHashMap>, @@ -119,6 +124,8 @@ impl Objects { screencasts: Default::default(), timelines: Default::default(), zwlr_data_sources: Default::default(), + zwlr_output_heads: Default::default(), + zwlr_output_modes: Default::default(), jay_toplevels: Default::default(), drm_lease_outputs: Default::default(), tablet_tools: Default::default(), @@ -147,6 +154,8 @@ impl Objects { self.registry.clear(); self.registries.clear(); self.outputs.clear(); + self.zwlr_output_heads.clear(); + self.zwlr_output_modes.clear(); self.surfaces.clear(); self.xdg_surfaces.clear(); self.xdg_toplevel.clear(); diff --git a/src/compositor.rs b/src/compositor.rs index 020b960f..7dda9add 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -36,6 +36,7 @@ use { wl_output::{OutputId, PersistentOutputState, WlOutputGlobal}, wl_seat::handle_position_hint_requests, wl_surface::{NoneSurfaceExt, zwp_input_popup_surface_v2::input_popup_positioning}, + wlr_output_manager::wlr_output_manager_done, workspace_manager::workspace_manager_done, }, io_uring::{IoUring, IoUringError}, @@ -246,6 +247,7 @@ fn start_compositor2( logger: logger.clone(), connectors: Default::default(), outputs: Default::default(), + wlr_output_managers: Default::default(), drm_devs: Default::default(), status: Default::default(), idle: IdleState { @@ -474,6 +476,10 @@ fn start_global_event_handlers(state: &Rc) -> Vec> { Phase::PostLayout, output_render_data(state.clone()), ), + eng.spawn( + "wlr output manager done", + wlr_output_manager_done(state.clone()), + ), eng.spawn2("float layout", Phase::Layout, float_layout(state.clone())), eng.spawn2( "float titles", @@ -670,6 +676,7 @@ fn create_dummy_output(state: &Rc) { handler: Cell::new(None), connected: Cell::new(true), name, + description: Default::default(), drm_dev: None, async_event: Default::default(), damaged: Cell::new(false), @@ -678,6 +685,7 @@ fn create_dummy_output(state: &Rc) { damage_intersect: Default::default(), state: Cell::new(backend_state), head_managers: HeadManagers::new(head_name, head_state), + wlr_output_heads: Default::default(), }); let schedule = Rc::new(OutputSchedule::new( &state.ring, diff --git a/src/globals.rs b/src/globals.rs index 2aeab076..4a39e817 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -42,6 +42,7 @@ use { wl_shm::WlShmGlobal, wl_subcompositor::WlSubcompositorGlobal, wl_surface::xwayland_shell_v1::XwaylandShellV1Global, + wlr_output_manager::zwlr_output_manager_v1::ZwlrOutputManagerV1Global, workspace_manager::ext_workspace_manager_v1::ExtWorkspaceManagerV1Global, wp_alpha_modifier_v1::WpAlphaModifierV1Global, wp_commit_timing_manager_v1::WpCommitTimingManagerV1Global, @@ -186,6 +187,7 @@ impl Globals { add_singleton!(OrgKdeKwinServerDecorationManagerGlobal); add_singleton!(ZwpPrimarySelectionDeviceManagerV1Global); add_singleton!(ZwlrLayerShellV1Global); + add_singleton!(ZwlrOutputManagerV1Global); add_singleton!(ZxdgOutputManagerV1Global); add_singleton!(JayCompositorGlobal); add_singleton!(ZwlrScreencopyManagerV1Global); diff --git a/src/ifs.rs b/src/ifs.rs index c3995f05..74b2fca6 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -52,6 +52,7 @@ pub mod wl_shm; pub mod wl_shm_pool; pub mod wl_subcompositor; pub mod wl_surface; +pub mod wlr_output_manager; pub mod workspace_manager; pub mod wp_alpha_modifier_v1; pub mod wp_commit_timing_manager_v1; diff --git a/src/ifs/head_management/jay_head_manager_session_v1.rs b/src/ifs/head_management/jay_head_manager_session_v1.rs index 7053dfa4..05394ad2 100644 --- a/src/ifs/head_management/jay_head_manager_session_v1.rs +++ b/src/ifs/head_management/jay_head_manager_session_v1.rs @@ -3,16 +3,13 @@ use { crate::{ backend::transaction::{ConnectorTransaction, PreparedConnectorTransaction}, client::{Client, ClientError}, - ifs::{ - head_management::{ - Head, HeadCommon, HeadCommonError, HeadMgrState, HeadName, HeadOp, - HeadTransactionError, - head_management_macros::{HeadExtension, MgrExts, announce_head, bind_extension}, - jay_head_manager_v1::JayHeadManagerV1, - jay_head_transaction_result_v1::JayHeadTransactionResultV1, - jay_head_v1::JayHeadV1, - }, - wl_output::PersistentOutputState, + ifs::head_management::{ + Head, HeadCommon, HeadCommonError, HeadMgrState, HeadName, HeadOp, + HeadTransactionError, + head_management_macros::{HeadExtension, MgrExts, announce_head, bind_extension}, + jay_head_manager_v1::JayHeadManagerV1, + jay_head_transaction_result_v1::JayHeadTransactionResultV1, + jay_head_v1::JayHeadV1, }, leaks::Tracker, object::{Object, Version}, @@ -566,22 +563,7 @@ impl JayHeadManagerSessionV1RequestHandler for JayHeadManagerSessionV1 { node.set_brightness(desired.brightness); } else if let Some(mi) = &desired.monitor_info { let pos = &self.client.state.persistent_output_states; - let pos = match pos.get(&mi.output_id) { - Some(ps) => ps, - _ => { - let ps = Rc::new(PersistentOutputState { - transform: Default::default(), - scale: Default::default(), - pos: Default::default(), - vrr_mode: Cell::new(&VrrMode::Never), - vrr_cursor_hz: Default::default(), - tearing_mode: Cell::new(&TearingMode::Never), - brightness: Default::default(), - }); - pos.set(mi.output_id.clone(), ps.clone()); - ps - } - }; + let pos = pos.lock().entry(mi.output_id.clone()).or_default().clone(); pos.pos.set(desired.position); pos.scale.set(desired.scale); pos.transform.set(desired.transform); diff --git a/src/ifs/wl_output.rs b/src/ifs/wl_output.rs index dc0dc28b..9872a970 100644 --- a/src/ifs/wl_output.rs +++ b/src/ifs/wl_output.rs @@ -125,6 +125,20 @@ pub struct PersistentOutputState { pub brightness: Cell>, } +impl Default for PersistentOutputState { + fn default() -> Self { + Self { + transform: Default::default(), + scale: Default::default(), + pos: Default::default(), + vrr_mode: Cell::new(&VrrMode::Never), + vrr_cursor_hz: Default::default(), + tearing_mode: Cell::new(&TearingMode::Never), + brightness: Default::default(), + } + } +} + #[derive(Eq, PartialEq, Hash, Debug)] pub struct OutputId { pub connector: Option, diff --git a/src/ifs/wlr_output_manager.rs b/src/ifs/wlr_output_manager.rs new file mode 100644 index 00000000..5b2948d8 --- /dev/null +++ b/src/ifs/wlr_output_manager.rs @@ -0,0 +1,47 @@ +use { + crate::{ + ifs::wlr_output_manager::zwlr_output_head_v1::WlrOutputHeadIds, + state::{OutputData, State}, + utils::{copyhashmap::CopyHashMap, queue::AsyncQueue}, + }, + std::rc::Rc, + zwlr_output_manager_v1::{WlrOutputManagerId, WlrOutputManagerIds, ZwlrOutputManagerV1}, +}; + +pub mod zwlr_output_configuration_head; +pub mod zwlr_output_configuration_v1; +pub mod zwlr_output_head_v1; +pub mod zwlr_output_manager_v1; +pub mod zwlr_output_mode_v1; + +#[derive(Default)] +pub struct WlrOutputManagerState { + queue: AsyncQueue>, + ids: WlrOutputManagerIds, + head_ids: WlrOutputHeadIds, + managers: CopyHashMap>, +} + +impl WlrOutputManagerState { + pub fn clear(&self) { + self.managers.clear(); + self.queue.clear(); + } + + pub fn announce_head(&self, on: &Rc) { + for manager in self.managers.lock().values() { + manager.announce_head(on); + } + } +} + +pub async fn wlr_output_manager_done(state: Rc) { + loop { + let manager = state.wlr_output_managers.queue.pop().await; + if manager.destroyed.get() { + continue; + } + manager.done_scheduled.set(false); + manager.send_done(); + } +} diff --git a/src/ifs/wlr_output_manager/zwlr_output_configuration_head.rs b/src/ifs/wlr_output_manager/zwlr_output_configuration_head.rs new file mode 100644 index 00000000..9e1d32e2 --- /dev/null +++ b/src/ifs/wlr_output_manager/zwlr_output_configuration_head.rs @@ -0,0 +1,146 @@ +use { + crate::{ + backend::Mode, + client::{Client, ClientError}, + fixed::Fixed, + ifs::wlr_output_manager::zwlr_output_head_v1::{ + ADAPTIVE_SYNC_STATE_DISABLED, ADAPTIVE_SYNC_STATE_ENABLED, WlrOutputHeadId, + }, + leaks::Tracker, + object::{Object, Version}, + scale::Scale, + tree::VrrMode, + utils::transform_ext::TransformExt, + wire::{ZwlrOutputConfigurationHeadV1Id, zwlr_output_configuration_head_v1::*}, + }, + jay_config::video::Transform, + std::{cell::RefCell, rc::Rc}, + thiserror::Error, +}; + +pub struct ZwlrOutputConfigurationHeadV1 { + pub(super) id: ZwlrOutputConfigurationHeadV1Id, + pub(super) head_id: WlrOutputHeadId, + pub(super) version: Version, + pub(super) client: Rc, + pub(super) config: RefCell, + pub(super) tracker: Tracker, +} + +#[derive(Default, Copy, Clone)] +pub struct OutputConfig { + pub(super) transform: Option, + pub(super) scale: Option, + pub(super) vrr_mode: Option<&'static VrrMode>, + pub(super) pos: Option<(i32, i32)>, + pub(super) mode: Option, +} + +impl ZwlrOutputConfigurationHeadV1RequestHandler for ZwlrOutputConfigurationHeadV1 { + type Error = ZwlrOutputConfigurationHeadV1Error; + + fn set_mode(&self, req: SetMode, _slf: &Rc) -> Result<(), Self::Error> { + let config = &mut *self.config.borrow_mut(); + if config.mode.is_some() { + return Err(ZwlrOutputConfigurationHeadV1Error::AlreadySet); + } + let mode = self.client.lookup(req.mode)?; + if self.head_id != mode.head_id { + return Err(ZwlrOutputConfigurationHeadV1Error::InvalidMode); + } + config.mode = Some(mode.mode); + Ok(()) + } + + fn set_custom_mode(&self, req: SetCustomMode, _slf: &Rc) -> Result<(), Self::Error> { + let config = &mut *self.config.borrow_mut(); + if config.mode.is_some() { + return Err(ZwlrOutputConfigurationHeadV1Error::AlreadySet); + } + config.mode = Some(Mode { + width: req.width, + height: req.height, + refresh_rate_millihz: req.refresh as u32, + }); + Ok(()) + } + + fn set_position(&self, req: SetPosition, _slf: &Rc) -> Result<(), Self::Error> { + let config = &mut *self.config.borrow_mut(); + if config.pos.is_some() { + return Err(ZwlrOutputConfigurationHeadV1Error::AlreadySet); + } + config.pos = Some((req.x, req.y)); + Ok(()) + } + + fn set_transform(&self, req: SetTransform, _slf: &Rc) -> Result<(), Self::Error> { + let config = &mut *self.config.borrow_mut(); + if config.transform.is_some() { + return Err(ZwlrOutputConfigurationHeadV1Error::AlreadySet); + } + let Some(transform) = Transform::from_wl(req.transform) else { + return Err(ZwlrOutputConfigurationHeadV1Error::InvalidTransform( + req.transform, + )); + }; + config.transform = Some(transform); + Ok(()) + } + + fn set_scale(&self, req: SetScale, _slf: &Rc) -> Result<(), Self::Error> { + let config = &mut *self.config.borrow_mut(); + if config.scale.is_some() { + return Err(ZwlrOutputConfigurationHeadV1Error::AlreadySet); + } + if req.scale <= 0 { + return Err(ZwlrOutputConfigurationHeadV1Error::InvalidScale(req.scale)); + } + config.scale = Some(Scale::from_f64(req.scale.to_f64())); + Ok(()) + } + + fn set_adaptive_sync(&self, req: SetAdaptiveSync, _slf: &Rc) -> Result<(), Self::Error> { + let config = &mut *self.config.borrow_mut(); + if config.vrr_mode.is_some() { + return Err(ZwlrOutputConfigurationHeadV1Error::AlreadySet); + } + let state = match req.state { + ADAPTIVE_SYNC_STATE_DISABLED => VrrMode::NEVER, + ADAPTIVE_SYNC_STATE_ENABLED => VrrMode::ALWAYS, + _ => { + return Err( + ZwlrOutputConfigurationHeadV1Error::InvalidAdaptiveSyncState(req.state), + ); + } + }; + config.vrr_mode = Some(state); + Ok(()) + } +} + +object_base! { + self = ZwlrOutputConfigurationHeadV1; + version = self.version; +} + +impl Object for ZwlrOutputConfigurationHeadV1 {} + +simple_add_obj!(ZwlrOutputConfigurationHeadV1); + +#[derive(Debug, Error)] +pub enum ZwlrOutputConfigurationHeadV1Error { + #[error(transparent)] + ClientError(Box), + #[error("Property has already been set")] + AlreadySet, + #[error("Mode doesn't belong to head")] + InvalidMode, + #[error("Unknown transform {0}")] + InvalidTransform(i32), + #[error("Invalid scale {0}")] + InvalidScale(Fixed), + #[error("Invalid adaptive sync state {0}")] + InvalidAdaptiveSyncState(u32), +} +efrom!(ZwlrOutputConfigurationHeadV1Error, ClientError); diff --git a/src/ifs/wlr_output_manager/zwlr_output_configuration_v1.rs b/src/ifs/wlr_output_manager/zwlr_output_configuration_v1.rs new file mode 100644 index 00000000..a1ad5217 --- /dev/null +++ b/src/ifs/wlr_output_manager/zwlr_output_configuration_v1.rs @@ -0,0 +1,256 @@ +use { + crate::{ + backend::{ + ConnectorId, + transaction::{ + BackendConnectorTransactionError, ConnectorTransaction, + PreparedConnectorTransaction, + }, + }, + client::{Client, ClientError}, + ifs::wlr_output_manager::{ + zwlr_output_configuration_head::ZwlrOutputConfigurationHeadV1, + zwlr_output_manager_v1::ZwlrOutputManagerV1, + }, + leaks::Tracker, + object::{Object, Version}, + utils::{copyhashmap::CopyHashMap, errorfmt::ErrorFmt, hash_map_ext::HashMapExt}, + wire::{ZwlrOutputConfigurationV1Id, zwlr_output_configuration_v1::*}, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +pub struct ZwlrOutputConfigurationV1 { + pub(super) id: ZwlrOutputConfigurationV1Id, + pub(super) version: Version, + pub(super) client: Rc, + pub(super) tracker: Tracker, + pub(super) serial: u64, + pub(super) manager: Rc, + pub(super) used: Cell, + pub(super) enabled_outputs: CopyHashMap>, + pub(super) configured_outputs: CopyHashMap, +} + +#[derive(Debug, Error)] +enum ConfigError { + #[error("Serial is out of date")] + OutOfDate, + #[error("Unconfigured output {0}")] + UnconfiguredOutput(Rc), + #[error("Could not add output to transaction")] + AddToTransaction(#[source] BackendConnectorTransactionError), + #[error("Could not prepare transaction")] + PrepareTransaction(#[source] BackendConnectorTransactionError), + #[error("Could not apply transaction")] + ApplyTransaction(#[source] BackendConnectorTransactionError), +} + +impl ZwlrOutputConfigurationV1 { + pub fn send_succeeded(&self) { + self.client.event(Succeeded { self_id: self.id }); + } + + pub fn send_failed(&self) { + self.client.event(Failed { self_id: self.id }); + } + + pub fn send_cancelled(&self) { + self.client.event(Cancelled { self_id: self.id }); + } + + fn assert_unused(&self) -> Result<(), ZwlrOutputConfigurationV1Error> { + if self.used.get() { + return Err(ZwlrOutputConfigurationV1Error::AlreadyUsed); + } + Ok(()) + } + + fn prepare_transaction(&self) -> Result { + if self.serial < self.manager.serial.get() { + return Err(ConfigError::OutOfDate); + } + let mut tran = ConnectorTransaction::new(&self.client.state); + for output in self.client.state.outputs.lock().values() { + let mut state = output.connector.state.get(); + match self.enabled_outputs.get(&output.connector.id) { + None => { + if self.configured_outputs.not_contains(&output.connector.id) { + return Err(ConfigError::UnconfiguredOutput( + output.connector.name.clone(), + )); + } + state.enabled = false; + } + Some(config) => { + state.enabled = true; + let config = *config.config.borrow(); + if let Some(mode) = config.mode { + state.mode = mode; + } + } + } + tran.add(&output.connector.connector, state) + .map_err(ConfigError::AddToTransaction)?; + } + tran.prepare().map_err(ConfigError::PrepareTransaction) + } + + fn apply_transaction(&self) -> Result<(), ConfigError> { + self.prepare_transaction()? + .apply() + .map_err(ConfigError::ApplyTransaction)? + .commit(); + for output in self.client.state.outputs.lock().values() { + let Some(config) = self.enabled_outputs.get(&output.connector.id) else { + continue; + }; + let config = *config.config.borrow(); + if let Some(node) = &output.node { + if let Some(v) = config.transform { + node.update_transform(v); + } + if let Some(v) = config.scale { + node.set_preferred_scale(v); + } + if let Some(v) = config.vrr_mode { + node.set_vrr_mode(v); + } + if let Some(v) = config.pos { + node.set_position(v.0, v.1); + } + } else { + let mi = &output.monitor_info; + let pos = &self.client.state.persistent_output_states; + let pos = pos.lock().entry(mi.output_id.clone()).or_default().clone(); + if let Some(v) = config.transform { + pos.transform.set(v); + } + if let Some(v) = config.scale { + pos.scale.set(v); + } + if let Some(v) = config.vrr_mode { + pos.vrr_mode.set(v); + } + if let Some(v) = config.pos { + pos.pos.set(v); + } + } + } + Ok(()) + } +} + +impl ZwlrOutputConfigurationV1RequestHandler for ZwlrOutputConfigurationV1 { + type Error = ZwlrOutputConfigurationV1Error; + + fn enable_head(&self, req: EnableHead, _slf: &Rc) -> Result<(), Self::Error> { + self.assert_unused()?; + let head = self.client.lookup(req.head)?; + if self.configured_outputs.set(head.connector_id, ()).is_some() { + return Err(ZwlrOutputConfigurationV1Error::AlreadyConfiguredHead( + head.output.connector.name.clone(), + )); + } + let configuration_head = Rc::new(ZwlrOutputConfigurationHeadV1 { + id: req.id, + head_id: head.head_id, + version: self.version, + client: self.client.clone(), + config: Default::default(), + tracker: Default::default(), + }); + track!(self.client, configuration_head); + self.client.add_client_obj(&configuration_head)?; + self.enabled_outputs + .set(head.connector_id, configuration_head); + Ok(()) + } + + fn disable_head(&self, req: DisableHead, _slf: &Rc) -> Result<(), Self::Error> { + self.assert_unused()?; + let head = self.client.lookup(req.head)?; + if self.configured_outputs.set(head.connector_id, ()).is_some() { + return Err(ZwlrOutputConfigurationV1Error::AlreadyConfiguredHead( + head.output.connector.name.clone(), + )); + } + Ok(()) + } + + fn apply(&self, _req: Apply, _slf: &Rc) -> Result<(), Self::Error> { + self.assert_unused()?; + self.used.set(true); + let Err(e) = self.apply_transaction() else { + self.send_succeeded(); + return Ok(()); + }; + log::error!("Could not apply output configuration: {}", ErrorFmt(&e)); + match e { + ConfigError::UnconfiguredOutput(o) => { + return Err(ZwlrOutputConfigurationV1Error::UnconfiguredHead(o)); + } + ConfigError::OutOfDate => { + self.send_cancelled(); + return Ok(()); + } + ConfigError::AddToTransaction(_) + | ConfigError::PrepareTransaction(_) + | ConfigError::ApplyTransaction(_) => {} + } + self.send_failed(); + Ok(()) + } + + fn test(&self, _req: Test, _slf: &Rc) -> Result<(), Self::Error> { + self.assert_unused()?; + self.used.set(true); + let Err(e) = self.prepare_transaction() else { + self.send_succeeded(); + return Ok(()); + }; + log::error!("Could not test output configuration: {}", ErrorFmt(&e)); + match e { + ConfigError::UnconfiguredOutput(o) => { + return Err(ZwlrOutputConfigurationV1Error::UnconfiguredHead(o)); + } + ConfigError::OutOfDate + | ConfigError::AddToTransaction(_) + | ConfigError::PrepareTransaction(_) + | ConfigError::ApplyTransaction(_) => {} + } + self.send_failed(); + Ok(()) + } + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + for head in self.enabled_outputs.lock().drain_values() { + self.client.remove_obj(&*head)?; + } + Ok(()) + } +} + +object_base! { + self = ZwlrOutputConfigurationV1; + version = self.version; +} + +impl Object for ZwlrOutputConfigurationV1 {} + +simple_add_obj!(ZwlrOutputConfigurationV1); + +#[derive(Debug, Error)] +pub enum ZwlrOutputConfigurationV1Error { + #[error(transparent)] + ClientError(Box), + #[error("Head {0} has alread been configured")] + AlreadyConfiguredHead(Rc), + #[error("Head {0} has not been configured")] + UnconfiguredHead(Rc), + #[error("Configuration has already been tested or applied")] + AlreadyUsed, +} +efrom!(ZwlrOutputConfigurationV1Error, ClientError); diff --git a/src/ifs/wlr_output_manager/zwlr_output_head_v1.rs b/src/ifs/wlr_output_manager/zwlr_output_head_v1.rs new file mode 100644 index 00000000..21d773a6 --- /dev/null +++ b/src/ifs/wlr_output_manager/zwlr_output_head_v1.rs @@ -0,0 +1,250 @@ +use { + crate::{ + backend::{self, ConnectorId}, + client::{Client, ClientError}, + fixed::Fixed, + ifs::wlr_output_manager::{ + zwlr_output_manager_v1::{WlrOutputManagerId, ZwlrOutputManagerV1}, + zwlr_output_mode_v1::ZwlrOutputModeV1, + }, + leaks::Tracker, + object::{Object, Version}, + scale, + state::OutputData, + tree::VrrMode, + utils::transform_ext::TransformExt, + wire::{ZwlrOutputHeadV1Id, zwlr_output_head_v1::*}, + }, + ahash::AHashMap, + jay_config::video, + std::rc::Rc, + thiserror::Error, +}; + +pub const MAKE_SINCE: Version = Version(2); +pub const MODEL_SINCE: Version = Version(2); +pub const SERIAL_NUMBER_SINCE: Version = Version(2); +#[expect(dead_code)] +pub const RELEASE_SINCE: Version = Version(3); +pub const ADAPTIVE_SYNC_SINCE: Version = Version(4); + +pub const HEAD_DISABLED: i32 = 0; +pub const HEAD_ENABLED: i32 = 1; + +pub const ADAPTIVE_SYNC_STATE_DISABLED: u32 = 0; +pub const ADAPTIVE_SYNC_STATE_ENABLED: u32 = 1; + +linear_ids!(WlrOutputHeadIds, WlrOutputHeadId, u64); + +pub struct ZwlrOutputHeadV1 { + pub(super) id: ZwlrOutputHeadV1Id, + pub(super) version: Version, + pub(super) client: Rc, + pub(super) tracker: Tracker, + pub(super) output: Rc, + pub(super) manager_id: WlrOutputManagerId, + pub(super) manager: Rc, + pub(super) head_id: WlrOutputHeadId, + pub(super) connector_id: ConnectorId, + pub(super) modes: AHashMap>, +} + +impl ZwlrOutputHeadV1 { + fn detach(&self) { + self.output + .connector + .wlr_output_heads + .remove(&self.manager_id); + } +} + +impl ZwlrOutputHeadV1 { + pub fn send_name(&self, name: &str) { + self.client.event(Name { + self_id: self.id, + name, + }); + } + + pub fn send_description(&self, description: &str) { + self.client.event(Description { + self_id: self.id, + description, + }); + } + + pub fn send_physical_size(&self, width: i32, height: i32) { + self.client.event(PhysicalSize { + self_id: self.id, + width, + height, + }); + } + + pub fn send_mode(&self, mode: &ZwlrOutputModeV1) { + self.client.event(Mode { + self_id: self.id, + mode: mode.id, + }); + } + + pub fn send_enabled(&self, enabled: bool) { + let enabled = if enabled { HEAD_ENABLED } else { HEAD_DISABLED }; + self.client.event(Enabled { + self_id: self.id, + enabled, + }); + } + + pub fn send_current_mode(&self, mode: &ZwlrOutputModeV1) { + self.client.event(CurrentMode { + self_id: self.id, + mode: mode.id, + }); + } + + pub fn send_position(&self, x: i32, y: i32) { + self.client.event(Position { + self_id: self.id, + x, + y, + }); + } + + pub fn send_transform(&self, transform: video::Transform) { + self.client.event(Transform { + self_id: self.id, + transform: transform.to_wl(), + }); + } + + pub fn send_scale(&self, scale: scale::Scale) { + let scale = Fixed::from_f64(scale.to_f64()); + self.client.event(Scale { + self_id: self.id, + scale, + }); + } + + pub fn send_finished(&self) { + self.client.event(Finished { self_id: self.id }) + } + + pub fn send_make(&self, make: &str) { + self.client.event(Make { + self_id: self.id, + make, + }); + } + + pub fn send_model(&self, model: &str) { + self.client.event(Model { + self_id: self.id, + model, + }); + } + + pub fn send_serial_number(&self, serial_number: &str) { + self.client.event(SerialNumber { + self_id: self.id, + serial_number, + }); + } + + pub fn send_adaptive_sync(&self, mode: &VrrMode) { + let state = if *mode == VrrMode::Always { + ADAPTIVE_SYNC_STATE_ENABLED + } else { + ADAPTIVE_SYNC_STATE_DISABLED + }; + self.client.event(AdaptiveSync { + self_id: self.id, + state, + }); + } + + pub fn announce_modes(&self, modes: &[Rc]) { + for mode in modes { + self.send_mode(mode); + mode.send(); + if mode.initial_current { + self.send_current_mode(mode); + } + } + } + + pub fn hande_transform_change(&self, transform: video::Transform) { + self.send_transform(transform); + self.manager.schedule_done(); + } + + pub fn handle_mode_change(&self, new: backend::Mode) { + let Some(mode) = self.modes.get(&new) else { + return; + }; + if mode.destroyed.get() { + return; + } + self.send_current_mode(mode); + self.manager.schedule_done(); + } + + pub fn handle_position_change(&self, x: i32, y: i32) { + self.send_position(x, y); + self.manager.schedule_done(); + } + + pub fn handle_vrr_mode_change(&self, mode: &VrrMode) { + if self.version < ADAPTIVE_SYNC_SINCE { + return; + } + self.send_adaptive_sync(mode); + self.manager.schedule_done(); + } + + pub fn handle_new_scale(&self, scale: scale::Scale) { + self.send_scale(scale); + self.manager.schedule_done(); + } + + pub fn handle_disconnected(&self) { + self.send_finished(); + for mode in self.modes.values() { + if !mode.destroyed.get() { + mode.send_finished(); + } + } + self.manager.schedule_done(); + } +} + +impl ZwlrOutputHeadV1RequestHandler for ZwlrOutputHeadV1 { + type Error = ZwlrOutputHeadV1Error; + + fn release(&self, _req: Release, _slf: &Rc) -> Result<(), Self::Error> { + self.send_finished(); + self.detach(); + self.client.remove_obj(self)?; + Ok(()) + } +} + +object_base! { + self = ZwlrOutputHeadV1; + version = self.version; +} + +impl Object for ZwlrOutputHeadV1 { + fn break_loops(&self) { + self.detach(); + } +} + +dedicated_add_obj!(ZwlrOutputHeadV1, ZwlrOutputHeadV1Id, zwlr_output_heads); + +#[derive(Debug, Error)] +pub enum ZwlrOutputHeadV1Error { + #[error(transparent)] + ClientError(Box), +} +efrom!(ZwlrOutputHeadV1Error, ClientError); diff --git a/src/ifs/wlr_output_manager/zwlr_output_manager_v1.rs b/src/ifs/wlr_output_manager/zwlr_output_manager_v1.rs new file mode 100644 index 00000000..8692a63b --- /dev/null +++ b/src/ifs/wlr_output_manager/zwlr_output_manager_v1.rs @@ -0,0 +1,289 @@ +use { + crate::{ + client::{CAP_HEAD_MANAGER, Client, ClientCaps, ClientError}, + globals::{Global, GlobalName}, + ifs::wlr_output_manager::{ + zwlr_output_configuration_v1::ZwlrOutputConfigurationV1, + zwlr_output_head_v1::{ + ADAPTIVE_SYNC_SINCE, MAKE_SINCE, MODEL_SINCE, SERIAL_NUMBER_SINCE, ZwlrOutputHeadV1, + }, + zwlr_output_mode_v1::ZwlrOutputModeV1, + }, + leaks::Tracker, + object::{Object, Version}, + state::OutputData, + utils::numcell::NumCell, + wire::{ZwlrOutputManagerV1Id, zwlr_output_manager_v1::*}, + }, + ahash::AHashMap, + isnt::std_1::string::IsntStringExt, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +linear_ids!(WlrOutputManagerIds, WlrOutputManagerId, u64); + +pub struct ZwlrOutputManagerV1Global { + name: GlobalName, +} + +pub struct ZwlrOutputManagerV1 { + pub(super) id: ZwlrOutputManagerV1Id, + pub(super) manager_id: WlrOutputManagerId, + pub(super) client: Rc, + pub(super) version: Version, + pub(super) tracker: Tracker, + pub(super) done_scheduled: Cell, + pub(super) serial: NumCell, + pub(super) destroyed: Cell, +} + +impl ZwlrOutputManagerV1 { + fn detach(&self) { + self.client + .state + .wlr_output_managers + .managers + .remove(&self.manager_id); + } +} + +impl ZwlrOutputManagerV1Global { + pub fn new(name: GlobalName) -> Self { + Self { name } + } + + fn bind_( + self: Rc, + id: ZwlrOutputManagerV1Id, + client: &Rc, + version: Version, + ) -> Result<(), ZwlrOutputManagerV1Error> { + let obj = Rc::new(ZwlrOutputManagerV1 { + id, + manager_id: client.state.wlr_output_managers.ids.next(), + client: client.clone(), + tracker: Default::default(), + version, + done_scheduled: Cell::new(false), + serial: Default::default(), + destroyed: Cell::new(false), + }); + track!(client, obj); + client.add_client_obj(&obj)?; + client + .state + .wlr_output_managers + .managers + .set(obj.manager_id, obj.clone()); + for output in client.state.outputs.lock().values() { + obj.announce_head(output); + } + Ok(()) + } +} + +impl ZwlrOutputManagerV1RequestHandler for ZwlrOutputManagerV1 { + type Error = ZwlrOutputManagerV1Error; + + fn create_configuration( + &self, + req: CreateConfiguration, + slf: &Rc, + ) -> Result<(), Self::Error> { + let last_serial = self.serial.get(); + let mut serial = (last_serial >> u32::BITS << u32::BITS) | (req.serial as u64); + if serial > last_serial { + serial = serial.saturating_sub(1 << u32::BITS); + } + let configuration = Rc::new(ZwlrOutputConfigurationV1 { + id: req.id, + client: self.client.clone(), + version: self.version, + tracker: Default::default(), + serial, + manager: slf.clone(), + used: Cell::new(false), + enabled_outputs: Default::default(), + configured_outputs: Default::default(), + }); + track!(self.client, configuration); + self.client.add_client_obj(&configuration)?; + Ok(()) + } + + fn stop(&self, _req: Stop, _slf: &Rc) -> Result<(), Self::Error> { + self.destroyed.set(true); + self.detach(); + self.send_finished(); + self.client.remove_obj(self)?; + Ok(()) + } +} + +impl ZwlrOutputManagerV1 { + pub fn announce_head(self: &Rc, output: &Rc) { + let id = match self.client.new_id() { + Ok(id) => id, + Err(e) => { + self.client.error(e); + return; + } + }; + let mi = &output.monitor_info; + let state = output.connector.state.get(); + let head_id = self.client.state.wlr_output_managers.head_ids.next(); + let mut modes_list = vec![]; + let mut modes = AHashMap::new(); + let mut have_current = false; + for (idx, mode) in mi.modes.iter().enumerate() { + if modes.contains_key(mode) { + continue; + } + let current = !have_current && *mode == state.mode; + if current { + have_current = true; + } + let id = match self.client.new_id() { + Ok(id) => id, + Err(e) => { + self.client.error(e); + return; + } + }; + let output_mode = Rc::new(ZwlrOutputModeV1 { + id, + head_id, + client: self.client.clone(), + tracker: Default::default(), + version: self.version, + mode: *mode, + preferred: idx == 0, + initial_current: current, + destroyed: Cell::new(false), + }); + track!(self.client, output_mode); + self.client.add_server_obj(&output_mode); + modes_list.push(output_mode.clone()); + modes.insert(*mode, output_mode); + } + let head = Rc::new(ZwlrOutputHeadV1 { + id, + client: self.client.clone(), + tracker: Default::default(), + version: self.version, + manager_id: self.manager_id, + manager: self.clone(), + head_id, + connector_id: output.connector.id, + modes, + output: output.clone(), + }); + track!(self.client, head); + self.client.add_server_obj(&head); + output + .connector + .wlr_output_heads + .set(self.manager_id, head.clone()); + self.send_head(&head); + head.send_name(&output.connector.name); + let description = &*output.connector.description.borrow(); + if description.is_not_empty() { + head.send_description(description); + } + head.send_enabled(!mi.non_desktop_effective); + head.announce_modes(&modes_list); + head.send_physical_size(mi.width_mm, mi.height_mm); + if mi.output_id.manufacturer.is_not_empty() && head.version >= MAKE_SINCE { + head.send_make(&mi.output_id.manufacturer); + } + if mi.output_id.model.is_not_empty() && head.version >= MODEL_SINCE { + head.send_model(&mi.output_id.model); + } + if mi.output_id.serial_number.is_not_empty() && head.version >= SERIAL_NUMBER_SINCE { + head.send_serial_number(&mi.output_id.serial_number); + } + if let Some(node) = &output.node { + let p = &node.global.persistent; + head.send_scale(p.scale.get()); + head.send_position(p.pos.get().0, p.pos.get().1); + head.send_transform(p.transform.get()); + if head.version >= ADAPTIVE_SYNC_SINCE { + head.send_adaptive_sync(p.vrr_mode.get()); + } + } + self.schedule_done(); + } + + pub fn send_head(&self, head: &ZwlrOutputHeadV1) { + self.client.event(Head { + self_id: self.id, + head: head.id, + }); + } + + pub fn send_done(&self) { + self.client.event(Done { + self_id: self.id, + serial: self.serial.get() as u32, + }); + } + + pub fn send_finished(&self) { + self.client.event(Finished { self_id: self.id }); + } + + pub(super) fn schedule_done(self: &Rc) { + if self.done_scheduled.replace(true) { + return; + } + self.serial.fetch_add(1); + self.client + .state + .wlr_output_managers + .queue + .push(self.clone()); + } +} + +global_base!( + ZwlrOutputManagerV1Global, + ZwlrOutputManagerV1, + ZwlrOutputManagerV1Error +); + +impl Global for ZwlrOutputManagerV1Global { + fn singleton(&self) -> bool { + true + } + + fn version(&self) -> u32 { + 4 + } + + fn required_caps(&self) -> ClientCaps { + CAP_HEAD_MANAGER + } +} + +simple_add_global!(ZwlrOutputManagerV1Global); + +object_base! { + self = ZwlrOutputManagerV1; + version = self.version; +} + +simple_add_obj!(ZwlrOutputManagerV1); + +impl Object for ZwlrOutputManagerV1 { + fn break_loops(&self) { + self.detach(); + } +} + +#[derive(Debug, Error)] +pub enum ZwlrOutputManagerV1Error { + #[error(transparent)] + ClientError(Box), +} +efrom!(ZwlrOutputManagerV1Error, ClientError); diff --git a/src/ifs/wlr_output_manager/zwlr_output_mode_v1.rs b/src/ifs/wlr_output_manager/zwlr_output_mode_v1.rs new file mode 100644 index 00000000..985515de --- /dev/null +++ b/src/ifs/wlr_output_manager/zwlr_output_mode_v1.rs @@ -0,0 +1,83 @@ +use { + crate::{ + backend::Mode, + client::{Client, ClientError}, + ifs::wlr_output_manager::zwlr_output_head_v1::WlrOutputHeadId, + leaks::Tracker, + object::{Object, Version}, + wire::{ZwlrOutputModeV1Id, zwlr_output_mode_v1::*}, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +pub struct ZwlrOutputModeV1 { + pub(super) id: ZwlrOutputModeV1Id, + pub(super) head_id: WlrOutputHeadId, + pub(super) version: Version, + pub(super) client: Rc, + pub(super) tracker: Tracker, + pub(super) mode: Mode, + pub(super) preferred: bool, + pub(super) initial_current: bool, + pub(super) destroyed: Cell, +} + +impl ZwlrOutputModeV1 { + pub fn send(&self) { + self.send_size(self.mode.width, self.mode.height); + self.send_refresh(self.mode.refresh_rate_millihz as _); + if self.preferred { + self.send_preferred(); + } + } + + fn send_size(&self, width: i32, height: i32) { + self.client.event(Size { + self_id: self.id, + width, + height, + }); + } + + fn send_refresh(&self, refresh: i32) { + self.client.event(Refresh { + self_id: self.id, + refresh, + }); + } + + fn send_preferred(&self) { + self.client.event(Preferred { self_id: self.id }); + } + + pub fn send_finished(&self) { + self.client.event(Finished { self_id: self.id }) + } +} + +impl ZwlrOutputModeV1RequestHandler for ZwlrOutputModeV1 { + type Error = ZwlrOutputModeV1Error; + + fn release(&self, _req: Release, _slf: &Rc) -> Result<(), Self::Error> { + self.destroyed.set(true); + self.client.remove_obj(self)?; + Ok(()) + } +} + +object_base! { + self = ZwlrOutputModeV1; + version = self.version; +} + +impl Object for ZwlrOutputModeV1 {} + +dedicated_add_obj!(ZwlrOutputModeV1, ZwlrOutputModeV1Id, zwlr_output_modes); + +#[derive(Debug, Error)] +pub enum ZwlrOutputModeV1Error { + #[error(transparent)] + ClientError(Box), +} +efrom!(ZwlrOutputModeV1Error, ClientError); diff --git a/src/ifs/zxdg_output_v1.rs b/src/ifs/zxdg_output_v1.rs index f33103dc..1ea76a27 100644 --- a/src/ifs/zxdg_output_v1.rs +++ b/src/ifs/zxdg_output_v1.rs @@ -11,7 +11,6 @@ use { }; pub const NAME_SINCE: Version = Version(2); -#[expect(dead_code)] pub const DESCRIPTION_SINCE: Version = Version(2); pub const NO_DONE_SINCE: Version = Version(3); @@ -53,7 +52,6 @@ impl ZxdgOutputV1 { }); } - #[expect(dead_code)] pub fn send_description(&self, description: &str) { self.client.event(Description { self_id: self.id, @@ -71,6 +69,9 @@ impl ZxdgOutputV1 { if self.version >= NAME_SINCE { self.send_name(&global.connector.name); } + if self.version >= DESCRIPTION_SINCE { + self.send_description(&global.connector.description.borrow()); + } if self.version >= NO_DONE_SINCE { if self.output.version >= SEND_DONE_SINCE { self.output.send_done(); diff --git a/src/state.rs b/src/state.rs index 1b31ed52..47747cf7 100644 --- a/src/state.rs +++ b/src/state.rs @@ -67,6 +67,10 @@ use { zwp_idle_inhibitor_v1::{IdleInhibitorId, IdleInhibitorIds, ZwpIdleInhibitorV1}, zwp_input_popup_surface_v2::ZwpInputPopupSurfaceV2, }, + wlr_output_manager::{ + WlrOutputManagerState, zwlr_output_head_v1::ZwlrOutputHeadV1, + zwlr_output_manager_v1::WlrOutputManagerId, + }, workspace_manager::WorkspaceManagerState, wp_drm_lease_connector_v1::WpDrmLeaseConnectorV1, wp_drm_lease_device_v1::WpDrmLeaseDeviceV1Global, @@ -185,6 +189,7 @@ pub struct State { pub logger: Option>, pub connectors: CopyHashMap>, pub outputs: CopyHashMap>, + pub wlr_output_managers: WlrOutputManagerState, pub drm_devs: CopyHashMap>, pub status: CloneCell>, pub idle: IdleState, @@ -382,6 +387,7 @@ pub struct ConnectorData { pub handler: Cell>>, pub connected: Cell, pub name: Rc, + pub description: RefCell, pub drm_dev: Option>, pub async_event: Rc, pub damaged: Cell, @@ -390,6 +396,7 @@ pub struct ConnectorData { pub damage_intersect: Cell, pub state: Cell, pub head_managers: HeadManagers, + pub wlr_output_heads: CopyHashMap>, } pub struct OutputData { @@ -468,6 +475,9 @@ impl ConnectorData { } if old.mode != s.mode { self.head_managers.handle_mode_change(s.mode); + for head in self.wlr_output_heads.lock().values() { + head.handle_mode_change(s.mode); + } } if let Some(output) = state.outputs.get(&self.connector.id()) && let Some(node) = &output.node @@ -1009,6 +1019,7 @@ impl State { for output in self.root.outputs.lock().values() { output.clear(); } + self.wlr_output_managers.clear(); self.dbus.clear(); self.pending_container_layout.clear(); self.pending_container_render_positions.clear(); diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index 3853a195..aed36666 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -15,7 +15,8 @@ use { state::{ConnectorData, OutputData, State}, tree::{OutputNode, WsMoveConfig, move_ws_to_output}, utils::{ - asyncevent::AsyncEvent, clonecell::CloneCell, hash_map_ext::HashMapExt, rc_eq::RcEq, + asyncevent::AsyncEvent, clonecell::CloneCell, debug_fn::debug_fn, + hash_map_ext::HashMapExt, rc_eq::RcEq, }, }, jay_config::video::Transform, @@ -76,6 +77,7 @@ pub fn handle(state: &Rc, connector: &Rc) { handler: Default::default(), connected: Cell::new(false), name, + description: Default::default(), drm_dev: drm_dev.clone(), async_event: Rc::new(AsyncEvent::default()), damaged: Cell::new(false), @@ -84,6 +86,7 @@ pub fn handle(state: &Rc, connector: &Rc) { damage_intersect: Default::default(), state: Cell::new(backend_state), head_managers: HeadManagers::new(state.head_names.next(), head_state), + wlr_output_heads: Default::default(), }); if let Some(dev) = drm_dev { dev.connectors.set(id, data.clone()); @@ -143,6 +146,7 @@ impl ConnectorHandler { log::info!("Connector {} connected", self.data.connector.kernel_id()); self.data.connected.set(true); self.data.set_state(&self.state, info.state); + *self.data.description.borrow_mut() = create_description(&info); let name = self.state.globals.name(); if info.non_desktop_effective { self.handle_non_desktop_connected(info).await; @@ -151,6 +155,9 @@ impl ConnectorHandler { } self.data.connected.set(false); self.data.head_managers.handle_output_disconnected(); + for head in self.data.wlr_output_heads.lock().drain_values() { + head.handle_disconnected(); + } log::info!("Connector {} disconnected", self.data.connector.kernel_id()); } @@ -316,6 +323,7 @@ impl ConnectorHandler { self.data .head_managers .handle_output_connected(&output_data); + self.state.wlr_output_managers.announce_head(&output_data); 'outer: loop { while let Some(event) = self.data.connector.event() { match event { @@ -433,6 +441,7 @@ impl ConnectorHandler { self.data .head_managers .handle_output_connected(&output_data); + self.state.wlr_output_managers.announce_head(&output_data); 'outer: loop { while let Some(event) = self.data.connector.event() { match event { @@ -451,3 +460,22 @@ impl ConnectorHandler { } } } + +fn create_description(info: &MonitorInfo) -> String { + debug_fn(|f| { + let mut needs_space = false; + let id = &info.output_id; + for s in [&id.manufacturer, &id.model, &id.serial_number] { + if s.is_empty() { + continue; + } + if needs_space { + f.write_str(" ")?; + } + needs_space = true; + f.write_str(s)?; + } + Ok(()) + }) + .to_string() +} diff --git a/src/tree/output.rs b/src/tree/output.rs index 73b67996..7aaba7a4 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -483,6 +483,9 @@ impl OutputNode { .connector .head_managers .handle_scale_change(scale); + for head in self.global.connector.wlr_output_heads.lock().values() { + head.handle_new_scale(scale); + } } pub fn schedule_update_render_data(self: &Rc) { @@ -778,6 +781,9 @@ impl OutputNode { } let rect = pos.at_point(x, y); self.change_extents_(&rect); + for head in self.global.connector.wlr_output_heads.lock().values() { + head.handle_position_change(x, y); + } } pub fn update_mode(self: &Rc, mode: Mode) { @@ -817,6 +823,9 @@ impl OutputNode { .connector .head_managers .handle_transform_change(transform); + for head in self.global.connector.wlr_output_heads.lock().values() { + head.hande_transform_change(transform); + } } } @@ -1339,6 +1348,9 @@ impl OutputNode { .connector .head_managers .handle_vrr_mode_change(mode.to_config()); + for head in self.global.connector.wlr_output_heads.lock().values() { + head.handle_vrr_mode_change(mode); + } } } diff --git a/wire/zwlr_output_configuration_head_v1.txt b/wire/zwlr_output_configuration_head_v1.txt new file mode 100644 index 00000000..68274260 --- /dev/null +++ b/wire/zwlr_output_configuration_head_v1.txt @@ -0,0 +1,28 @@ +# requests + +request set_mode { + mode: id(zwlr_output_mode_v1), +} + +request set_custom_mode { + width: i32, + height: i32, + refresh: i32, +} + +request set_position { + x: i32, + y: i32, +} + +request set_transform { + transform: i32, +} + +request set_scale { + scale: fixed, +} + +request set_adaptive_sync { + state: u32, +} diff --git a/wire/zwlr_output_configuration_v1.txt b/wire/zwlr_output_configuration_v1.txt new file mode 100644 index 00000000..32ebff7e --- /dev/null +++ b/wire/zwlr_output_configuration_v1.txt @@ -0,0 +1,36 @@ +# requests + +request enable_head { + id: id(zwlr_output_configuration_head_v1), + head: id(zwlr_output_head_v1), +} + +request disable_head { + head: id(zwlr_output_head_v1), +} + +request apply { + +} + +request test { + +} + +request destroy { + +} + +# events + +event succeeded { + +} + +event failed { + +} + +event cancelled { + +} diff --git a/wire/zwlr_output_head_v1.txt b/wire/zwlr_output_head_v1.txt new file mode 100644 index 00000000..967f9276 --- /dev/null +++ b/wire/zwlr_output_head_v1.txt @@ -0,0 +1,65 @@ +# requests + +request release (since = 3) { + +} + +# events + +event name { + name: str, +} + +event description { + description: str, +} + +event physical_size { + width: i32, + height: i32 +} + +event mode { + mode: id(zwlr_output_mode_v1), +} + +event enabled { + enabled: i32, +} + +event current_mode { + mode: id(zwlr_output_mode_v1), +} + +event position { + x: i32, + y: i32, +} + +event transform { + transform: i32, +} + +event scale { + scale: fixed, +} + +event finished { + +} + +event make (since = 2) { + make: str +} + +event model (since = 2) { + model: str +} + +event serial_number (since = 2) { + serial_number: str +} + +event adaptive_sync (since = 4) { + state: u32, +} diff --git a/wire/zwlr_output_manager_v1.txt b/wire/zwlr_output_manager_v1.txt new file mode 100644 index 00000000..47f977a2 --- /dev/null +++ b/wire/zwlr_output_manager_v1.txt @@ -0,0 +1,24 @@ +# requests + +request create_configuration { + id: id(zwlr_output_configuration_v1), + serial: u32, +} + +request stop { + +} + +# events + +event head { + head: id(zwlr_output_head_v1) +} + +event done { + serial: u32 +} + +event finished { + +} diff --git a/wire/zwlr_output_mode_v1.txt b/wire/zwlr_output_mode_v1.txt new file mode 100644 index 00000000..96952948 --- /dev/null +++ b/wire/zwlr_output_mode_v1.txt @@ -0,0 +1,24 @@ +# requests + +request release (since = 3) { + +} + +# events + +event size { + width: i32, + height: i32, +} + +event refresh { + refresh: i32, +} + +event preferred { + +} + +event finished { + +}