From 1a9753847acd679a4249948076ac6ef88b5396a6 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 17 Mar 2026 19:29:11 +0100 Subject: [PATCH 1/7] backend: support outputs with arbitrary modes --- jay-config/src/_private/client.rs | 13 ++++ jay-config/src/_private/ipc.rs | 6 ++ jay-config/src/video.rs | 8 ++ src/backend.rs | 2 +- src/backends/metal/video.rs | 2 +- src/backends/x.rs | 2 +- src/cli/randr.rs | 29 +++++++- src/compositor.rs | 2 +- src/config/handler.rs | 15 ++++ src/control_center/cc_outputs.rs | 24 +++++- .../jay_head_ext_mode_setter_v1.rs | 9 ++- .../jay_head_ext_physical_display_info_v1.rs | 2 +- .../jay_head_manager_session_v1.rs | 8 +- src/ifs/jay_compositor.rs | 2 +- src/ifs/jay_randr.rs | 12 ++- src/ifs/wl_output.rs | 4 +- .../wlr_output_manager/zwlr_output_head_v1.rs | 18 +++-- .../zwlr_output_manager_v1.rs | 74 ++++++++++++------- src/it/test_backend.rs | 2 +- src/it/tests/t0034_workspace_restoration.rs | 2 +- src/tools/tool_client.rs | 2 +- toml-config/src/lib.rs | 17 ++++- wire/jay_randr.txt | 3 + 23 files changed, 199 insertions(+), 59 deletions(-) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index 9c3be1e6..d74042c5 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1191,6 +1191,19 @@ impl ConfigClient { modes.into_iter().map(WireMode::to_mode).collect() } + pub fn connector_supports_arbitrary_modes(&self, connector: Connector) -> bool { + let res = + self.send_with_response(&ClientMessage::ConnectorSupportsArbitraryModes { connector }); + get_response!( + res, + false, + ConnectorSupportsArbitraryModes { + supports_arbitrary_modes + } + ); + supports_arbitrary_modes + } + pub fn connector_size(&self, connector: Connector) -> (i32, i32) { let res = self.send_with_response(&ClientMessage::ConnectorSize { connector }); get_response!(res, (0, 0), ConnectorSize { width, height }); diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index 53e3662d..b3df04f6 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -846,6 +846,9 @@ pub enum ClientMessage<'a> { monospace: Option>, }, OpenControlCenter, + ConnectorSupportsArbitraryModes { + connector: Connector, + }, } #[derive(Serialize, Deserialize, Debug)] @@ -1096,6 +1099,9 @@ pub enum Response { KeymapFromNames { keymap: Keymap, }, + ConnectorSupportsArbitraryModes { + supports_arbitrary_modes: bool, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/video.rs b/jay-config/src/video.rs index dd5b6829..c2ada2f6 100644 --- a/jay-config/src/video.rs +++ b/jay-config/src/video.rs @@ -157,6 +157,14 @@ impl Connector { get!(Vec::new()).connector_modes(self) } + /// Returns whether this connector supports arbitrary modes. + pub fn supports_arbitrary_modes(self) -> bool { + if !self.exists() { + return false; + } + get!(false).connector_supports_arbitrary_modes(self) + } + /// Returns the logical width of the connector. /// /// The returned value will be different from `mode().width()` if the scale is not 1. diff --git a/src/backend.rs b/src/backend.rs index 746fc7b4..12361b7c 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -100,7 +100,7 @@ impl Display for Mode { #[derive(Clone, Debug)] pub struct MonitorInfo { - pub modes: Vec, + pub modes: Option>, pub output_id: Rc, pub width_mm: i32, pub height_mm: i32, diff --git a/src/backends/metal/video.rs b/src/backends/metal/video.rs index b61a676c..51cba21c 100644 --- a/src/backends/metal/video.rs +++ b/src/backends/metal/video.rs @@ -1959,7 +1959,7 @@ impl MetalBackend { let mut state = dd.persistent.state.borrow().clone(); state.serial = self.state.backend_connector_state_serials.next(); connector.send_event(ConnectorEvent::Connected(MonitorInfo { - modes, + modes: Some(modes), output_id: dd.output_id.clone(), width_mm: dd.mm_width as _, height_mm: dd.mm_height as _, diff --git a/src/backends/x.rs b/src/backends/x.rs index f338c446..199f16e7 100644 --- a/src/backends/x.rs +++ b/src/backends/x.rs @@ -590,7 +590,7 @@ impl XBackend { .backend_events .push(BackendEvent::NewConnector(output.clone())); output.events.push(ConnectorEvent::Connected(MonitorInfo { - modes: vec![], + modes: Some(vec![]), output_id: Rc::new(OutputId::new( String::new(), "X.Org Foundation".to_string(), diff --git a/src/cli/randr.rs b/src/cli/randr.rs index d6041d39..d9b14112 100644 --- a/src/cli/randr.rs +++ b/src/cli/randr.rs @@ -527,6 +527,7 @@ struct Output { pub blend_space: Option, pub native_gamut: Option, pub use_native_gamut: bool, + pub arbitrary_modes: bool, } #[derive(Copy, Clone, Debug)] @@ -641,9 +642,22 @@ impl Randr { log::error!("Connector {} is not connected", connector.name); return; }; - let Some(mode) = output.modes.iter().find(|m| { - m.width == t.width && m.height == t.height && m.refresh_rate() == t.refresh_rate - }) else { + let mode = 'mode: { + if let Some(mode) = output.modes.iter().find(|m| { + m.width == t.width + && m.height == t.height + && m.refresh_rate() == t.refresh_rate + }) { + break 'mode *mode; + } + if output.arbitrary_modes { + break 'mode Mode { + width: t.width, + height: t.height, + refresh_rate_millihz: (t.refresh_rate * 1_000.0).round() as u32, + current: false, + }; + } log::error!( "Output {} does not support this refresh rate", connector.name @@ -1082,6 +1096,9 @@ impl Randr { p.b.0.0, p.b.1.0, p.wp.0.0, p.wp.1.0 ); } + if o.arbitrary_modes { + println!(" supports arbitrary modes"); + } if o.modes.is_not_empty() && modes { println!(" modes:"); for mode in &o.modes { @@ -1280,6 +1297,12 @@ impl Randr { let output = c.output.as_mut().unwrap(); output.use_native_gamut = true; }); + jay_randr::ArbitraryModes::handle(tc, randr, data.clone(), |data, _| { + let mut data = data.borrow_mut(); + let c = data.connectors.last_mut().unwrap(); + let output = c.output.as_mut().unwrap(); + output.arbitrary_modes = true; + }); tc.round_trip().await; data.borrow_mut().clone() } diff --git a/src/compositor.rs b/src/compositor.rs index 2e10d5bb..938bbe03 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -754,7 +754,7 @@ fn create_dummy_output(state: &Rc) { state.globals.name(), state, &connector_data, - Vec::new(), + Some(Vec::new()), 0, 0, &output_id, diff --git a/src/config/handler.rs b/src/config/handler.rs index 6da5c2d5..9bbc7505 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1205,6 +1205,7 @@ impl ConfigProxyHandler { .global .modes .iter() + .flatten() .map(|m| WireMode { width: m.width, height: m.height, @@ -1215,6 +1216,17 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_connector_supports_arbitrary_modes( + &self, + connector: Connector, + ) -> Result<(), CphError> { + let connector = self.get_output_node(connector)?; + self.respond(Response::ConnectorSupportsArbitraryModes { + supports_arbitrary_modes: connector.global.modes.is_none(), + }); + Ok(()) + } + fn handle_connector_name(&self, connector: Connector) -> Result<(), CphError> { let connector = self.get_connector(connector)?; self.respond(Response::GetConnectorName { @@ -3328,6 +3340,9 @@ impl ConfigProxyHandler { monospace, } => self.handle_set_egui_fonts(proportional, monospace), ClientMessage::OpenControlCenter => self.handle_open_control_center(), + ClientMessage::ConnectorSupportsArbitraryModes { connector } => self + .handle_connector_supports_arbitrary_modes(connector) + .wrn("connector_supports_arbitrary_modes")?, } Ok(()) } diff --git a/src/control_center/cc_outputs.rs b/src/control_center/cc_outputs.rs index fc3159be..c0f2697b 100644 --- a/src/control_center/cc_outputs.rs +++ b/src/control_center/cc_outputs.rs @@ -27,7 +27,7 @@ use { egui::{ Align, Button, Checkbox, Color32, ComboBox, DragValue, EventFilter, FontId, Frame, Grid, Id, Key, Layout, PointerButton, Rect, ScrollArea, Sense, Shadow, Stroke, StrokeKind, Style, - TextFormat, Ui, UiBuilder, Vec2, Widget, WidgetText, pos2, text::LayoutJob, vec2, + TextFormat, Ui, UiBuilder, Vec2, Widget, WidgetText, emath, pos2, text::LayoutJob, vec2, }, egui_tiles::{ Behavior, Container, Linear, LinearDir, ResizeState, SimplificationOptions, Tile, TileId, @@ -1087,15 +1087,33 @@ fn show_mode(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { ) }; if let Some(monitor_info) = &m.monitor_info - && monitor_info.modes.len() > 1 + && let Some(modes) = &monitor_info.modes + && modes.len() > 1 { ComboBox::from_id_salt("modes") .selected_text(mode_text(mode)) .show_ui(ui, |ui| { - for v in &monitor_info.modes { + for v in modes { ui.selectable_value(&mut mode, *v, mode_text(*v)); } }); + } else if let Some(monitor_info) = &m.monitor_info + && monitor_info.modes.is_none() + { + ui.horizontal(|ui| { + fn value(ui: &mut Ui, v: &mut T, min: T, max: T) -> bool { + let res = DragValue::new(v).range(min..=max).speed(1.0).ui(ui); + res.changed() + } + value(ui, &mut mode.width, 1, u16::MAX as i32); + ui.label("x"); + value(ui, &mut mode.height, 1, u16::MAX as i32); + ui.label("@"); + let mut hz = mode.refresh_rate_millihz as f64 / 1_000.0; + if value(ui, &mut hz, 0.0, 1_000_000.0) { + mode.refresh_rate_millihz = (hz * 1_000.0).round() as u32; + } + }); } else { ui.label(mode_text(mode)); } diff --git a/src/ifs/head_management/jay_head_ext/jay_head_ext_mode_setter_v1.rs b/src/ifs/head_management/jay_head_ext/jay_head_ext_mode_setter_v1.rs index 7fb64c2f..e7c345ae 100644 --- a/src/ifs/head_management/jay_head_ext/jay_head_ext_mode_setter_v1.rs +++ b/src/ifs/head_management/jay_head_ext/jay_head_ext_mode_setter_v1.rs @@ -41,8 +41,10 @@ impl HeadName { pub(in super::super) fn send_modes(&self, state: &HeadState) { self.client.event(Reset { self_id: self.id }); - if let Some(mi) = &state.monitor_info { - for mode in &mi.modes { + if let Some(mi) = &state.monitor_info + && let Some(modes) = &mi.modes + { + for mode in modes { self.client.event(Mode { self_id: self.id, width: mode.width, @@ -73,7 +75,8 @@ impl JayHeadExtModeSetterV1RequestHandler for HeadName { .borrow() .monitor_info .as_deref() - .map(|i| i.modes.len()) + .and_then(|i| i.modes.as_ref()) + .map(|m| m.len()) .unwrap_or(0); let idx = req.idx as usize; if idx >= num_modes { diff --git a/src/ifs/head_management/jay_head_ext/jay_head_ext_physical_display_info_v1.rs b/src/ifs/head_management/jay_head_ext/jay_head_ext_physical_display_info_v1.rs index 0c1fff34..dbd00b80 100644 --- a/src/ifs/head_management/jay_head_ext/jay_head_ext_physical_display_info_v1.rs +++ b/src/ifs/head_management/jay_head_ext/jay_head_ext_physical_display_info_v1.rs @@ -43,7 +43,7 @@ impl HeadName { pub(in super::super) fn send_info(&self, state: &HeadState) { self.send_reset(); if let Some(mi) = &state.monitor_info { - for mode in &mi.modes { + for mode in mi.modes.iter().flatten() { self.send_mode(mode); } self.send_manufacturer(&mi.output_id.manufacturer); 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 309405e5..15dbe174 100644 --- a/src/ifs/head_management/jay_head_manager_session_v1.rs +++ b/src/ifs/head_management/jay_head_manager_session_v1.rs @@ -422,7 +422,13 @@ impl JayHeadManagerSessionV1RequestHandler for JayHeadManagerSessionV1 { to_send |= COMPOSITOR_SPACE_INFO_SIZE; } HeadOp::SetMode(i) => { - state.mode = snapshot.monitor_info.as_deref().unwrap().modes[i]; + state.mode = snapshot + .monitor_info + .as_deref() + .unwrap() + .modes + .as_ref() + .unwrap()[i]; state.update_size(); to_send |= MODE_INFO; to_send |= COMPOSITOR_SPACE_INFO_SIZE; diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index e37c11a3..3e4b9ca3 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -78,7 +78,7 @@ global_base!(JayCompositorGlobal, JayCompositor, JayCompositorError); impl Global for JayCompositorGlobal { fn version(&self) -> u32 { - 28 + 29 } fn required_caps(&self) -> ClientCaps { diff --git a/src/ifs/jay_randr.rs b/src/ifs/jay_randr.rs index 156c7432..c04bee1a 100644 --- a/src/ifs/jay_randr.rs +++ b/src/ifs/jay_randr.rs @@ -16,7 +16,7 @@ use { }, jay_config::video::{TearingMode as ConfigTearingMode, VrrMode as ConfigVrrMode}, linearize::LinearizeExt, - std::rc::Rc, + std::{rc::Rc, slice}, thiserror::Error, }; @@ -36,6 +36,7 @@ const COLORIMETRY_SINCE: Version = Version(15); const BRIGHTNESS_SINCE: Version = Version(16); const BLEND_SPACE_SINCE: Version = Version(21); const NATIVE_GAMUT_SINCE: Version = Version(23); +const ARBITRARY_MODES_SINCE: Version = Version(29); impl JayRandr { pub fn new(id: JayRandrId, client: &Rc, version: Version) -> Self { @@ -162,7 +163,11 @@ impl JayRandr { } } let current_mode = global.mode.get(); - for mode in &global.modes { + for mode in global + .modes + .as_deref() + .unwrap_or(slice::from_ref(¤t_mode)) + { self.client.event(Mode { self_id: self.id, width: mode.width, @@ -232,6 +237,9 @@ impl JayRandr { self.client.event(UseNativeGamut { self_id: self.id }); } } + if self.version >= ARBITRARY_MODES_SINCE && global.modes.is_none() { + self.client.event(ArbitraryModes { self_id: self.id }); + } } fn send_error(&self, msg: &str) { diff --git a/src/ifs/wl_output.rs b/src/ifs/wl_output.rs index f90f5645..5ff56fa8 100644 --- a/src/ifs/wl_output.rs +++ b/src/ifs/wl_output.rs @@ -71,7 +71,7 @@ pub struct WlOutputGlobal { pub output_id: Rc, pub mode: Cell, pub refresh_nsec: Cell, - pub modes: Vec, + pub modes: Option>, pub formats: CloneCell>>, pub format: Cell<&'static Format>, pub width_mm: i32, @@ -199,7 +199,7 @@ impl WlOutputGlobal { name: GlobalName, state: &Rc, connector: &Rc, - modes: Vec, + modes: Option>, width_mm: i32, height_mm: i32, output_id: &Rc, diff --git a/src/ifs/wlr_output_manager/zwlr_output_head_v1.rs b/src/ifs/wlr_output_manager/zwlr_output_head_v1.rs index 9debdccf..5e51a558 100644 --- a/src/ifs/wlr_output_manager/zwlr_output_head_v1.rs +++ b/src/ifs/wlr_output_manager/zwlr_output_head_v1.rs @@ -12,9 +12,9 @@ use { scale, state::OutputData, tree::{self, VrrMode}, + utils::copyhashmap::CopyHashMap, wire::{ZwlrOutputHeadV1Id, zwlr_output_head_v1::*}, }, - ahash::AHashMap, std::rc::Rc, thiserror::Error, }; @@ -44,7 +44,7 @@ pub struct ZwlrOutputHeadV1 { pub(super) manager: Rc, pub(super) head_id: WlrOutputHeadId, pub(super) connector_id: ConnectorId, - pub(super) modes: AHashMap>, + pub(super) modes: CopyHashMap>, } impl ZwlrOutputHeadV1 { @@ -177,13 +177,21 @@ impl ZwlrOutputHeadV1 { } pub fn handle_mode_change(&self, new: backend::Mode) { - let Some(mode) = self.modes.get(&new) else { + let Some(mode) = self.modes.get(&new).or_else(|| { + self.manager + .create_mode(self.head_id, &new, false, false) + .inspect(|mode| { + self.modes.set(new, mode.clone()); + self.send_mode(mode); + mode.send(); + }) + }) else { return; }; if mode.destroyed.get() { return; } - self.send_current_mode(mode); + self.send_current_mode(&mode); self.manager.schedule_done(); } @@ -207,7 +215,7 @@ impl ZwlrOutputHeadV1 { pub fn handle_disconnected(&self) { self.send_finished(); - for mode in self.modes.values() { + for mode in self.modes.lock().values() { if !mode.destroyed.get() { mode.send_finished(); } diff --git a/src/ifs/wlr_output_manager/zwlr_output_manager_v1.rs b/src/ifs/wlr_output_manager/zwlr_output_manager_v1.rs index 972bc6b6..a4c116a6 100644 --- a/src/ifs/wlr_output_manager/zwlr_output_manager_v1.rs +++ b/src/ifs/wlr_output_manager/zwlr_output_manager_v1.rs @@ -1,23 +1,24 @@ use { crate::{ + backend::Mode, 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, + ADAPTIVE_SYNC_SINCE, MAKE_SINCE, MODEL_SINCE, SERIAL_NUMBER_SINCE, WlrOutputHeadId, + ZwlrOutputHeadV1, }, zwlr_output_mode_v1::ZwlrOutputModeV1, }, leaks::Tracker, object::{Object, Version}, state::OutputData, - utils::numcell::NumCell, + utils::{copyhashmap::CopyHashMap, numcell::NumCell}, wire::{ZwlrOutputManagerV1Id, zwlr_output_manager_v1::*}, }, - ahash::AHashMap, isnt::std_1::string::IsntStringExt, - std::{cell::Cell, rc::Rc}, + std::{cell::Cell, rc::Rc, slice}, thiserror::Error, }; @@ -134,38 +135,27 @@ impl ZwlrOutputManagerV1 { let state_mode = output.connector.state.borrow().mode; let head_id = self.client.state.wlr_output_managers.head_ids.next(); let mut modes_list = vec![]; - let mut modes = AHashMap::new(); + let modes = CopyHashMap::new(); let mut have_current = false; - for (idx, mode) in mi.modes.iter().enumerate() { - if modes.contains_key(mode) { + for (idx, mode) in mi + .modes + .as_deref() + .unwrap_or(slice::from_ref(&state_mode)) + .iter() + .enumerate() + { + if modes.contains(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 Some(output_mode) = self.create_mode(head_id, mode, idx == 0, current) else { + 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); + modes.set(*mode, output_mode); } let head = Rc::new(ZwlrOutputHeadV1 { id, @@ -244,6 +234,36 @@ impl ZwlrOutputManagerV1 { .queue .push(self.clone()); } + + pub(super) fn create_mode( + self: &Rc, + head_id: WlrOutputHeadId, + mode: &Mode, + preferred: bool, + initial_current: bool, + ) -> Option> { + let id = match self.client.new_id() { + Ok(id) => id, + Err(e) => { + self.client.error(e); + return None; + } + }; + let output_mode = Rc::new(ZwlrOutputModeV1 { + id, + head_id, + client: self.client.clone(), + tracker: Default::default(), + version: self.version, + mode: *mode, + preferred, + initial_current, + destroyed: Cell::new(false), + }); + track!(self.client, output_mode); + self.client.add_server_obj(&output_mode); + Some(output_mode) + } } global_base!( diff --git a/src/it/test_backend.rs b/src/it/test_backend.rs index 42748ae0..54a2409b 100644 --- a/src/it/test_backend.rs +++ b/src/it/test_backend.rs @@ -126,7 +126,7 @@ impl TestBackend { refresh_rate_millihz: 60_000, }; let default_monitor_info = MonitorInfo { - modes: vec![mode], + modes: Some(vec![mode]), output_id: Rc::new(OutputId { connector: None, manufacturer: "jay".to_string(), diff --git a/src/it/tests/t0034_workspace_restoration.rs b/src/it/tests/t0034_workspace_restoration.rs index 4203d1ee..5a9ca422 100644 --- a/src/it/tests/t0034_workspace_restoration.rs +++ b/src/it/tests/t0034_workspace_restoration.rs @@ -39,7 +39,7 @@ async fn test(run: Rc) -> TestResult { damage_calls: NumCell::new(0), }); let new_monitor_info = MonitorInfo { - modes: vec![], + modes: Some(vec![]), output_id: Rc::new(OutputId { connector: None, manufacturer: "jay".to_string(), diff --git a/src/tools/tool_client.rs b/src/tools/tool_client.rs index dc680d88..aba85d42 100644 --- a/src/tools/tool_client.rs +++ b/src/tools/tool_client.rs @@ -334,7 +334,7 @@ impl ToolClient { self_id: s.registry, name: s.jay_compositor.0, interface: JayCompositor.name(), - version: s.jay_compositor.1.min(28), + version: s.jay_compositor.1.min(29), id: id.into(), }); self.jay_compositor.set(Some(id)); diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 04222433..cbe55830 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -830,11 +830,20 @@ impl Output { Some(rr) => m.refresh_rate() as f64 / 1000.0 == rr, } }); - match m { - None => { + 'set_mode: { + let (w, h, mhz) = 'mode: { + if let Some(m) = m { + break 'mode (m.width(), m.height(), m.refresh_rate()); + } + if c.supports_arbitrary_modes() + && let Some(refresh) = mode.refresh_rate + { + break 'mode (mode.width, mode.height, (refresh * 1_000.0).round() as u32); + } log::warn!("Output {} does not support mode {mode}", c.name()); - } - Some(m) => c.set_mode(m.width(), m.height(), Some(m.refresh_rate())), + break 'set_mode; + }; + c.set_mode(w, h, Some(mhz)); } } if let Some(vrr) = &self.vrr { diff --git a/wire/jay_randr.txt b/wire/jay_randr.txt index 0305d734..6f4f90b2 100644 --- a/wire/jay_randr.txt +++ b/wire/jay_randr.txt @@ -229,3 +229,6 @@ event native_gamut (since = 23) { event use_native_gamut (since = 23) { } + +event arbitrary_modes (since = 29) { +} From d321e888be5e936f1e1dfc1dab655d625f5f8a98 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 17 Mar 2026 20:41:00 +0100 Subject: [PATCH 2/7] backend: take initial backend state from backend --- src/backend.rs | 1 + src/backends/dummy.rs | 25 +++++++++- src/backends/metal/video.rs | 4 ++ src/backends/x.rs | 4 ++ src/compositor.rs | 21 +------- src/it/test_backend.rs | 54 +++++++++++++-------- src/it/tests/t0034_workspace_restoration.rs | 30 ++++++------ src/tasks/connector.rs | 20 +------- 8 files changed, 88 insertions(+), 71 deletions(-) diff --git a/src/backend.rs b/src/backend.rs index 12361b7c..8e0a2bc0 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -141,6 +141,7 @@ pub trait Connector: Any { fn damage(&self); fn drm_dev(&self) -> Option; fn effectively_locked(&self) -> bool; + fn state(&self) -> BackendConnectorState; fn caps(&self) -> ConnectorCaps { ConnectorCaps::none() } diff --git a/src/backends/dummy.rs b/src/backends/dummy.rs index 5242bdc1..1ac3be15 100644 --- a/src/backends/dummy.rs +++ b/src/backends/dummy.rs @@ -2,8 +2,10 @@ use { crate::{ async_engine::SpawnedFuture, backend::{ - Backend, Connector, ConnectorEvent, ConnectorId, ConnectorKernelId, DrmDeviceId, + self, Backend, BackendConnectorState, BackendConnectorStateSerial, Connector, + ConnectorEvent, ConnectorId, ConnectorKernelId, DrmDeviceId, }, + format::XRGB8888, video::drm::ConnectorType, }, std::{error::Error, rc::Rc}, @@ -52,4 +54,25 @@ impl Connector for DummyOutput { fn effectively_locked(&self) -> bool { true } + + fn state(&self) -> BackendConnectorState { + let mode = backend::Mode { + width: 0, + height: 0, + refresh_rate_millihz: 40_000, + }; + BackendConnectorState { + serial: BackendConnectorStateSerial::from_raw(0), + enabled: true, + active: false, + mode, + non_desktop_override: None, + vrr: false, + tearing: false, + format: XRGB8888, + color_space: Default::default(), + eotf: Default::default(), + gamma_lut: Default::default(), + } + } } diff --git a/src/backends/metal/video.rs b/src/backends/metal/video.rs index 51cba21c..ecbba0c3 100644 --- a/src/backends/metal/video.rs +++ b/src/backends/metal/video.rs @@ -867,6 +867,10 @@ impl Connector for MetalConnector { fb.locked } + fn state(&self) -> BackendConnectorState { + self.display.borrow().persistent.state.borrow().clone() + } + fn caps(&self) -> ConnectorCaps { CONCAP_CONNECTOR | CONCAP_MODE_SETTING | CONCAP_PHYSICAL_DISPLAY } diff --git a/src/backends/x.rs b/src/backends/x.rs index 199f16e7..72e1dde5 100644 --- a/src/backends/x.rs +++ b/src/backends/x.rs @@ -1113,6 +1113,10 @@ impl Connector for XOutput { true } + fn state(&self) -> BackendConnectorState { + self.state.borrow().clone() + } + fn transaction_type(&self) -> Box { Box::new(XTransactionType) } diff --git a/src/compositor.rs b/src/compositor.rs index 938bbe03..95e8463a 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -4,7 +4,7 @@ use { crate::{ acceptor::{Acceptor, AcceptorError}, async_engine::{AsyncEngine, Phase, SpawnedFuture}, - backend::{self, Backend, BackendConnectorState, BackendConnectorStateSerial, Connector}, + backend::{Backend, Connector}, backends::{ dummy::{DummyBackend, DummyOutput}, metal, x, @@ -675,26 +675,9 @@ fn create_dummy_output(state: &Rc) { serial_number: "".to_string(), }); let persistent_state = Rc::new(PersistentOutputState::default()); - let mode = backend::Mode { - width: 0, - height: 0, - refresh_rate_millihz: 40_000, - }; - let backend_state = BackendConnectorState { - serial: BackendConnectorStateSerial::from_raw(0), - enabled: true, - active: false, - mode, - non_desktop_override: None, - vrr: false, - tearing: false, - format: XRGB8888, - color_space: Default::default(), - eotf: Default::default(), - gamma_lut: Default::default(), - }; let id = state.connector_ids.next(); let connector = Rc::new(DummyOutput { id }) as Rc; + let backend_state = connector.state(); let name = Rc::new("Dummy".to_string()); let head_name = state.head_names.next(); let head_state = HeadState { diff --git a/src/it/test_backend.rs b/src/it/test_backend.rs index 54a2409b..0099d7fd 100644 --- a/src/it/test_backend.rs +++ b/src/it/test_backend.rs @@ -37,7 +37,15 @@ use { }, ahash::AHashMap, bstr::ByteSlice, - std::{any::Any, cell::Cell, error::Error, io, os::unix::ffi::OsStrExt, pin::Pin, rc::Rc}, + std::{ + any::Any, + cell::{Cell, RefCell}, + error::Error, + io, + os::unix::ffi::OsStrExt, + pin::Pin, + rc::Rc, + }, thiserror::Error, uapi::c, }; @@ -75,6 +83,24 @@ pub struct TestBackend { impl TestBackend { pub fn new(state: &Rc, future: TestFuture) -> Self { state.set_backend_idle(false); + let mode = Mode { + width: 800, + height: 600, + refresh_rate_millihz: 60_000, + }; + let bcs = BackendConnectorState { + serial: state.backend_connector_state_serials.next(), + enabled: true, + active: true, + mode, + non_desktop_override: None, + vrr: false, + tearing: false, + format: XRGB8888, + color_space: Default::default(), + eotf: Default::default(), + gamma_lut: Default::default(), + }; let default_connector = Rc::new(TestConnector { id: state.connector_ids.next(), kernel_id: ConnectorKernelId { @@ -85,6 +111,7 @@ impl TestBackend { feedback: Default::default(), idle: Default::default(), damage_calls: NumCell::new(0), + state: RefCell::new(bcs.clone()), }); let default_mouse = Rc::new(TestBackendMouse { common: TestInputDeviceCommon { @@ -120,11 +147,6 @@ impl TestBackend { state: state.clone(), }, }); - let mode = Mode { - width: 800, - height: 600, - refresh_rate_millihz: 60_000, - }; let default_monitor_info = MonitorInfo { modes: Some(vec![mode]), output_id: Rc::new(OutputId { @@ -142,19 +164,7 @@ impl TestBackend { color_spaces: vec![], primaries: Primaries::SRGB, luminance: None, - state: BackendConnectorState { - serial: state.backend_connector_state_serials.next(), - enabled: true, - active: true, - mode, - non_desktop_override: None, - vrr: false, - tearing: false, - format: XRGB8888, - color_space: Default::default(), - eotf: Default::default(), - gamma_lut: Default::default(), - }, + state: bcs, }; Self { state: state.clone(), @@ -325,6 +335,7 @@ pub struct TestConnector { pub feedback: CloneCell>>, pub idle: TEEH, pub damage_calls: NumCell, + pub state: RefCell, } impl Connector for TestConnector { @@ -357,6 +368,10 @@ impl Connector for TestConnector { true } + fn state(&self) -> BackendConnectorState { + self.state.borrow().clone() + } + fn drm_feedback(&self) -> Option> { self.feedback.get() } @@ -404,6 +419,7 @@ impl BackendPreparedConnectorTransaction for TestBackendTransaction { self: Box, ) -> Result, BackendConnectorTransactionError> { for (c, s) in self.connectors.values() { + *c.state.borrow_mut() = s.clone(); c.idle.push(!s.active); } Ok(self) diff --git a/src/it/tests/t0034_workspace_restoration.rs b/src/it/tests/t0034_workspace_restoration.rs index 5a9ca422..03f1f389 100644 --- a/src/it/tests/t0034_workspace_restoration.rs +++ b/src/it/tests/t0034_workspace_restoration.rs @@ -10,7 +10,7 @@ use { utils::numcell::NumCell, video::drm::ConnectorType, }, - std::rc::Rc, + std::{cell::RefCell, rc::Rc}, }; testcase!(); @@ -27,6 +27,19 @@ async fn test(run: Rc) -> TestResult { bail!("no dummy output"); }; + let bcs = BackendConnectorState { + serial: run.state.backend_connector_state_serials.next(), + enabled: true, + active: true, + mode: Default::default(), + non_desktop_override: None, + vrr: false, + tearing: false, + format: XRGB8888, + color_space: Default::default(), + eotf: Default::default(), + gamma_lut: Default::default(), + }; let new_connector = Rc::new(TestConnector { id: run.state.connector_ids.next(), kernel_id: ConnectorKernelId { @@ -37,6 +50,7 @@ async fn test(run: Rc) -> TestResult { feedback: Default::default(), idle: Default::default(), damage_calls: NumCell::new(0), + state: RefCell::new(bcs.clone()), }); let new_monitor_info = MonitorInfo { modes: Some(vec![]), @@ -55,19 +69,7 @@ async fn test(run: Rc) -> TestResult { color_spaces: vec![], primaries: Primaries::SRGB, luminance: None, - state: BackendConnectorState { - serial: run.state.backend_connector_state_serials.next(), - enabled: true, - active: true, - mode: Default::default(), - non_desktop_override: None, - vrr: false, - tearing: false, - format: XRGB8888, - color_space: Default::default(), - eotf: Default::default(), - gamma_lut: Default::default(), - }, + state: bcs, }; run.backend .state diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index 82bfc4ce..a46a7ed1 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -1,11 +1,7 @@ use { crate::{ - backend::{ - BackendConnectorState, BackendConnectorStateSerial, Connector, ConnectorEvent, - ConnectorId, MonitorInfo, - }, + backend::{Connector, ConnectorEvent, ConnectorId, MonitorInfo}, control_center::CCI_OUTPUTS, - format::XRGB8888, globals::GlobalName, ifs::{ head_management::{HeadManagers, HeadState}, @@ -35,19 +31,7 @@ pub fn handle(state: &Rc, connector: &Rc) { _ => panic!("connector's drm device does not exist"), }; } - let backend_state = BackendConnectorState { - serial: BackendConnectorStateSerial::from_raw(0), - enabled: true, - active: false, - mode: Default::default(), - non_desktop_override: None, - vrr: false, - tearing: false, - format: XRGB8888, - color_space: Default::default(), - eotf: Default::default(), - gamma_lut: None, - }; + let backend_state = connector.state(); let id = connector.id(); let name = Rc::new(connector.kernel_id().to_string()); let head_state = HeadState { From 52b91654cac4c9448c771748d58bd3fb7e6e90c4 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 17 Mar 2026 21:14:51 +0100 Subject: [PATCH 3/7] backend: take connector name from connector --- src/backend.rs | 3 +++ src/control_center/cc_gpus.rs | 4 ++-- src/tasks/connector.rs | 10 +++++++--- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/backend.rs b/src/backend.rs index 8e0a2bc0..44cf6fe6 100644 --- a/src/backend.rs +++ b/src/backend.rs @@ -170,6 +170,9 @@ pub trait Connector: Any { fn gamma_lut_size(&self) -> Option { None } + fn name(&self) -> String { + self.kernel_id().to_string() + } } #[derive(Debug)] diff --git a/src/control_center/cc_gpus.rs b/src/control_center/cc_gpus.rs index 8d68a4ff..38beffe6 100644 --- a/src/control_center/cc_gpus.rs +++ b/src/control_center/cc_gpus.rs @@ -134,11 +134,11 @@ impl GpusPane { .connectors .lock() .values() - .map(|v| v.connector.kernel_id().to_string()) + .map(|v| v.name.clone()) .collect::>(); cs.sort(); for c in cs { - ui.label(c); + ui.label(&**c); } }); }); diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index a46a7ed1..2b6af537 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -33,7 +33,7 @@ pub fn handle(state: &Rc, connector: &Rc) { } let backend_state = connector.state(); let id = connector.id(); - let name = Rc::new(connector.kernel_id().to_string()); + let name = Rc::new(connector.name()); let head_state = HeadState { name: RcEq(name.clone()), position: (0, 0), @@ -137,7 +137,11 @@ impl ConnectorHandler { } async fn handle_connected(&self, info: MonitorInfo) { - log::info!("Connector {} connected", self.data.connector.kernel_id()); + log::info!( + "Connector {} connected ({})", + self.data.name, + self.data.connector.kernel_id(), + ); self.data.connected.set(true); self.data.set_state(&self.state, info.state.clone()); *self.data.description.borrow_mut() = create_description(&info); @@ -153,7 +157,7 @@ impl ConnectorHandler { for head in self.data.wlr_output_heads.lock().drain_values() { head.handle_disconnected(); } - log::info!("Connector {} disconnected", self.data.connector.kernel_id()); + log::info!("Connector {} disconnected", self.data.name); } async fn handle_desktop_connected(&self, info: MonitorInfo, name: GlobalName) { From cf36da407725615479bc7abb4eba5a545666efe1 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Wed, 18 Mar 2026 12:12:33 +0100 Subject: [PATCH 4/7] config: add get_connector_by_name --- jay-config/src/_private/client.rs | 6 ++++++ jay-config/src/_private/ipc.rs | 3 +++ jay-config/src/video.rs | 8 ++++++++ src/config/handler.rs | 14 ++++++++++++++ 4 files changed, 31 insertions(+) diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index d74042c5..7a46f9d1 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -560,6 +560,12 @@ impl ConfigClient { connector } + pub fn get_connector_by_name(&self, name: &str) -> Connector { + let res = self.send_with_response(&ClientMessage::GetConnectorByName { name }); + get_response!(res, Connector(0), GetConnector { connector }); + connector + } + pub fn get_seat_cursor_workspace(&self, seat: Seat) -> Workspace { let res = self.send_with_response(&ClientMessage::GetSeatCursorWorkspace { seat }); get_response!(res, Workspace(0), GetSeatCursorWorkspace { workspace }); diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index b3df04f6..d002f7b7 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -849,6 +849,9 @@ pub enum ClientMessage<'a> { ConnectorSupportsArbitraryModes { connector: Connector, }, + GetConnectorByName { + name: &'a str, + }, } #[derive(Serialize, Deserialize, Debug)] diff --git a/jay-config/src/video.rs b/jay-config/src/video.rs index c2ada2f6..9dcbea0e 100644 --- a/jay-config/src/video.rs +++ b/jay-config/src/video.rs @@ -454,6 +454,14 @@ pub fn get_connector(id: impl ToConnectorId) -> Connector { get!(Connector(0)).get_connector(ty, idx) } +/// Returns the connector with the given name. +/// +/// Unlike [`get_connector`], this function can also be used for connectors whose names +/// don't follow the `-` pattern. +pub fn get_connector_by_name(name: &str) -> Connector { + get!(Connector(0)).get_connector_by_name(name) +} + /// A type that can be converted to a `(ConnectorType, idx)` tuple. pub trait ToConnectorId { fn to_connector_id(&self) -> Result<(ConnectorType, u32), String>; diff --git a/src/config/handler.rs b/src/config/handler.rs index 9bbc7505..73cf2be9 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1592,6 +1592,19 @@ impl ConfigProxyHandler { Ok(()) } + fn handle_get_connector_by_name(&self, name: &str) { + let connector = self + .state + .connectors + .lock() + .values() + .find(|c| *c.name == name) + .map(|c| c.connector.id().raw() as _) + .map(Connector) + .unwrap_or(Connector(0)); + self.respond(Response::GetConnector { connector }); + } + fn handle_get_connector_active_workspace(&self, connector: Connector) -> Result<(), CphError> { let output = self.get_output_node(connector)?; let workspace = output @@ -3343,6 +3356,7 @@ impl ConfigProxyHandler { ClientMessage::ConnectorSupportsArbitraryModes { connector } => self .handle_connector_supports_arbitrary_modes(connector) .wrn("connector_supports_arbitrary_modes")?, + ClientMessage::GetConnectorByName { name } => self.handle_get_connector_by_name(name), } Ok(()) } From 0cb887c4b52336c2f0a827907139bde18982aa31 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Wed, 18 Mar 2026 12:54:03 +0100 Subject: [PATCH 5/7] control-center: handle outputs without unique names --- src/control_center/cc_outputs.rs | 79 +++++++++++++++++--------------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/src/control_center/cc_outputs.rs b/src/control_center/cc_outputs.rs index c0f2697b..d5027777 100644 --- a/src/control_center/cc_outputs.rs +++ b/src/control_center/cc_outputs.rs @@ -25,9 +25,10 @@ use { }, ahash::AHashMap, egui::{ - Align, Button, Checkbox, Color32, ComboBox, DragValue, EventFilter, FontId, Frame, Grid, - Id, Key, Layout, PointerButton, Rect, ScrollArea, Sense, Shadow, Stroke, StrokeKind, Style, - TextFormat, Ui, UiBuilder, Vec2, Widget, WidgetText, emath, pos2, text::LayoutJob, vec2, + Align, Button, Checkbox, CollapsingHeader, Color32, ComboBox, DragValue, EventFilter, + FontId, Frame, Grid, Id, Key, Layout, PointerButton, Rect, ScrollArea, Sense, Shadow, + Stroke, StrokeKind, Style, TextFormat, Ui, UiBuilder, Vec2, Widget, WidgetText, emath, + pos2, text::LayoutJob, vec2, }, egui_tiles::{ Behavior, Container, Linear, LinearDir, ResizeState, SimplificationOptions, Tile, TileId, @@ -68,6 +69,7 @@ enum Pane { struct CompleteHead { id: ConnectorId, name: HeadName, + pretty_name: Rc, live_state: ReadOnlyHeadState, changed_state: Option, z: u64, @@ -123,9 +125,9 @@ pub enum View { #[derive(Error, Debug)] enum HeadTransactionError { #[error("The connector {} has been removed", .0)] - HeadRemoved(ConnectorId), + HeadRemoved(Rc), #[error("The display connected to connector {} has changed", .0)] - MonitorChanged(ConnectorId), + MonitorChanged(Rc), #[error(transparent)] Backend(#[from] BackendConnectorTransactionError), } @@ -811,10 +813,12 @@ impl OutputsPaneInner { continue; }; let Some(connector) = self.state.connectors.get(&head.id) else { - return Err(HeadTransactionError::HeadRemoved(head.id)); + return Err(HeadTransactionError::HeadRemoved(head.pretty_name.clone())); }; if head.live_state.borrow().monitor_info != desired.monitor_info { - return Err(HeadTransactionError::MonitorChanged(head.id)); + return Err(HeadTransactionError::MonitorChanged( + head.pretty_name.clone(), + )); } let old = connector.state.borrow().clone(); let mut new = old.clone(); @@ -894,6 +898,7 @@ impl OutputsPaneInner { self.heads.entry(mgrs.name).or_insert_with(|| CompleteHead { id: connector.id, name: mgrs.name, + pretty_name: connector.name.clone(), live_state: mgrs.state(), changed_state: None, z: 0, @@ -946,36 +951,38 @@ fn show_connector(state: &State, settings: &Settings, head: &mut CompleteHead, u ..Default::default() }, ); - ui.collapsing(layout_job, |ui| { - grid(ui, ("settings", head.name), |ui| { - let mut diff = false; - show_serial_number(ui, m); - diff |= show_enablement(ui, m, t); - diff |= show_position(ui, m, t); - diff |= show_scale(ui, m, t); - diff |= show_mode(ui, m, t); - diff |= show_size(ui, m, t); - diff |= show_transform(ui, m, t); - diff |= show_brightness(ui, m, t); - diff |= show_color_space(ui, m, t); - diff |= show_eotf(ui, m, t); - diff |= show_format(ui, m, t); - diff |= show_tearing(ui, m, t); - diff |= show_vrr(ui, m, t); - diff |= show_non_desktop(ui, m, t); - diff |= show_blend_space(ui, m, t); - diff |= show_use_native_gamut(ui, m, t); - show_native_gamut(ui, m); - diff |= show_cursor_hz(ui, m, t); - show_flip_margin(state, ui, m, t, head.id); - if diff { - let ui = &mut *ui.row(); - ui.label(""); - ui.label(""); - ui.label("^ current"); - } + CollapsingHeader::new(layout_job) + .id_salt(("connector", head.name)) + .show(ui, |ui| { + grid(ui, ("settings", head.name), |ui| { + let mut diff = false; + show_serial_number(ui, m); + diff |= show_enablement(ui, m, t); + diff |= show_position(ui, m, t); + diff |= show_scale(ui, m, t); + diff |= show_mode(ui, m, t); + diff |= show_size(ui, m, t); + diff |= show_transform(ui, m, t); + diff |= show_brightness(ui, m, t); + diff |= show_color_space(ui, m, t); + diff |= show_eotf(ui, m, t); + diff |= show_format(ui, m, t); + diff |= show_tearing(ui, m, t); + diff |= show_vrr(ui, m, t); + diff |= show_non_desktop(ui, m, t); + diff |= show_blend_space(ui, m, t); + diff |= show_use_native_gamut(ui, m, t); + show_native_gamut(ui, m); + diff |= show_cursor_hz(ui, m, t); + show_flip_margin(state, ui, m, t, head.id); + if diff { + let ui = &mut *ui.row(); + ui.label(""); + ui.label(""); + ui.label("^ current"); + } + }); }); - }); } fn show_serial_number(ui: &mut Ui, m: &HeadState) { From 479cb1d79501bf62db65602d1bb66f9e7e4f7845 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Wed, 18 Mar 2026 16:53:10 +0100 Subject: [PATCH 6/7] metal: move direct-scanout preparation to shared code --- src/backends/metal/present.rs | 132 ++------------------------------- src/gfx_api.rs | 135 ++++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 124 deletions(-) diff --git a/src/backends/metal/present.rs b/src/backends/metal/present.rs index 5f1a8940..88024bfb 100644 --- a/src/backends/metal/present.rs +++ b/src/backends/metal/present.rs @@ -9,12 +9,11 @@ use { }, cmm::cmm_description::ColorDescription, gfx_api::{ - AcquireSync, AlphaMode, BufferResv, GfxApiOpt, GfxRenderPass, GfxTexture, ReleaseSync, + AcquireSync, BufferResv, DirectScanoutPosition, GfxRenderPass, GfxTexture, ReleaseSync, SyncFile, create_render_pass, }, ifs::wl_output::BlendSpace, rect::Region, - theme::Color, time::Time, tracy::FrameName, tree::OutputNode, @@ -56,16 +55,6 @@ pub struct DirectScanoutData { position: DirectScanoutPosition, } -#[derive(Debug)] -pub struct DirectScanoutPosition { - pub src_width: i32, - pub src_height: i32, - pub crtc_x: i32, - pub crtc_y: i32, - pub crtc_width: i32, - pub crtc_height: i32, -} - pub struct PresentFb { fb: Rc, tex: Rc, @@ -643,122 +632,17 @@ impl MetalConnector { blend_cd: &Rc, cd: &Rc, ) -> Option { - let ct = 'ct: { - let mut ops = pass.ops.iter().rev(); - let ct = 'ct2: { - for opt in &mut ops { - match opt { - GfxApiOpt::Sync => {} - GfxApiOpt::FillRect(_) => { - // Top-most layer must be a texture. - return None; - } - GfxApiOpt::CopyTexture(ct) => break 'ct2 ct, - } - } - return None; - }; - if ct.alpha_mode != AlphaMode::PremultipliedElectrical { - // Direct scanout requires premultiplied electrical alpha. - return None; - } - if !ct.cd.embeds_into(cd) { - // Direct scanout requires embeddable color descriptions. - return None; - } - if !ct.opaque && !ct.cd.embeds_into(blend_cd) { - // Blending changes the appearance of translucent buffers. - return None; - } - if ct.alpha.is_some() { - // Direct scanout with alpha factor is not supported. - return None; - } - if !ct.tex.format().has_alpha && ct.target.is_covering() { - // Texture covers the entire screen and is opaque. - break 'ct ct; - } - for opt in ops { - match opt { - GfxApiOpt::Sync => {} - GfxApiOpt::FillRect(fr) => { - if fr.effective_color() == Color::SOLID_BLACK { - // Black fills can be ignored because this is the CRTC background color. - if fr.rect.is_covering() { - // If fill covers the entire screen, we don't have to look further. - break 'ct ct; - } - } else { - // Fill could be visible. - return None; - } - } - GfxApiOpt::CopyTexture(_) => { - // Texture could be visible. - return None; - } - } - } - if let Some(clear) = pass.clear - && clear != Color::SOLID_BLACK - { - // Background could be visible. - return None; - } - ct - }; - if let AcquireSync::None | AcquireSync::Implicit = ct.acquire_sync { - // Cannot perform scanout without explicit sync. - return None; - } - if ct.source.buffer_transform != ct.target.output_transform { - // Rotations and mirroring are not supported. - return None; - } - if !ct.source.is_covering() { - // Viewports are not supported. - return None; - } - if ct.target.x1 < -1.0 || ct.target.y1 < -1.0 || ct.target.x2 > 1.0 || ct.target.y2 > 1.0 { - // Rendering outside the screen is not supported. - return None; - } - let (tex_w, tex_h) = ct.tex.size(); - let (x1, x2, y1, y2) = { - let plane_w = plane.mode_w.get() as f32; - let plane_h = plane.mode_h.get() as f32; - let ((x1, x2), (y1, y2)) = ct - .target - .output_transform - .maybe_swap(((ct.target.x1, ct.target.x2), (ct.target.y1, ct.target.y2))); - ( - (x1 + 1.0) * plane_w / 2.0, - (x2 + 1.0) * plane_w / 2.0, - (y1 + 1.0) * plane_h / 2.0, - (y2 + 1.0) * plane_h / 2.0, - ) - }; - let (crtc_w, crtc_h) = (x2 - x1, y2 - y1); - if crtc_w < 0.0 || crtc_h < 0.0 { - // Flipping x or y axis is not supported. - return None; - } - if self.cursor_enabled.get() && (tex_w as f32, tex_h as f32) != (crtc_w, crtc_h) { - // If hardware cursors are used, we cannot scale the texture. - return None; - } + let (ct, position) = pass.prepare_direct_scanout( + plane.mode_w.get(), + plane.mode_h.get(), + blend_cd, + cd, + self.cursor_enabled.get(), + )?; let Some(dmabuf) = ct.tex.dmabuf() else { // Shm buffers cannot be scanned out. return None; }; - let position = DirectScanoutPosition { - src_width: tex_w, - src_height: tex_h, - crtc_x: x1 as _, - crtc_y: y1 as _, - crtc_width: crtc_w as _, - crtc_height: crtc_h as _, - }; let mut cache = self.scanout_buffers.borrow_mut(); if let Some(buffer) = cache.get(&dmabuf.id) { return buffer.fb.as_ref().map(|fb| DirectScanoutData { diff --git a/src/gfx_api.rs b/src/gfx_api.rs index 2900f5a8..e908a9ac 100644 --- a/src/gfx_api.rs +++ b/src/gfx_api.rs @@ -1214,3 +1214,138 @@ impl FdSync { } } } + +#[derive(Debug)] +pub struct DirectScanoutPosition { + pub src_width: i32, + pub src_height: i32, + pub crtc_x: i32, + pub crtc_y: i32, + pub crtc_width: i32, + pub crtc_height: i32, +} + +impl GfxRenderPass { + pub fn prepare_direct_scanout( + &self, + mode_w: i32, + mode_h: i32, + blend_cd: &Rc, + cd: &Rc, + no_scaling: bool, + ) -> Option<(&CopyTexture, DirectScanoutPosition)> { + let ct = 'ct: { + let mut ops = self.ops.iter().rev(); + let ct = 'ct2: { + for opt in &mut ops { + match opt { + GfxApiOpt::Sync => {} + GfxApiOpt::FillRect(_) => { + // Top-most layer must be a texture. + return None; + } + GfxApiOpt::CopyTexture(ct) => break 'ct2 ct, + } + } + return None; + }; + if ct.alpha_mode != AlphaMode::PremultipliedElectrical { + // Direct scanout requires premultiplied electrical alpha. + return None; + } + if !ct.cd.embeds_into(cd) { + // Direct scanout requires embeddable color descriptions. + return None; + } + if !ct.opaque && !ct.cd.embeds_into(blend_cd) { + // Blending changes the appearance of translucent buffers. + return None; + } + if ct.alpha.is_some() { + // Direct scanout with alpha factor is not supported. + return None; + } + if !ct.tex.format().has_alpha && ct.target.is_covering() { + // Texture covers the entire screen and is opaque. + break 'ct ct; + } + for opt in ops { + match opt { + GfxApiOpt::Sync => {} + GfxApiOpt::FillRect(fr) => { + if fr.effective_color() == Color::SOLID_BLACK { + // Black fills can be ignored because this is the CRTC background color. + if fr.rect.is_covering() { + // If fill covers the entire screen, we don't have to look further. + break 'ct ct; + } + } else { + // Fill could be visible. + return None; + } + } + GfxApiOpt::CopyTexture(_) => { + // Texture could be visible. + return None; + } + } + } + if let Some(clear) = self.clear + && clear != Color::SOLID_BLACK + { + // Background could be visible. + return None; + } + ct + }; + if let AcquireSync::None | AcquireSync::Implicit = ct.acquire_sync { + // Cannot perform scanout without explicit sync. + return None; + } + if ct.source.buffer_transform != ct.target.output_transform { + // Rotations and mirroring are not supported. + return None; + } + if !ct.source.is_covering() { + // Viewports are not supported. + return None; + } + if ct.target.x1 < -1.0 || ct.target.y1 < -1.0 || ct.target.x2 > 1.0 || ct.target.y2 > 1.0 { + // Rendering outside the screen is not supported. + return None; + } + let (tex_w, tex_h) = ct.tex.size(); + let (x1, x2, y1, y2) = { + let plane_w = mode_w as f32; + let plane_h = mode_h as f32; + let ((x1, x2), (y1, y2)) = ct + .target + .output_transform + .maybe_swap(((ct.target.x1, ct.target.x2), (ct.target.y1, ct.target.y2))); + ( + (x1 + 1.0) * plane_w / 2.0, + (x2 + 1.0) * plane_w / 2.0, + (y1 + 1.0) * plane_h / 2.0, + (y2 + 1.0) * plane_h / 2.0, + ) + }; + let (crtc_w, crtc_h) = (x2 - x1, y2 - y1); + if crtc_w < 0.0 || crtc_h < 0.0 { + // Flipping x or y axis is not supported. + return None; + } + if no_scaling && (tex_w as f32, tex_h as f32) != (crtc_w, crtc_h) { + // If scaling is not supported, we cannot scale the texture. + return None; + } + let position = DirectScanoutPosition { + src_width: tex_w, + src_height: tex_h, + crtc_x: x1 as _, + crtc_y: y1 as _, + crtc_width: crtc_w as _, + crtc_height: crtc_h as _, + }; + Some((ct, position)) + } +} From b6c857598891b49b9b9a5eda584734a52b6d5b8f Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Wed, 18 Mar 2026 14:32:19 +0100 Subject: [PATCH 7/7] head-management: pull persistent state upon first change --- src/compositor.rs | 2 + src/control_center/cc_outputs.rs | 13 ++-- src/ifs/head_management.rs | 67 +++++++++++++------ .../jay_head_manager_session_v1.rs | 5 +- src/state.rs | 41 +++++++++++- src/tasks/connector.rs | 44 +++--------- 6 files changed, 107 insertions(+), 65 deletions(-) diff --git a/src/compositor.rs b/src/compositor.rs index 95e8463a..c48af940 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -681,6 +681,7 @@ fn create_dummy_output(state: &Rc) { let name = Rc::new("Dummy".to_string()); let head_name = state.head_names.next(); let head_state = HeadState { + connector_id: id, name: RcEq(name.clone()), position: (0, 0), size: (0, 0), @@ -708,6 +709,7 @@ fn create_dummy_output(state: &Rc) { blend_space: BlendSpace::Srgb, use_native_gamut: false, vrr_cursor_hz: None, + persistent_state: Some(RcEq(persistent_state.clone())), }; let connector_data = Rc::new(ConnectorData { id, diff --git a/src/control_center/cc_outputs.rs b/src/control_center/cc_outputs.rs index d5027777..9c48cc83 100644 --- a/src/control_center/cc_outputs.rs +++ b/src/control_center/cc_outputs.rs @@ -842,6 +842,7 @@ impl OutputsPaneInner { let Some(desired) = &head.changed_state else { continue; }; + desired.flush_persistent_state(&self.state); if let Some(output) = self.state.outputs.get(&head.id) && let Some(node) = &output.node { @@ -957,7 +958,7 @@ fn show_connector(state: &State, settings: &Settings, head: &mut CompleteHead, u grid(ui, ("settings", head.name), |ui| { let mut diff = false; show_serial_number(ui, m); - diff |= show_enablement(ui, m, t); + diff |= show_enablement(state, ui, m, t); diff |= show_position(ui, m, t); diff |= show_scale(ui, m, t); diff |= show_mode(ui, m, t); @@ -969,7 +970,7 @@ fn show_connector(state: &State, settings: &Settings, head: &mut CompleteHead, u diff |= show_format(ui, m, t); diff |= show_tearing(ui, m, t); diff |= show_vrr(ui, m, t); - diff |= show_non_desktop(ui, m, t); + diff |= show_non_desktop(state, ui, m, t); diff |= show_blend_space(ui, m, t); diff |= show_use_native_gamut(ui, m, t); show_native_gamut(ui, m); @@ -993,7 +994,7 @@ fn show_serial_number(ui: &mut Ui, m: &HeadState) { } } -fn show_enablement(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { +fn show_enablement(state: &State, ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { let ui = &mut *ui.row(); grid_label(ui, "Enabled"); let mut v = effective!(m, t).connector_enabled; @@ -1001,7 +1002,7 @@ fn show_enablement(ui: &mut Ui, m: &HeadState, t: &mut Option) -> boo if changed { let t = modify!(m, t); t.connector_enabled = v; - t.update_in_compositor_space(m.wl_output); + t.update_in_compositor_space(state, m.wl_output); } let diff = v != m.connector_enabled; if diff { @@ -1550,7 +1551,7 @@ fn show_vrr(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { diff } -fn show_non_desktop(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { +fn show_non_desktop(state: &State, ui: &mut Ui, m: &HeadState, t: &mut Option) -> bool { { let ui = &mut *ui.row(); grid_label(ui, "Non-desktop"); @@ -1580,7 +1581,7 @@ fn show_non_desktop(ui: &mut Ui, m: &HeadState, t: &mut Option) -> bo if changed { let t = modify!(m, t); t.override_non_desktop = v; - t.update_in_compositor_space(m.wl_output); + t.update_in_compositor_space(state, m.wl_output); } let diff = v != m.override_non_desktop; if diff { diff --git a/src/ifs/head_management.rs b/src/ifs/head_management.rs index 46dd514b..f10ace52 100644 --- a/src/ifs/head_management.rs +++ b/src/ifs/head_management.rs @@ -12,16 +12,17 @@ use { head_management_macros::HeadExts, jay_head_manager_session_v1::JayHeadManagerSessionV1, jay_head_v1::JayHeadV1, }, - wl_output::BlendSpace, + wl_output::{BlendSpace, PersistentOutputState}, }, scale::Scale, - state::OutputData, + state::{OutputData, State}, tree::{OutputNode, TearingMode, Transform, VrrMode}, utils::{copyhashmap::CopyHashMap, hash_map_ext::HashMapExt, rc_eq::RcEq}, wire::JayHeadManagerSessionV1Id, }, std::{ cell::{Cell, Ref, RefCell}, + collections::hash_map::Entry, rc::Rc, }, thiserror::Error, @@ -71,6 +72,7 @@ struct HeadCommon { #[derive(Clone, PartialEq)] pub struct HeadState { + pub connector_id: ConnectorId, pub name: RcEq, pub wl_output: Option, pub connector_enabled: bool, @@ -98,6 +100,7 @@ pub struct HeadState { pub blend_space: BlendSpace, pub use_native_gamut: bool, pub vrr_cursor_hz: Option, + pub persistent_state: Option>, } pub struct ReadOnlyHeadState { @@ -111,7 +114,7 @@ impl ReadOnlyHeadState { } impl HeadState { - pub fn update_in_compositor_space(&mut self, wl_output: Option) { + pub fn update_in_compositor_space(&mut self, state: &State, wl_output: Option) { self.in_compositor_space = false; self.wl_output = None; if !self.connector_enabled { @@ -128,6 +131,26 @@ impl HeadState { } self.in_compositor_space = true; self.wl_output = wl_output; + if self.persistent_state.is_none() { + let ds = state + .persistent_output_states + .get(&mi.output_id) + .unwrap_or_else(|| state.new_persistent_output_state()); + self.position = ds.pos.get(); + self.transform = ds.transform.get(); + self.vrr_mode = ds.vrr_mode.get(); + self.tearing_mode = ds.tearing_mode.get(); + self.brightness = ds.brightness.get(); + self.blend_space = ds.blend_space.get(); + self.use_native_gamut = ds.use_native_gamut.get(); + self.vrr_cursor_hz = ds.vrr_cursor_hz.get(); + self.scale = ds.scale.get(); + self.persistent_state = Some(RcEq(ds)); + if let Some(c) = state.connectors.get(&self.connector_id) { + self.mode = c.state.borrow().mode; + } + self.update_size(); + } } pub fn update_size(&mut self) { @@ -135,6 +158,18 @@ impl HeadState { OutputNode::calculate_extents_(self.mode, self.transform, self.scale, self.position) .size(); } + + pub fn flush_persistent_state(&self, state: &State) { + if let Some(mi) = &self.monitor_info + && let Some(ds) = &self.persistent_state + && let Entry::Vacant(v) = state + .persistent_output_states + .lock() + .entry(mi.output_id.clone()) + { + v.insert(ds.0.clone()); + } + } } enum HeadOp { @@ -249,24 +284,13 @@ impl HeadManagers { } } - pub fn handle_output_connected(&self, output: &OutputData) { + pub fn handle_output_connected(&self, s: &State, output: &OutputData) { let state = &mut *self.state.borrow_mut(); state.connected = true; state.monitor_info = Some(RcEq(output.monitor_info.clone())); + state.persistent_state = None; state.inherent_non_desktop = output.monitor_info.non_desktop; - state.update_in_compositor_space(output.node.as_ref().map(|n| n.global.name)); - if let Some(n) = &output.node { - state.position = n.global.pos.get().position(); - state.size = n.global.pos.get().size(); - state.mode = n.global.mode.get(); - state.transform = n.global.persistent.transform.get(); - state.vrr_mode = n.global.persistent.vrr_mode.get(); - state.tearing_mode = n.global.persistent.tearing_mode.get(); - state.brightness = n.global.persistent.brightness.get(); - state.blend_space = n.global.persistent.blend_space.get(); - state.use_native_gamut = n.global.persistent.use_native_gamut.get(); - state.vrr_cursor_hz = n.global.persistent.vrr_cursor_hz.get(); - } + state.update_in_compositor_space(s, output.node.as_ref().map(|n| n.global.name)); for head in self.managers.lock().values() { skip_in_transaction!(head); if let Some(ext) = &head.ext.connector_info_v1 { @@ -321,11 +345,12 @@ impl HeadManagers { } } - pub fn handle_output_disconnected(&self) { + pub fn handle_output_disconnected(&self, s: &State) { let state = &mut *self.state.borrow_mut(); state.connected = false; state.monitor_info = None; - state.update_in_compositor_space(None); + state.persistent_state = None; + state.update_in_compositor_space(s, None); for head in self.managers.lock().values() { skip_in_transaction!(head); if let Some(ext) = &head.ext.compositor_space_info_v1 { @@ -406,10 +431,10 @@ impl HeadManagers { } } - pub fn handle_enabled_change(&self, enabled: bool) { + pub fn handle_enabled_change(&self, s: &State, enabled: bool) { let state = &mut *self.state.borrow_mut(); state.connector_enabled = enabled; - state.update_in_compositor_space(state.wl_output); + state.update_in_compositor_space(s, state.wl_output); for head in self.managers.lock().values() { skip_in_transaction!(head); if let Some(ext) = &head.ext.compositor_space_info_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 15dbe174..f1316c5e 100644 --- a/src/ifs/head_management/jay_head_manager_session_v1.rs +++ b/src/ifs/head_management/jay_head_manager_session_v1.rs @@ -404,7 +404,7 @@ impl JayHeadManagerSessionV1RequestHandler for JayHeadManagerSessionV1 { } HeadOp::SetConnectorEnabled(enabled) => { state.connector_enabled = enabled; - state.update_in_compositor_space(snapshot.wl_output); + state.update_in_compositor_space(&self.client.state, snapshot.wl_output); to_send |= COMPOSITOR_SPACE_INFO_FULL; to_send |= COMPOSITOR_SPACE_INFO_ENABLED; to_send |= CORE_INFO; @@ -435,7 +435,7 @@ impl JayHeadManagerSessionV1RequestHandler for JayHeadManagerSessionV1 { } HeadOp::SetNonDesktopOverride(m) => { state.override_non_desktop = m; - state.update_in_compositor_space(snapshot.wl_output); + state.update_in_compositor_space(&self.client.state, snapshot.wl_output); to_send |= COMPOSITOR_SPACE_INFO_FULL; to_send |= CORE_INFO; to_send |= NON_DESKTOP_INFO; @@ -557,6 +557,7 @@ impl JayHeadManagerSessionV1RequestHandler for JayHeadManagerSessionV1 { } for head in self.heads.lock().values() { let desired = &*head.common.transaction_state.borrow(); + desired.flush_persistent_state(&self.client.state); if let Some(output) = self.client.state.outputs.get(&head.common.id) && let Some(node) = &output.node { diff --git a/src/state.rs b/src/state.rs index ad537ee1..04fb9cbd 100644 --- a/src/state.rs +++ b/src/state.rs @@ -62,7 +62,7 @@ use { jay_seat_events::JaySeatEvents, jay_workspace_watcher::JayWorkspaceWatcher, wl_buffer::WlBuffer, - wl_output::{OutputGlobalOpt, OutputId, PersistentOutputState}, + wl_output::{BlendSpace, OutputGlobalOpt, OutputId, PersistentOutputState}, wl_seat::{ PhysicalKeyboardId, PhysicalKeyboardIds, PositionHintRequest, SeatIds, WlSeatGlobal, @@ -504,7 +504,7 @@ impl ConnectorData { }}; } if b!(old.enabled != s.enabled) { - self.head_managers.handle_enabled_change(s.enabled); + self.head_managers.handle_enabled_change(state, s.enabled); } if b!(old.active != s.active) { self.head_managers.handle_active_change(s.active); @@ -1961,6 +1961,43 @@ impl State { colored.field(&self.theme).set(v); self.colors_changed(); } + + pub fn ensure_persistent_output_state( + &self, + output_id: &Rc, + ) -> Rc { + match self.persistent_output_states.get(output_id) { + Some(ds) => ds, + _ => { + let ds = self.new_persistent_output_state(); + self.persistent_output_states + .set(output_id.clone(), ds.clone()); + ds + } + } + } + + pub fn new_persistent_output_state(&self) -> Rc { + let x1 = self + .root + .outputs + .lock() + .values() + .map(|o| o.global.pos.get().x2()) + .max() + .unwrap_or(0); + Rc::new(PersistentOutputState { + transform: Default::default(), + scale: Default::default(), + pos: Cell::new((x1, 0)), + vrr_mode: Cell::new(self.default_vrr_mode.get()), + vrr_cursor_hz: Cell::new(self.default_vrr_cursor_hz.get()), + tearing_mode: Cell::new(self.default_tearing_mode.get()), + brightness: Cell::new(None), + blend_space: Cell::new(BlendSpace::Srgb), + use_native_gamut: Cell::new(false), + }) + } } #[derive(Debug, Error)] diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index 2b6af537..cdf3cf44 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -6,7 +6,7 @@ use { ifs::{ head_management::{HeadManagers, HeadState}, jay_tray_v1::JayTrayV1Global, - wl_output::{BlendSpace, PersistentOutputState, WlOutputGlobal}, + wl_output::{BlendSpace, WlOutputGlobal}, }, output_schedule::OutputSchedule, state::{ConnectorData, OutputData, State}, @@ -35,6 +35,7 @@ pub fn handle(state: &Rc, connector: &Rc) { let id = connector.id(); let name = Rc::new(connector.name()); let head_state = HeadState { + connector_id: id, name: RcEq(name.clone()), position: (0, 0), size: (0, 0), @@ -45,7 +46,7 @@ pub fn handle(state: &Rc, connector: &Rc) { wl_output: None, connector_enabled: backend_state.enabled, in_compositor_space: false, - mode: Default::default(), + mode: backend_state.mode, monitor_info: None, inherent_non_desktop: false, override_non_desktop: backend_state.non_desktop_override, @@ -62,6 +63,7 @@ pub fn handle(state: &Rc, connector: &Rc) { blend_space: BlendSpace::Srgb, use_native_gamut: false, vrr_cursor_hz: None, + persistent_state: None, }; let data = Rc::new(ConnectorData { id, @@ -152,7 +154,9 @@ impl ConnectorHandler { self.handle_desktop_connected(info, name).await; } self.data.connected.set(false); - self.data.head_managers.handle_output_disconnected(); + self.data + .head_managers + .handle_output_disconnected(&self.state); self.state.trigger_cci(CCI_OUTPUTS); for head in self.data.wlr_output_heads.lock().drain_values() { head.handle_disconnected(); @@ -162,35 +166,7 @@ impl ConnectorHandler { async fn handle_desktop_connected(&self, info: MonitorInfo, name: GlobalName) { let output_id = info.output_id.clone(); - let desired_state = match self.state.persistent_output_states.get(&output_id) { - Some(ds) => ds, - _ => { - let x1 = self - .state - .root - .outputs - .lock() - .values() - .map(|o| o.global.pos.get().x2()) - .max() - .unwrap_or(0); - let ds = Rc::new(PersistentOutputState { - transform: Default::default(), - scale: Default::default(), - pos: Cell::new((x1, 0)), - vrr_mode: Cell::new(self.state.default_vrr_mode.get()), - vrr_cursor_hz: Cell::new(self.state.default_vrr_cursor_hz.get()), - tearing_mode: Cell::new(self.state.default_tearing_mode.get()), - brightness: Cell::new(None), - blend_space: Cell::new(BlendSpace::Srgb), - use_native_gamut: Cell::new(false), - }); - self.state - .persistent_output_states - .set(output_id.clone(), ds.clone()); - ds - } - }; + let desired_state = self.state.ensure_persistent_output_state(&output_id); let global = Rc::new(WlOutputGlobal::new( name, &self.state, @@ -327,7 +303,7 @@ impl ConnectorHandler { self.state.workspace_managers.announce_output(&on); self.data .head_managers - .handle_output_connected(&output_data); + .handle_output_connected(&self.state, &output_data); self.state.trigger_cci(CCI_OUTPUTS); self.state.wlr_output_managers.announce_head(&output_data); 'outer: loop { @@ -454,7 +430,7 @@ impl ConnectorHandler { } self.data .head_managers - .handle_output_connected(&output_data); + .handle_output_connected(&self.state, &output_data); self.state.trigger_cci(CCI_OUTPUTS); self.state.wlr_output_managers.announce_head(&output_data); 'outer: loop {