1
0
Fork 0
forked from wry/wry

Merge pull request #812 from mahkoh/jorth/vo-preparation

Preparations for virtual outputs
This commit is contained in:
mahkoh 2026-03-18 20:49:35 +01:00 committed by GitHub
commit 7e479490c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 620 additions and 357 deletions

View file

@ -560,6 +560,12 @@ impl ConfigClient {
connector 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 { pub fn get_seat_cursor_workspace(&self, seat: Seat) -> Workspace {
let res = self.send_with_response(&ClientMessage::GetSeatCursorWorkspace { seat }); let res = self.send_with_response(&ClientMessage::GetSeatCursorWorkspace { seat });
get_response!(res, Workspace(0), GetSeatCursorWorkspace { workspace }); get_response!(res, Workspace(0), GetSeatCursorWorkspace { workspace });
@ -1191,6 +1197,19 @@ impl ConfigClient {
modes.into_iter().map(WireMode::to_mode).collect() 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) { pub fn connector_size(&self, connector: Connector) -> (i32, i32) {
let res = self.send_with_response(&ClientMessage::ConnectorSize { connector }); let res = self.send_with_response(&ClientMessage::ConnectorSize { connector });
get_response!(res, (0, 0), ConnectorSize { width, height }); get_response!(res, (0, 0), ConnectorSize { width, height });

View file

@ -846,6 +846,12 @@ pub enum ClientMessage<'a> {
monospace: Option<Vec<&'a str>>, monospace: Option<Vec<&'a str>>,
}, },
OpenControlCenter, OpenControlCenter,
ConnectorSupportsArbitraryModes {
connector: Connector,
},
GetConnectorByName {
name: &'a str,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -1096,6 +1102,9 @@ pub enum Response {
KeymapFromNames { KeymapFromNames {
keymap: Keymap, keymap: Keymap,
}, },
ConnectorSupportsArbitraryModes {
supports_arbitrary_modes: bool,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -157,6 +157,14 @@ impl Connector {
get!(Vec::new()).connector_modes(self) 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. /// Returns the logical width of the connector.
/// ///
/// The returned value will be different from `mode().width()` if the scale is not 1. /// The returned value will be different from `mode().width()` if the scale is not 1.
@ -446,6 +454,14 @@ pub fn get_connector(id: impl ToConnectorId) -> Connector {
get!(Connector(0)).get_connector(ty, idx) 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 `<type>-<id>` 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. /// A type that can be converted to a `(ConnectorType, idx)` tuple.
pub trait ToConnectorId { pub trait ToConnectorId {
fn to_connector_id(&self) -> Result<(ConnectorType, u32), String>; fn to_connector_id(&self) -> Result<(ConnectorType, u32), String>;

View file

@ -100,7 +100,7 @@ impl Display for Mode {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct MonitorInfo { pub struct MonitorInfo {
pub modes: Vec<Mode>, pub modes: Option<Vec<Mode>>,
pub output_id: Rc<OutputId>, pub output_id: Rc<OutputId>,
pub width_mm: i32, pub width_mm: i32,
pub height_mm: i32, pub height_mm: i32,
@ -141,6 +141,7 @@ pub trait Connector: Any {
fn damage(&self); fn damage(&self);
fn drm_dev(&self) -> Option<DrmDeviceId>; fn drm_dev(&self) -> Option<DrmDeviceId>;
fn effectively_locked(&self) -> bool; fn effectively_locked(&self) -> bool;
fn state(&self) -> BackendConnectorState;
fn caps(&self) -> ConnectorCaps { fn caps(&self) -> ConnectorCaps {
ConnectorCaps::none() ConnectorCaps::none()
} }
@ -169,6 +170,9 @@ pub trait Connector: Any {
fn gamma_lut_size(&self) -> Option<u32> { fn gamma_lut_size(&self) -> Option<u32> {
None None
} }
fn name(&self) -> String {
self.kernel_id().to_string()
}
} }
#[derive(Debug)] #[derive(Debug)]

View file

@ -2,8 +2,10 @@ use {
crate::{ crate::{
async_engine::SpawnedFuture, async_engine::SpawnedFuture,
backend::{ backend::{
Backend, Connector, ConnectorEvent, ConnectorId, ConnectorKernelId, DrmDeviceId, self, Backend, BackendConnectorState, BackendConnectorStateSerial, Connector,
ConnectorEvent, ConnectorId, ConnectorKernelId, DrmDeviceId,
}, },
format::XRGB8888,
video::drm::ConnectorType, video::drm::ConnectorType,
}, },
std::{error::Error, rc::Rc}, std::{error::Error, rc::Rc},
@ -52,4 +54,25 @@ impl Connector for DummyOutput {
fn effectively_locked(&self) -> bool { fn effectively_locked(&self) -> bool {
true 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(),
}
}
} }

View file

@ -9,12 +9,11 @@ use {
}, },
cmm::cmm_description::ColorDescription, cmm::cmm_description::ColorDescription,
gfx_api::{ gfx_api::{
AcquireSync, AlphaMode, BufferResv, GfxApiOpt, GfxRenderPass, GfxTexture, ReleaseSync, AcquireSync, BufferResv, DirectScanoutPosition, GfxRenderPass, GfxTexture, ReleaseSync,
SyncFile, create_render_pass, SyncFile, create_render_pass,
}, },
ifs::wl_output::BlendSpace, ifs::wl_output::BlendSpace,
rect::Region, rect::Region,
theme::Color,
time::Time, time::Time,
tracy::FrameName, tracy::FrameName,
tree::OutputNode, tree::OutputNode,
@ -56,16 +55,6 @@ pub struct DirectScanoutData {
position: DirectScanoutPosition, 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 { pub struct PresentFb {
fb: Rc<DrmFramebuffer>, fb: Rc<DrmFramebuffer>,
tex: Rc<dyn GfxTexture>, tex: Rc<dyn GfxTexture>,
@ -643,122 +632,17 @@ impl MetalConnector {
blend_cd: &Rc<ColorDescription>, blend_cd: &Rc<ColorDescription>,
cd: &Rc<ColorDescription>, cd: &Rc<ColorDescription>,
) -> Option<DirectScanoutData> { ) -> Option<DirectScanoutData> {
let ct = 'ct: { let (ct, position) = pass.prepare_direct_scanout(
let mut ops = pass.ops.iter().rev(); plane.mode_w.get(),
let ct = 'ct2: { plane.mode_h.get(),
for opt in &mut ops { blend_cd,
match opt { cd,
GfxApiOpt::Sync => {} self.cursor_enabled.get(),
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 Some(dmabuf) = ct.tex.dmabuf() else { let Some(dmabuf) = ct.tex.dmabuf() else {
// Shm buffers cannot be scanned out. // Shm buffers cannot be scanned out.
return None; 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(); let mut cache = self.scanout_buffers.borrow_mut();
if let Some(buffer) = cache.get(&dmabuf.id) { if let Some(buffer) = cache.get(&dmabuf.id) {
return buffer.fb.as_ref().map(|fb| DirectScanoutData { return buffer.fb.as_ref().map(|fb| DirectScanoutData {

View file

@ -867,6 +867,10 @@ impl Connector for MetalConnector {
fb.locked fb.locked
} }
fn state(&self) -> BackendConnectorState {
self.display.borrow().persistent.state.borrow().clone()
}
fn caps(&self) -> ConnectorCaps { fn caps(&self) -> ConnectorCaps {
CONCAP_CONNECTOR | CONCAP_MODE_SETTING | CONCAP_PHYSICAL_DISPLAY CONCAP_CONNECTOR | CONCAP_MODE_SETTING | CONCAP_PHYSICAL_DISPLAY
} }
@ -1959,7 +1963,7 @@ impl MetalBackend {
let mut state = dd.persistent.state.borrow().clone(); let mut state = dd.persistent.state.borrow().clone();
state.serial = self.state.backend_connector_state_serials.next(); state.serial = self.state.backend_connector_state_serials.next();
connector.send_event(ConnectorEvent::Connected(MonitorInfo { connector.send_event(ConnectorEvent::Connected(MonitorInfo {
modes, modes: Some(modes),
output_id: dd.output_id.clone(), output_id: dd.output_id.clone(),
width_mm: dd.mm_width as _, width_mm: dd.mm_width as _,
height_mm: dd.mm_height as _, height_mm: dd.mm_height as _,

View file

@ -590,7 +590,7 @@ impl XBackend {
.backend_events .backend_events
.push(BackendEvent::NewConnector(output.clone())); .push(BackendEvent::NewConnector(output.clone()));
output.events.push(ConnectorEvent::Connected(MonitorInfo { output.events.push(ConnectorEvent::Connected(MonitorInfo {
modes: vec![], modes: Some(vec![]),
output_id: Rc::new(OutputId::new( output_id: Rc::new(OutputId::new(
String::new(), String::new(),
"X.Org Foundation".to_string(), "X.Org Foundation".to_string(),
@ -1113,6 +1113,10 @@ impl Connector for XOutput {
true true
} }
fn state(&self) -> BackendConnectorState {
self.state.borrow().clone()
}
fn transaction_type(&self) -> Box<dyn BackendConnectorTransactionTypeDyn> { fn transaction_type(&self) -> Box<dyn BackendConnectorTransactionTypeDyn> {
Box::new(XTransactionType) Box::new(XTransactionType)
} }

View file

@ -527,6 +527,7 @@ struct Output {
pub blend_space: Option<String>, pub blend_space: Option<String>,
pub native_gamut: Option<Primaries>, pub native_gamut: Option<Primaries>,
pub use_native_gamut: bool, pub use_native_gamut: bool,
pub arbitrary_modes: bool,
} }
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
@ -641,9 +642,22 @@ impl Randr {
log::error!("Connector {} is not connected", connector.name); log::error!("Connector {} is not connected", connector.name);
return; return;
}; };
let Some(mode) = output.modes.iter().find(|m| { let mode = 'mode: {
m.width == t.width && m.height == t.height && m.refresh_rate() == t.refresh_rate if let Some(mode) = output.modes.iter().find(|m| {
}) else { 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!( log::error!(
"Output {} does not support this refresh rate", "Output {} does not support this refresh rate",
connector.name connector.name
@ -1082,6 +1096,9 @@ impl Randr {
p.b.0.0, p.b.1.0, p.wp.0.0, p.wp.1.0 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 { if o.modes.is_not_empty() && modes {
println!(" modes:"); println!(" modes:");
for mode in &o.modes { for mode in &o.modes {
@ -1280,6 +1297,12 @@ impl Randr {
let output = c.output.as_mut().unwrap(); let output = c.output.as_mut().unwrap();
output.use_native_gamut = true; 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; tc.round_trip().await;
data.borrow_mut().clone() data.borrow_mut().clone()
} }

View file

@ -4,7 +4,7 @@ use {
crate::{ crate::{
acceptor::{Acceptor, AcceptorError}, acceptor::{Acceptor, AcceptorError},
async_engine::{AsyncEngine, Phase, SpawnedFuture}, async_engine::{AsyncEngine, Phase, SpawnedFuture},
backend::{self, Backend, BackendConnectorState, BackendConnectorStateSerial, Connector}, backend::{Backend, Connector},
backends::{ backends::{
dummy::{DummyBackend, DummyOutput}, dummy::{DummyBackend, DummyOutput},
metal, x, metal, x,
@ -675,29 +675,13 @@ fn create_dummy_output(state: &Rc<State>) {
serial_number: "".to_string(), serial_number: "".to_string(),
}); });
let persistent_state = Rc::new(PersistentOutputState::default()); 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 id = state.connector_ids.next();
let connector = Rc::new(DummyOutput { id }) as Rc<dyn Connector>; let connector = Rc::new(DummyOutput { id }) as Rc<dyn Connector>;
let backend_state = connector.state();
let name = Rc::new("Dummy".to_string()); let name = Rc::new("Dummy".to_string());
let head_name = state.head_names.next(); let head_name = state.head_names.next();
let head_state = HeadState { let head_state = HeadState {
connector_id: id,
name: RcEq(name.clone()), name: RcEq(name.clone()),
position: (0, 0), position: (0, 0),
size: (0, 0), size: (0, 0),
@ -725,6 +709,7 @@ fn create_dummy_output(state: &Rc<State>) {
blend_space: BlendSpace::Srgb, blend_space: BlendSpace::Srgb,
use_native_gamut: false, use_native_gamut: false,
vrr_cursor_hz: None, vrr_cursor_hz: None,
persistent_state: Some(RcEq(persistent_state.clone())),
}; };
let connector_data = Rc::new(ConnectorData { let connector_data = Rc::new(ConnectorData {
id, id,
@ -754,7 +739,7 @@ fn create_dummy_output(state: &Rc<State>) {
state.globals.name(), state.globals.name(),
state, state,
&connector_data, &connector_data,
Vec::new(), Some(Vec::new()),
0, 0,
0, 0,
&output_id, &output_id,

View file

@ -1205,6 +1205,7 @@ impl ConfigProxyHandler {
.global .global
.modes .modes
.iter() .iter()
.flatten()
.map(|m| WireMode { .map(|m| WireMode {
width: m.width, width: m.width,
height: m.height, height: m.height,
@ -1215,6 +1216,17 @@ impl ConfigProxyHandler {
Ok(()) 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> { fn handle_connector_name(&self, connector: Connector) -> Result<(), CphError> {
let connector = self.get_connector(connector)?; let connector = self.get_connector(connector)?;
self.respond(Response::GetConnectorName { self.respond(Response::GetConnectorName {
@ -1580,6 +1592,19 @@ impl ConfigProxyHandler {
Ok(()) 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> { fn handle_get_connector_active_workspace(&self, connector: Connector) -> Result<(), CphError> {
let output = self.get_output_node(connector)?; let output = self.get_output_node(connector)?;
let workspace = output let workspace = output
@ -3328,6 +3353,10 @@ impl ConfigProxyHandler {
monospace, monospace,
} => self.handle_set_egui_fonts(proportional, monospace), } => self.handle_set_egui_fonts(proportional, monospace),
ClientMessage::OpenControlCenter => self.handle_open_control_center(), ClientMessage::OpenControlCenter => self.handle_open_control_center(),
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(()) Ok(())
} }

View file

@ -134,11 +134,11 @@ impl GpusPane {
.connectors .connectors
.lock() .lock()
.values() .values()
.map(|v| v.connector.kernel_id().to_string()) .map(|v| v.name.clone())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
cs.sort(); cs.sort();
for c in cs { for c in cs {
ui.label(c); ui.label(&**c);
} }
}); });
}); });

View file

@ -25,9 +25,10 @@ use {
}, },
ahash::AHashMap, ahash::AHashMap,
egui::{ egui::{
Align, Button, Checkbox, Color32, ComboBox, DragValue, EventFilter, FontId, Frame, Grid, Align, Button, Checkbox, CollapsingHeader, Color32, ComboBox, DragValue, EventFilter,
Id, Key, Layout, PointerButton, Rect, ScrollArea, Sense, Shadow, Stroke, StrokeKind, Style, FontId, Frame, Grid, Id, Key, Layout, PointerButton, Rect, ScrollArea, Sense, Shadow,
TextFormat, Ui, UiBuilder, Vec2, Widget, WidgetText, pos2, text::LayoutJob, vec2, Stroke, StrokeKind, Style, TextFormat, Ui, UiBuilder, Vec2, Widget, WidgetText, emath,
pos2, text::LayoutJob, vec2,
}, },
egui_tiles::{ egui_tiles::{
Behavior, Container, Linear, LinearDir, ResizeState, SimplificationOptions, Tile, TileId, Behavior, Container, Linear, LinearDir, ResizeState, SimplificationOptions, Tile, TileId,
@ -68,6 +69,7 @@ enum Pane {
struct CompleteHead { struct CompleteHead {
id: ConnectorId, id: ConnectorId,
name: HeadName, name: HeadName,
pretty_name: Rc<String>,
live_state: ReadOnlyHeadState, live_state: ReadOnlyHeadState,
changed_state: Option<HeadState>, changed_state: Option<HeadState>,
z: u64, z: u64,
@ -123,9 +125,9 @@ pub enum View {
#[derive(Error, Debug)] #[derive(Error, Debug)]
enum HeadTransactionError { enum HeadTransactionError {
#[error("The connector {} has been removed", .0)] #[error("The connector {} has been removed", .0)]
HeadRemoved(ConnectorId), HeadRemoved(Rc<String>),
#[error("The display connected to connector {} has changed", .0)] #[error("The display connected to connector {} has changed", .0)]
MonitorChanged(ConnectorId), MonitorChanged(Rc<String>),
#[error(transparent)] #[error(transparent)]
Backend(#[from] BackendConnectorTransactionError), Backend(#[from] BackendConnectorTransactionError),
} }
@ -811,10 +813,12 @@ impl OutputsPaneInner {
continue; continue;
}; };
let Some(connector) = self.state.connectors.get(&head.id) else { 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 { 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 old = connector.state.borrow().clone();
let mut new = old.clone(); let mut new = old.clone();
@ -838,6 +842,7 @@ impl OutputsPaneInner {
let Some(desired) = &head.changed_state else { let Some(desired) = &head.changed_state else {
continue; continue;
}; };
desired.flush_persistent_state(&self.state);
if let Some(output) = self.state.outputs.get(&head.id) if let Some(output) = self.state.outputs.get(&head.id)
&& let Some(node) = &output.node && let Some(node) = &output.node
{ {
@ -894,6 +899,7 @@ impl OutputsPaneInner {
self.heads.entry(mgrs.name).or_insert_with(|| CompleteHead { self.heads.entry(mgrs.name).or_insert_with(|| CompleteHead {
id: connector.id, id: connector.id,
name: mgrs.name, name: mgrs.name,
pretty_name: connector.name.clone(),
live_state: mgrs.state(), live_state: mgrs.state(),
changed_state: None, changed_state: None,
z: 0, z: 0,
@ -946,36 +952,38 @@ fn show_connector(state: &State, settings: &Settings, head: &mut CompleteHead, u
..Default::default() ..Default::default()
}, },
); );
ui.collapsing(layout_job, |ui| { CollapsingHeader::new(layout_job)
grid(ui, ("settings", head.name), |ui| { .id_salt(("connector", head.name))
let mut diff = false; .show(ui, |ui| {
show_serial_number(ui, m); grid(ui, ("settings", head.name), |ui| {
diff |= show_enablement(ui, m, t); let mut diff = false;
diff |= show_position(ui, m, t); show_serial_number(ui, m);
diff |= show_scale(ui, m, t); diff |= show_enablement(state, ui, m, t);
diff |= show_mode(ui, m, t); diff |= show_position(ui, m, t);
diff |= show_size(ui, m, t); diff |= show_scale(ui, m, t);
diff |= show_transform(ui, m, t); diff |= show_mode(ui, m, t);
diff |= show_brightness(ui, m, t); diff |= show_size(ui, m, t);
diff |= show_color_space(ui, m, t); diff |= show_transform(ui, m, t);
diff |= show_eotf(ui, m, t); diff |= show_brightness(ui, m, t);
diff |= show_format(ui, m, t); diff |= show_color_space(ui, m, t);
diff |= show_tearing(ui, m, t); diff |= show_eotf(ui, m, t);
diff |= show_vrr(ui, m, t); diff |= show_format(ui, m, t);
diff |= show_non_desktop(ui, m, t); diff |= show_tearing(ui, m, t);
diff |= show_blend_space(ui, m, t); diff |= show_vrr(ui, m, t);
diff |= show_use_native_gamut(ui, m, t); diff |= show_non_desktop(state, ui, m, t);
show_native_gamut(ui, m); diff |= show_blend_space(ui, m, t);
diff |= show_cursor_hz(ui, m, t); diff |= show_use_native_gamut(ui, m, t);
show_flip_margin(state, ui, m, t, head.id); show_native_gamut(ui, m);
if diff { diff |= show_cursor_hz(ui, m, t);
let ui = &mut *ui.row(); show_flip_margin(state, ui, m, t, head.id);
ui.label(""); if diff {
ui.label(""); let ui = &mut *ui.row();
ui.label("^ current"); ui.label("");
} ui.label("");
ui.label("^ current");
}
});
}); });
});
} }
fn show_serial_number(ui: &mut Ui, m: &HeadState) { fn show_serial_number(ui: &mut Ui, m: &HeadState) {
@ -986,7 +994,7 @@ fn show_serial_number(ui: &mut Ui, m: &HeadState) {
} }
} }
fn show_enablement(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool { fn show_enablement(state: &State, ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
let ui = &mut *ui.row(); let ui = &mut *ui.row();
grid_label(ui, "Enabled"); grid_label(ui, "Enabled");
let mut v = effective!(m, t).connector_enabled; let mut v = effective!(m, t).connector_enabled;
@ -994,7 +1002,7 @@ fn show_enablement(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> boo
if changed { if changed {
let t = modify!(m, t); let t = modify!(m, t);
t.connector_enabled = v; 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; let diff = v != m.connector_enabled;
if diff { if diff {
@ -1087,15 +1095,33 @@ fn show_mode(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
) )
}; };
if let Some(monitor_info) = &m.monitor_info 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") ComboBox::from_id_salt("modes")
.selected_text(mode_text(mode)) .selected_text(mode_text(mode))
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
for v in &monitor_info.modes { for v in modes {
ui.selectable_value(&mut mode, *v, mode_text(*v)); 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<T: emath::Numeric>(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 { } else {
ui.label(mode_text(mode)); ui.label(mode_text(mode));
} }
@ -1525,7 +1551,7 @@ fn show_vrr(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
diff diff
} }
fn show_non_desktop(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool { fn show_non_desktop(state: &State, ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
{ {
let ui = &mut *ui.row(); let ui = &mut *ui.row();
grid_label(ui, "Non-desktop"); grid_label(ui, "Non-desktop");
@ -1555,7 +1581,7 @@ fn show_non_desktop(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bo
if changed { if changed {
let t = modify!(m, t); let t = modify!(m, t);
t.override_non_desktop = v; 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; let diff = v != m.override_non_desktop;
if diff { if diff {

View file

@ -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<ColorDescription>,
cd: &Rc<ColorDescription>,
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))
}
}

View file

@ -12,16 +12,17 @@ use {
head_management_macros::HeadExts, head_management_macros::HeadExts,
jay_head_manager_session_v1::JayHeadManagerSessionV1, jay_head_v1::JayHeadV1, jay_head_manager_session_v1::JayHeadManagerSessionV1, jay_head_v1::JayHeadV1,
}, },
wl_output::BlendSpace, wl_output::{BlendSpace, PersistentOutputState},
}, },
scale::Scale, scale::Scale,
state::OutputData, state::{OutputData, State},
tree::{OutputNode, TearingMode, Transform, VrrMode}, tree::{OutputNode, TearingMode, Transform, VrrMode},
utils::{copyhashmap::CopyHashMap, hash_map_ext::HashMapExt, rc_eq::RcEq}, utils::{copyhashmap::CopyHashMap, hash_map_ext::HashMapExt, rc_eq::RcEq},
wire::JayHeadManagerSessionV1Id, wire::JayHeadManagerSessionV1Id,
}, },
std::{ std::{
cell::{Cell, Ref, RefCell}, cell::{Cell, Ref, RefCell},
collections::hash_map::Entry,
rc::Rc, rc::Rc,
}, },
thiserror::Error, thiserror::Error,
@ -71,6 +72,7 @@ struct HeadCommon {
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct HeadState { pub struct HeadState {
pub connector_id: ConnectorId,
pub name: RcEq<String>, pub name: RcEq<String>,
pub wl_output: Option<GlobalName>, pub wl_output: Option<GlobalName>,
pub connector_enabled: bool, pub connector_enabled: bool,
@ -98,6 +100,7 @@ pub struct HeadState {
pub blend_space: BlendSpace, pub blend_space: BlendSpace,
pub use_native_gamut: bool, pub use_native_gamut: bool,
pub vrr_cursor_hz: Option<f64>, pub vrr_cursor_hz: Option<f64>,
pub persistent_state: Option<RcEq<PersistentOutputState>>,
} }
pub struct ReadOnlyHeadState { pub struct ReadOnlyHeadState {
@ -111,7 +114,7 @@ impl ReadOnlyHeadState {
} }
impl HeadState { impl HeadState {
pub fn update_in_compositor_space(&mut self, wl_output: Option<GlobalName>) { pub fn update_in_compositor_space(&mut self, state: &State, wl_output: Option<GlobalName>) {
self.in_compositor_space = false; self.in_compositor_space = false;
self.wl_output = None; self.wl_output = None;
if !self.connector_enabled { if !self.connector_enabled {
@ -128,6 +131,26 @@ impl HeadState {
} }
self.in_compositor_space = true; self.in_compositor_space = true;
self.wl_output = wl_output; 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) { pub fn update_size(&mut self) {
@ -135,6 +158,18 @@ impl HeadState {
OutputNode::calculate_extents_(self.mode, self.transform, self.scale, self.position) OutputNode::calculate_extents_(self.mode, self.transform, self.scale, self.position)
.size(); .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 { 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(); let state = &mut *self.state.borrow_mut();
state.connected = true; state.connected = true;
state.monitor_info = Some(RcEq(output.monitor_info.clone())); state.monitor_info = Some(RcEq(output.monitor_info.clone()));
state.persistent_state = None;
state.inherent_non_desktop = output.monitor_info.non_desktop; state.inherent_non_desktop = output.monitor_info.non_desktop;
state.update_in_compositor_space(output.node.as_ref().map(|n| n.global.name)); state.update_in_compositor_space(s, 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();
}
for head in self.managers.lock().values() { for head in self.managers.lock().values() {
skip_in_transaction!(head); skip_in_transaction!(head);
if let Some(ext) = &head.ext.connector_info_v1 { 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(); let state = &mut *self.state.borrow_mut();
state.connected = false; state.connected = false;
state.monitor_info = None; 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() { for head in self.managers.lock().values() {
skip_in_transaction!(head); skip_in_transaction!(head);
if let Some(ext) = &head.ext.compositor_space_info_v1 { 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(); let state = &mut *self.state.borrow_mut();
state.connector_enabled = enabled; 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() { for head in self.managers.lock().values() {
skip_in_transaction!(head); skip_in_transaction!(head);
if let Some(ext) = &head.ext.compositor_space_info_v1 { if let Some(ext) = &head.ext.compositor_space_info_v1 {

View file

@ -41,8 +41,10 @@ impl HeadName {
pub(in super::super) fn send_modes(&self, state: &HeadState) { pub(in super::super) fn send_modes(&self, state: &HeadState) {
self.client.event(Reset { self_id: self.id }); self.client.event(Reset { self_id: self.id });
if let Some(mi) = &state.monitor_info { if let Some(mi) = &state.monitor_info
for mode in &mi.modes { && let Some(modes) = &mi.modes
{
for mode in modes {
self.client.event(Mode { self.client.event(Mode {
self_id: self.id, self_id: self.id,
width: mode.width, width: mode.width,
@ -73,7 +75,8 @@ impl JayHeadExtModeSetterV1RequestHandler for HeadName {
.borrow() .borrow()
.monitor_info .monitor_info
.as_deref() .as_deref()
.map(|i| i.modes.len()) .and_then(|i| i.modes.as_ref())
.map(|m| m.len())
.unwrap_or(0); .unwrap_or(0);
let idx = req.idx as usize; let idx = req.idx as usize;
if idx >= num_modes { if idx >= num_modes {

View file

@ -43,7 +43,7 @@ impl HeadName {
pub(in super::super) fn send_info(&self, state: &HeadState) { pub(in super::super) fn send_info(&self, state: &HeadState) {
self.send_reset(); self.send_reset();
if let Some(mi) = &state.monitor_info { 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_mode(mode);
} }
self.send_manufacturer(&mi.output_id.manufacturer); self.send_manufacturer(&mi.output_id.manufacturer);

View file

@ -404,7 +404,7 @@ impl JayHeadManagerSessionV1RequestHandler for JayHeadManagerSessionV1 {
} }
HeadOp::SetConnectorEnabled(enabled) => { HeadOp::SetConnectorEnabled(enabled) => {
state.connector_enabled = 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_FULL;
to_send |= COMPOSITOR_SPACE_INFO_ENABLED; to_send |= COMPOSITOR_SPACE_INFO_ENABLED;
to_send |= CORE_INFO; to_send |= CORE_INFO;
@ -422,14 +422,20 @@ impl JayHeadManagerSessionV1RequestHandler for JayHeadManagerSessionV1 {
to_send |= COMPOSITOR_SPACE_INFO_SIZE; to_send |= COMPOSITOR_SPACE_INFO_SIZE;
} }
HeadOp::SetMode(i) => { 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(); state.update_size();
to_send |= MODE_INFO; to_send |= MODE_INFO;
to_send |= COMPOSITOR_SPACE_INFO_SIZE; to_send |= COMPOSITOR_SPACE_INFO_SIZE;
} }
HeadOp::SetNonDesktopOverride(m) => { HeadOp::SetNonDesktopOverride(m) => {
state.override_non_desktop = 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 |= COMPOSITOR_SPACE_INFO_FULL;
to_send |= CORE_INFO; to_send |= CORE_INFO;
to_send |= NON_DESKTOP_INFO; to_send |= NON_DESKTOP_INFO;
@ -551,6 +557,7 @@ impl JayHeadManagerSessionV1RequestHandler for JayHeadManagerSessionV1 {
} }
for head in self.heads.lock().values() { for head in self.heads.lock().values() {
let desired = &*head.common.transaction_state.borrow(); 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) if let Some(output) = self.client.state.outputs.get(&head.common.id)
&& let Some(node) = &output.node && let Some(node) = &output.node
{ {

View file

@ -78,7 +78,7 @@ global_base!(JayCompositorGlobal, JayCompositor, JayCompositorError);
impl Global for JayCompositorGlobal { impl Global for JayCompositorGlobal {
fn version(&self) -> u32 { fn version(&self) -> u32 {
28 29
} }
fn required_caps(&self) -> ClientCaps { fn required_caps(&self) -> ClientCaps {

View file

@ -16,7 +16,7 @@ use {
}, },
jay_config::video::{TearingMode as ConfigTearingMode, VrrMode as ConfigVrrMode}, jay_config::video::{TearingMode as ConfigTearingMode, VrrMode as ConfigVrrMode},
linearize::LinearizeExt, linearize::LinearizeExt,
std::rc::Rc, std::{rc::Rc, slice},
thiserror::Error, thiserror::Error,
}; };
@ -36,6 +36,7 @@ const COLORIMETRY_SINCE: Version = Version(15);
const BRIGHTNESS_SINCE: Version = Version(16); const BRIGHTNESS_SINCE: Version = Version(16);
const BLEND_SPACE_SINCE: Version = Version(21); const BLEND_SPACE_SINCE: Version = Version(21);
const NATIVE_GAMUT_SINCE: Version = Version(23); const NATIVE_GAMUT_SINCE: Version = Version(23);
const ARBITRARY_MODES_SINCE: Version = Version(29);
impl JayRandr { impl JayRandr {
pub fn new(id: JayRandrId, client: &Rc<Client>, version: Version) -> Self { pub fn new(id: JayRandrId, client: &Rc<Client>, version: Version) -> Self {
@ -162,7 +163,11 @@ impl JayRandr {
} }
} }
let current_mode = global.mode.get(); let current_mode = global.mode.get();
for mode in &global.modes { for mode in global
.modes
.as_deref()
.unwrap_or(slice::from_ref(&current_mode))
{
self.client.event(Mode { self.client.event(Mode {
self_id: self.id, self_id: self.id,
width: mode.width, width: mode.width,
@ -232,6 +237,9 @@ impl JayRandr {
self.client.event(UseNativeGamut { self_id: self.id }); 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) { fn send_error(&self, msg: &str) {

View file

@ -71,7 +71,7 @@ pub struct WlOutputGlobal {
pub output_id: Rc<OutputId>, pub output_id: Rc<OutputId>,
pub mode: Cell<backend::Mode>, pub mode: Cell<backend::Mode>,
pub refresh_nsec: Cell<u64>, pub refresh_nsec: Cell<u64>,
pub modes: Vec<backend::Mode>, pub modes: Option<Vec<backend::Mode>>,
pub formats: CloneCell<Rc<Vec<&'static Format>>>, pub formats: CloneCell<Rc<Vec<&'static Format>>>,
pub format: Cell<&'static Format>, pub format: Cell<&'static Format>,
pub width_mm: i32, pub width_mm: i32,
@ -199,7 +199,7 @@ impl WlOutputGlobal {
name: GlobalName, name: GlobalName,
state: &Rc<State>, state: &Rc<State>,
connector: &Rc<ConnectorData>, connector: &Rc<ConnectorData>,
modes: Vec<backend::Mode>, modes: Option<Vec<backend::Mode>>,
width_mm: i32, width_mm: i32,
height_mm: i32, height_mm: i32,
output_id: &Rc<OutputId>, output_id: &Rc<OutputId>,

View file

@ -12,9 +12,9 @@ use {
scale, scale,
state::OutputData, state::OutputData,
tree::{self, VrrMode}, tree::{self, VrrMode},
utils::copyhashmap::CopyHashMap,
wire::{ZwlrOutputHeadV1Id, zwlr_output_head_v1::*}, wire::{ZwlrOutputHeadV1Id, zwlr_output_head_v1::*},
}, },
ahash::AHashMap,
std::rc::Rc, std::rc::Rc,
thiserror::Error, thiserror::Error,
}; };
@ -44,7 +44,7 @@ pub struct ZwlrOutputHeadV1 {
pub(super) manager: Rc<ZwlrOutputManagerV1>, pub(super) manager: Rc<ZwlrOutputManagerV1>,
pub(super) head_id: WlrOutputHeadId, pub(super) head_id: WlrOutputHeadId,
pub(super) connector_id: ConnectorId, pub(super) connector_id: ConnectorId,
pub(super) modes: AHashMap<backend::Mode, Rc<ZwlrOutputModeV1>>, pub(super) modes: CopyHashMap<backend::Mode, Rc<ZwlrOutputModeV1>>,
} }
impl ZwlrOutputHeadV1 { impl ZwlrOutputHeadV1 {
@ -177,13 +177,21 @@ impl ZwlrOutputHeadV1 {
} }
pub fn handle_mode_change(&self, new: backend::Mode) { 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; return;
}; };
if mode.destroyed.get() { if mode.destroyed.get() {
return; return;
} }
self.send_current_mode(mode); self.send_current_mode(&mode);
self.manager.schedule_done(); self.manager.schedule_done();
} }
@ -207,7 +215,7 @@ impl ZwlrOutputHeadV1 {
pub fn handle_disconnected(&self) { pub fn handle_disconnected(&self) {
self.send_finished(); self.send_finished();
for mode in self.modes.values() { for mode in self.modes.lock().values() {
if !mode.destroyed.get() { if !mode.destroyed.get() {
mode.send_finished(); mode.send_finished();
} }

View file

@ -1,23 +1,24 @@
use { use {
crate::{ crate::{
backend::Mode,
client::{CAP_HEAD_MANAGER, Client, ClientCaps, ClientError}, client::{CAP_HEAD_MANAGER, Client, ClientCaps, ClientError},
globals::{Global, GlobalName}, globals::{Global, GlobalName},
ifs::wlr_output_manager::{ ifs::wlr_output_manager::{
zwlr_output_configuration_v1::ZwlrOutputConfigurationV1, zwlr_output_configuration_v1::ZwlrOutputConfigurationV1,
zwlr_output_head_v1::{ 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, zwlr_output_mode_v1::ZwlrOutputModeV1,
}, },
leaks::Tracker, leaks::Tracker,
object::{Object, Version}, object::{Object, Version},
state::OutputData, state::OutputData,
utils::numcell::NumCell, utils::{copyhashmap::CopyHashMap, numcell::NumCell},
wire::{ZwlrOutputManagerV1Id, zwlr_output_manager_v1::*}, wire::{ZwlrOutputManagerV1Id, zwlr_output_manager_v1::*},
}, },
ahash::AHashMap,
isnt::std_1::string::IsntStringExt, isnt::std_1::string::IsntStringExt,
std::{cell::Cell, rc::Rc}, std::{cell::Cell, rc::Rc, slice},
thiserror::Error, thiserror::Error,
}; };
@ -134,38 +135,27 @@ impl ZwlrOutputManagerV1 {
let state_mode = output.connector.state.borrow().mode; let state_mode = output.connector.state.borrow().mode;
let head_id = self.client.state.wlr_output_managers.head_ids.next(); let head_id = self.client.state.wlr_output_managers.head_ids.next();
let mut modes_list = vec![]; let mut modes_list = vec![];
let mut modes = AHashMap::new(); let modes = CopyHashMap::new();
let mut have_current = false; let mut have_current = false;
for (idx, mode) in mi.modes.iter().enumerate() { for (idx, mode) in mi
if modes.contains_key(mode) { .modes
.as_deref()
.unwrap_or(slice::from_ref(&state_mode))
.iter()
.enumerate()
{
if modes.contains(mode) {
continue; continue;
} }
let current = !have_current && *mode == state_mode; let current = !have_current && *mode == state_mode;
if current { if current {
have_current = true; have_current = true;
} }
let id = match self.client.new_id() { let Some(output_mode) = self.create_mode(head_id, mode, idx == 0, current) else {
Ok(id) => id, return;
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_list.push(output_mode.clone());
modes.insert(*mode, output_mode); modes.set(*mode, output_mode);
} }
let head = Rc::new(ZwlrOutputHeadV1 { let head = Rc::new(ZwlrOutputHeadV1 {
id, id,
@ -244,6 +234,36 @@ impl ZwlrOutputManagerV1 {
.queue .queue
.push(self.clone()); .push(self.clone());
} }
pub(super) fn create_mode(
self: &Rc<Self>,
head_id: WlrOutputHeadId,
mode: &Mode,
preferred: bool,
initial_current: bool,
) -> Option<Rc<ZwlrOutputModeV1>> {
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!( global_base!(

View file

@ -37,7 +37,15 @@ use {
}, },
ahash::AHashMap, ahash::AHashMap,
bstr::ByteSlice, 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, thiserror::Error,
uapi::c, uapi::c,
}; };
@ -75,6 +83,24 @@ pub struct TestBackend {
impl TestBackend { impl TestBackend {
pub fn new(state: &Rc<State>, future: TestFuture) -> Self { pub fn new(state: &Rc<State>, future: TestFuture) -> Self {
state.set_backend_idle(false); 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 { let default_connector = Rc::new(TestConnector {
id: state.connector_ids.next(), id: state.connector_ids.next(),
kernel_id: ConnectorKernelId { kernel_id: ConnectorKernelId {
@ -85,6 +111,7 @@ impl TestBackend {
feedback: Default::default(), feedback: Default::default(),
idle: Default::default(), idle: Default::default(),
damage_calls: NumCell::new(0), damage_calls: NumCell::new(0),
state: RefCell::new(bcs.clone()),
}); });
let default_mouse = Rc::new(TestBackendMouse { let default_mouse = Rc::new(TestBackendMouse {
common: TestInputDeviceCommon { common: TestInputDeviceCommon {
@ -120,13 +147,8 @@ impl TestBackend {
state: state.clone(), state: state.clone(),
}, },
}); });
let mode = Mode {
width: 800,
height: 600,
refresh_rate_millihz: 60_000,
};
let default_monitor_info = MonitorInfo { let default_monitor_info = MonitorInfo {
modes: vec![mode], modes: Some(vec![mode]),
output_id: Rc::new(OutputId { output_id: Rc::new(OutputId {
connector: None, connector: None,
manufacturer: "jay".to_string(), manufacturer: "jay".to_string(),
@ -142,19 +164,7 @@ impl TestBackend {
color_spaces: vec![], color_spaces: vec![],
primaries: Primaries::SRGB, primaries: Primaries::SRGB,
luminance: None, luminance: None,
state: BackendConnectorState { state: bcs,
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(),
},
}; };
Self { Self {
state: state.clone(), state: state.clone(),
@ -325,6 +335,7 @@ pub struct TestConnector {
pub feedback: CloneCell<Option<Rc<DrmFeedback>>>, pub feedback: CloneCell<Option<Rc<DrmFeedback>>>,
pub idle: TEEH<bool>, pub idle: TEEH<bool>,
pub damage_calls: NumCell<u32>, pub damage_calls: NumCell<u32>,
pub state: RefCell<BackendConnectorState>,
} }
impl Connector for TestConnector { impl Connector for TestConnector {
@ -357,6 +368,10 @@ impl Connector for TestConnector {
true true
} }
fn state(&self) -> BackendConnectorState {
self.state.borrow().clone()
}
fn drm_feedback(&self) -> Option<Rc<DrmFeedback>> { fn drm_feedback(&self) -> Option<Rc<DrmFeedback>> {
self.feedback.get() self.feedback.get()
} }
@ -404,6 +419,7 @@ impl BackendPreparedConnectorTransaction for TestBackendTransaction {
self: Box<Self>, self: Box<Self>,
) -> Result<Box<dyn BackendAppliedConnectorTransaction>, BackendConnectorTransactionError> { ) -> Result<Box<dyn BackendAppliedConnectorTransaction>, BackendConnectorTransactionError> {
for (c, s) in self.connectors.values() { for (c, s) in self.connectors.values() {
*c.state.borrow_mut() = s.clone();
c.idle.push(!s.active); c.idle.push(!s.active);
} }
Ok(self) Ok(self)

View file

@ -10,7 +10,7 @@ use {
utils::numcell::NumCell, utils::numcell::NumCell,
video::drm::ConnectorType, video::drm::ConnectorType,
}, },
std::rc::Rc, std::{cell::RefCell, rc::Rc},
}; };
testcase!(); testcase!();
@ -27,6 +27,19 @@ async fn test(run: Rc<TestRun>) -> TestResult {
bail!("no dummy output"); 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 { let new_connector = Rc::new(TestConnector {
id: run.state.connector_ids.next(), id: run.state.connector_ids.next(),
kernel_id: ConnectorKernelId { kernel_id: ConnectorKernelId {
@ -37,9 +50,10 @@ async fn test(run: Rc<TestRun>) -> TestResult {
feedback: Default::default(), feedback: Default::default(),
idle: Default::default(), idle: Default::default(),
damage_calls: NumCell::new(0), damage_calls: NumCell::new(0),
state: RefCell::new(bcs.clone()),
}); });
let new_monitor_info = MonitorInfo { let new_monitor_info = MonitorInfo {
modes: vec![], modes: Some(vec![]),
output_id: Rc::new(OutputId { output_id: Rc::new(OutputId {
connector: None, connector: None,
manufacturer: "jay".to_string(), manufacturer: "jay".to_string(),
@ -55,19 +69,7 @@ async fn test(run: Rc<TestRun>) -> TestResult {
color_spaces: vec![], color_spaces: vec![],
primaries: Primaries::SRGB, primaries: Primaries::SRGB,
luminance: None, luminance: None,
state: BackendConnectorState { state: bcs,
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(),
},
}; };
run.backend run.backend
.state .state

View file

@ -62,7 +62,7 @@ use {
jay_seat_events::JaySeatEvents, jay_seat_events::JaySeatEvents,
jay_workspace_watcher::JayWorkspaceWatcher, jay_workspace_watcher::JayWorkspaceWatcher,
wl_buffer::WlBuffer, wl_buffer::WlBuffer,
wl_output::{OutputGlobalOpt, OutputId, PersistentOutputState}, wl_output::{BlendSpace, OutputGlobalOpt, OutputId, PersistentOutputState},
wl_seat::{ wl_seat::{
PhysicalKeyboardId, PhysicalKeyboardIds, PositionHintRequest, SeatIds, PhysicalKeyboardId, PhysicalKeyboardIds, PositionHintRequest, SeatIds,
WlSeatGlobal, WlSeatGlobal,
@ -504,7 +504,7 @@ impl ConnectorData {
}}; }};
} }
if b!(old.enabled != s.enabled) { 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) { if b!(old.active != s.active) {
self.head_managers.handle_active_change(s.active); self.head_managers.handle_active_change(s.active);
@ -1961,6 +1961,43 @@ impl State {
colored.field(&self.theme).set(v); colored.field(&self.theme).set(v);
self.colors_changed(); self.colors_changed();
} }
pub fn ensure_persistent_output_state(
&self,
output_id: &Rc<OutputId>,
) -> Rc<PersistentOutputState> {
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<PersistentOutputState> {
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)] #[derive(Debug, Error)]

View file

@ -1,16 +1,12 @@
use { use {
crate::{ crate::{
backend::{ backend::{Connector, ConnectorEvent, ConnectorId, MonitorInfo},
BackendConnectorState, BackendConnectorStateSerial, Connector, ConnectorEvent,
ConnectorId, MonitorInfo,
},
control_center::CCI_OUTPUTS, control_center::CCI_OUTPUTS,
format::XRGB8888,
globals::GlobalName, globals::GlobalName,
ifs::{ ifs::{
head_management::{HeadManagers, HeadState}, head_management::{HeadManagers, HeadState},
jay_tray_v1::JayTrayV1Global, jay_tray_v1::JayTrayV1Global,
wl_output::{BlendSpace, PersistentOutputState, WlOutputGlobal}, wl_output::{BlendSpace, WlOutputGlobal},
}, },
output_schedule::OutputSchedule, output_schedule::OutputSchedule,
state::{ConnectorData, OutputData, State}, state::{ConnectorData, OutputData, State},
@ -35,22 +31,11 @@ pub fn handle(state: &Rc<State>, connector: &Rc<dyn Connector>) {
_ => panic!("connector's drm device does not exist"), _ => panic!("connector's drm device does not exist"),
}; };
} }
let backend_state = BackendConnectorState { let backend_state = connector.state();
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 id = connector.id(); let id = connector.id();
let name = Rc::new(connector.kernel_id().to_string()); let name = Rc::new(connector.name());
let head_state = HeadState { let head_state = HeadState {
connector_id: id,
name: RcEq(name.clone()), name: RcEq(name.clone()),
position: (0, 0), position: (0, 0),
size: (0, 0), size: (0, 0),
@ -61,7 +46,7 @@ pub fn handle(state: &Rc<State>, connector: &Rc<dyn Connector>) {
wl_output: None, wl_output: None,
connector_enabled: backend_state.enabled, connector_enabled: backend_state.enabled,
in_compositor_space: false, in_compositor_space: false,
mode: Default::default(), mode: backend_state.mode,
monitor_info: None, monitor_info: None,
inherent_non_desktop: false, inherent_non_desktop: false,
override_non_desktop: backend_state.non_desktop_override, override_non_desktop: backend_state.non_desktop_override,
@ -78,6 +63,7 @@ pub fn handle(state: &Rc<State>, connector: &Rc<dyn Connector>) {
blend_space: BlendSpace::Srgb, blend_space: BlendSpace::Srgb,
use_native_gamut: false, use_native_gamut: false,
vrr_cursor_hz: None, vrr_cursor_hz: None,
persistent_state: None,
}; };
let data = Rc::new(ConnectorData { let data = Rc::new(ConnectorData {
id, id,
@ -153,7 +139,11 @@ impl ConnectorHandler {
} }
async fn handle_connected(&self, info: MonitorInfo) { 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.connected.set(true);
self.data.set_state(&self.state, info.state.clone()); self.data.set_state(&self.state, info.state.clone());
*self.data.description.borrow_mut() = create_description(&info); *self.data.description.borrow_mut() = create_description(&info);
@ -164,45 +154,19 @@ impl ConnectorHandler {
self.handle_desktop_connected(info, name).await; self.handle_desktop_connected(info, name).await;
} }
self.data.connected.set(false); 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); self.state.trigger_cci(CCI_OUTPUTS);
for head in self.data.wlr_output_heads.lock().drain_values() { for head in self.data.wlr_output_heads.lock().drain_values() {
head.handle_disconnected(); 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) { async fn handle_desktop_connected(&self, info: MonitorInfo, name: GlobalName) {
let output_id = info.output_id.clone(); let output_id = info.output_id.clone();
let desired_state = match self.state.persistent_output_states.get(&output_id) { let desired_state = self.state.ensure_persistent_output_state(&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 global = Rc::new(WlOutputGlobal::new( let global = Rc::new(WlOutputGlobal::new(
name, name,
&self.state, &self.state,
@ -339,7 +303,7 @@ impl ConnectorHandler {
self.state.workspace_managers.announce_output(&on); self.state.workspace_managers.announce_output(&on);
self.data self.data
.head_managers .head_managers
.handle_output_connected(&output_data); .handle_output_connected(&self.state, &output_data);
self.state.trigger_cci(CCI_OUTPUTS); self.state.trigger_cci(CCI_OUTPUTS);
self.state.wlr_output_managers.announce_head(&output_data); self.state.wlr_output_managers.announce_head(&output_data);
'outer: loop { 'outer: loop {
@ -466,7 +430,7 @@ impl ConnectorHandler {
} }
self.data self.data
.head_managers .head_managers
.handle_output_connected(&output_data); .handle_output_connected(&self.state, &output_data);
self.state.trigger_cci(CCI_OUTPUTS); self.state.trigger_cci(CCI_OUTPUTS);
self.state.wlr_output_managers.announce_head(&output_data); self.state.wlr_output_managers.announce_head(&output_data);
'outer: loop { 'outer: loop {

View file

@ -334,7 +334,7 @@ impl ToolClient {
self_id: s.registry, self_id: s.registry,
name: s.jay_compositor.0, name: s.jay_compositor.0,
interface: JayCompositor.name(), interface: JayCompositor.name(),
version: s.jay_compositor.1.min(28), version: s.jay_compositor.1.min(29),
id: id.into(), id: id.into(),
}); });
self.jay_compositor.set(Some(id)); self.jay_compositor.set(Some(id));

View file

@ -830,11 +830,20 @@ impl Output {
Some(rr) => m.refresh_rate() as f64 / 1000.0 == rr, Some(rr) => m.refresh_rate() as f64 / 1000.0 == rr,
} }
}); });
match m { 'set_mode: {
None => { 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()); log::warn!("Output {} does not support mode {mode}", c.name());
} break 'set_mode;
Some(m) => c.set_mode(m.width(), m.height(), Some(m.refresh_rate())), };
c.set_mode(w, h, Some(mhz));
} }
} }
if let Some(vrr) = &self.vrr { if let Some(vrr) = &self.vrr {

View file

@ -229,3 +229,6 @@ event native_gamut (since = 23) {
event use_native_gamut (since = 23) { event use_native_gamut (since = 23) {
} }
event arbitrary_modes (since = 29) {
}