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
}
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 });
@ -1191,6 +1197,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 });

View file

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

View file

@ -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.
@ -446,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 `<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.
pub trait ToConnectorId {
fn to_connector_id(&self) -> Result<(ConnectorType, u32), String>;

View file

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

View file

@ -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(),
}
}
}

View file

@ -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<DrmFramebuffer>,
tex: Rc<dyn GfxTexture>,
@ -643,122 +632,17 @@ impl MetalConnector {
blend_cd: &Rc<ColorDescription>,
cd: &Rc<ColorDescription>,
) -> Option<DirectScanoutData> {
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 {

View file

@ -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
}
@ -1959,7 +1963,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 _,

View file

@ -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(),
@ -1113,6 +1113,10 @@ impl Connector for XOutput {
true
}
fn state(&self) -> BackendConnectorState {
self.state.borrow().clone()
}
fn transaction_type(&self) -> Box<dyn BackendConnectorTransactionTypeDyn> {
Box::new(XTransactionType)
}

View file

@ -527,6 +527,7 @@ struct Output {
pub blend_space: Option<String>,
pub native_gamut: Option<Primaries>,
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()
}

View file

@ -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,29 +675,13 @@ fn create_dummy_output(state: &Rc<State>) {
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<dyn Connector>;
let backend_state = connector.state();
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),
@ -725,6 +709,7 @@ fn create_dummy_output(state: &Rc<State>) {
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,
@ -754,7 +739,7 @@ fn create_dummy_output(state: &Rc<State>) {
state.globals.name(),
state,
&connector_data,
Vec::new(),
Some(Vec::new()),
0,
0,
&output_id,

View file

@ -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 {
@ -1580,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
@ -3328,6 +3353,10 @@ 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")?,
ClientMessage::GetConnectorByName { name } => self.handle_get_connector_by_name(name),
}
Ok(())
}

View file

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

View file

@ -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, 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<String>,
live_state: ReadOnlyHeadState,
changed_state: Option<HeadState>,
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<String>),
#[error("The display connected to connector {} has changed", .0)]
MonitorChanged(ConnectorId),
MonitorChanged(Rc<String>),
#[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();
@ -838,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
{
@ -894,6 +899,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 +952,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(state, 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(state, 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) {
@ -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();
grid_label(ui, "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 {
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 {
@ -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
&& 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<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 {
ui.label(mode_text(mode));
}
@ -1525,7 +1551,7 @@ fn show_vrr(ui: &mut Ui, m: &HeadState, t: &mut Option<HeadState>) -> bool {
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();
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 {
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 {

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,
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<String>,
pub wl_output: Option<GlobalName>,
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<f64>,
pub persistent_state: Option<RcEq<PersistentOutputState>>,
}
pub struct ReadOnlyHeadState {
@ -111,7 +114,7 @@ impl ReadOnlyHeadState {
}
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.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 {

View file

@ -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 {

View file

@ -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);

View file

@ -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;
@ -422,14 +422,20 @@ 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;
}
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;
@ -551,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
{

View file

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

View file

@ -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<Client>, 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(&current_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) {

View file

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

View file

@ -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<ZwlrOutputManagerV1>,
pub(super) head_id: WlrOutputHeadId,
pub(super) connector_id: ConnectorId,
pub(super) modes: AHashMap<backend::Mode, Rc<ZwlrOutputModeV1>>,
pub(super) modes: CopyHashMap<backend::Mode, Rc<ZwlrOutputModeV1>>,
}
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();
}

View file

@ -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<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!(

View file

@ -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<State>, 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,13 +147,8 @@ impl TestBackend {
state: state.clone(),
},
});
let mode = Mode {
width: 800,
height: 600,
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(),
@ -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<Option<Rc<DrmFeedback>>>,
pub idle: TEEH<bool>,
pub damage_calls: NumCell<u32>,
pub state: RefCell<BackendConnectorState>,
}
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<Rc<DrmFeedback>> {
self.feedback.get()
}
@ -404,6 +419,7 @@ impl BackendPreparedConnectorTransaction for TestBackendTransaction {
self: Box<Self>,
) -> Result<Box<dyn BackendAppliedConnectorTransaction>, BackendConnectorTransactionError> {
for (c, s) in self.connectors.values() {
*c.state.borrow_mut() = s.clone();
c.idle.push(!s.active);
}
Ok(self)

View file

@ -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<TestRun>) -> 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,9 +50,10 @@ async fn test(run: Rc<TestRun>) -> TestResult {
feedback: Default::default(),
idle: Default::default(),
damage_calls: NumCell::new(0),
state: RefCell::new(bcs.clone()),
});
let new_monitor_info = MonitorInfo {
modes: vec![],
modes: Some(vec![]),
output_id: Rc::new(OutputId {
connector: None,
manufacturer: "jay".to_string(),
@ -55,19 +69,7 @@ async fn test(run: Rc<TestRun>) -> 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

View file

@ -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<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)]

View file

@ -1,16 +1,12 @@
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},
jay_tray_v1::JayTrayV1Global,
wl_output::{BlendSpace, PersistentOutputState, WlOutputGlobal},
wl_output::{BlendSpace, WlOutputGlobal},
},
output_schedule::OutputSchedule,
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"),
};
}
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 name = Rc::new(connector.name());
let head_state = HeadState {
connector_id: id,
name: RcEq(name.clone()),
position: (0, 0),
size: (0, 0),
@ -61,7 +46,7 @@ pub fn handle(state: &Rc<State>, connector: &Rc<dyn Connector>) {
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,
@ -78,6 +63,7 @@ pub fn handle(state: &Rc<State>, connector: &Rc<dyn Connector>) {
blend_space: BlendSpace::Srgb,
use_native_gamut: false,
vrr_cursor_hz: None,
persistent_state: None,
};
let data = Rc::new(ConnectorData {
id,
@ -153,7 +139,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);
@ -164,45 +154,19 @@ 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();
}
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) {
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,
@ -339,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 {
@ -466,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 {

View file

@ -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));

View file

@ -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 {

View file

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