From 1a9753847acd679a4249948076ac6ef88b5396a6 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 17 Mar 2026 19:29:11 +0100 Subject: [PATCH] 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) { +}