feat: configure and inspect DRM outputs

This commit is contained in:
Samuel Onoja committed 2026-09-25 04:32:19 +01:00
1 parent d46becfd60
commit 07de17a399
7 files changed
+723 -48

No files matched your search

+271 -45
View File
@@ -41,6 +41,7 @@ use smithay::{
use crate::{
Ferese,
config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform},
metrics::RenderMetrics,
winit::{animated_window_elements, cursorless_window_elements, redraw_output},
};
@@ -50,6 +51,29 @@ pub struct DirectBackendState {
pub active: bool,
devices: HashMap<DrmNode, DirectDevice>,
presentation: HashMap<(DrmNode, crtc::Handle), PresentationClock>,
pub(crate) connected_outputs: Vec<ConnectedOutputInfo>,
}
#[derive(Clone, Debug)]
pub(crate) struct ConnectedOutputInfo {
pub connector: String,
pub identity: String,
pub enabled: bool,
pub profile: Option<String>,
pub physical_size: Option<(u32, u32)>,
pub current_mode: Option<ConnectedModeInfo>,
pub available_modes: Vec<ConnectedModeInfo>,
pub scale: f64,
pub transform: OutputTransform,
pub configured_position: Option<[i32; 2]>,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct ConnectedModeInfo {
pub width: u16,
pub height: u16,
pub refresh_millihertz: i32,
pub preferred: bool,
}
#[derive(Default)]
@@ -70,6 +94,7 @@ struct DirectDevice {
struct DirectOutput {
connector: connector::Handle,
mode: DrmMode,
settings: OutputSettings,
output: Output,
global: GlobalId,
surface: GbmBufferedSurface<GbmAllocator<DrmDeviceFd>, OutputPresentationFeedback>,
@@ -78,6 +103,18 @@ struct DirectOutput {
frame_pending: bool,
}
struct OutputSelection {
connector: connector::Info,
crtc: crtc::Handle,
mode: DrmMode,
settings: OutputSettings,
}
struct OutputScan {
selections: Vec<OutputSelection>,
connected_outputs: Vec<ConnectedOutputInfo>,
}
pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<(), Box<dyn Error>> {
let (session, notifier) = LibSeatSession::new()?;
let seat_name = session.seat();
@@ -106,6 +143,7 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
active: session_active,
devices: HashMap::new(),
presentation: HashMap::new(),
connected_outputs: Vec::new(),
});
open_primary_device(event_loop, state, &primary_path)?;
@@ -242,10 +280,30 @@ fn open_primary_device(
state
.dmabuf_state
.create_global::<Ferese>(&display_handle, dmabuf_formats);
let selections = select_outputs(&drm)?;
let scan = select_outputs(&drm, &state.output_profiles)?;
if scan.selections.is_empty() {
return Err(io::Error::new(
io::ErrorKind::NotFound,
"no connected enabled DRM output with a usable CRTC",
)
.into());
}
state
.direct_backend
.as_mut()
.expect("direct backend state remains initialized")
.connected_outputs = scan.connected_outputs;
let mut outputs = HashMap::new();
for (connector, crtc, mode) in selections {
let output = create_direct_output(state, &mut drm, &gbm, &renderer, connector, crtc, mode)?;
for selection in scan.selections {
let OutputSelection {
connector,
crtc,
mode,
settings,
} = selection;
let output = create_direct_output(
state, &mut drm, &gbm, &renderer, connector, crtc, mode, &settings,
)?;
state
.direct_backend
.as_mut()
@@ -505,25 +563,30 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
return;
};
let selections = match select_outputs(&device.drm) {
Ok(selections) => selections,
Err(error) if error.kind() == io::ErrorKind::NotFound => Vec::new(),
let scan = match select_outputs(&device.drm, &state.output_profiles) {
Ok(scan) => scan,
Err(error) => {
tracing::error!(?node, %error, "failed to scan DRM connectors");
restore_device(state, node, device);
return;
}
};
let mut selections = selections
if let Some(backend) = state.direct_backend.as_mut() {
backend.connected_outputs = scan.connected_outputs;
}
let mut selections = scan
.selections
.into_iter()
.map(|selection @ (_, crtc, _)| (crtc, selection))
.map(|selection| (selection.crtc, selection))
.collect::<HashMap<_, _>>();
let existing = device.outputs.keys().copied().collect::<Vec<_>>();
for crtc in existing {
let unchanged = device.outputs.get(&crtc).is_some_and(|output| {
selections.get(&crtc).is_some_and(|(connector, _, mode)| {
connector.handle() == output.connector && *mode == output.mode
selections.get(&crtc).is_some_and(|selection| {
selection.connector.handle() == output.connector
&& selection.mode == output.mode
&& selection.settings == output.settings
})
});
if unchanged {
@@ -541,7 +604,13 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
}
}
for (_, (connector, crtc, mode)) in selections {
for (_, selection) in selections {
let OutputSelection {
connector,
crtc,
mode,
settings,
} = selection;
match create_direct_output(
state,
&mut device.drm,
@@ -550,6 +619,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
connector,
crtc,
mode,
&settings,
) {
Ok(output) => {
state
@@ -587,6 +657,9 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
backend.presentation.remove(&(node, crtc));
}
}
if let Some(backend) = state.direct_backend.as_mut() {
backend.connected_outputs.clear();
}
tracing::info!(?node, "removed DRM device");
}
@@ -632,23 +705,72 @@ fn send_frame_callbacks(state: &mut Ferese, output: &Output) {
}
}
fn select_outputs(drm: &DrmDevice) -> io::Result<Vec<(connector::Info, crtc::Handle, DrmMode)>> {
fn select_outputs(drm: &DrmDevice, profiles: &[OutputProfile]) -> io::Result<OutputScan> {
let resources = drm.resource_handles()?;
let connected = resources
.connectors()
.iter()
.map(|handle| drm.get_connector(*handle, true))
.collect::<io::Result<Vec<_>>>()?
.into_iter()
.filter(|connector| connector.state() == connector::State::Connected)
.collect::<Vec<_>>();
let identities = connected
.iter()
.map(|connector| connector_identity(drm, connector))
.collect::<Vec<_>>();
let active_profile = profiles.iter().find(|profile| {
profile.outputs.iter().all(|settings| {
connected
.iter()
.zip(&identities)
.any(|(connector, identity)| output_matches(settings, connector, identity))
})
});
if let Some(profile) = active_profile {
tracing::info!(profile = %profile.name, "selected output profile");
}
let mut selections = Vec::new();
let mut connected_outputs = Vec::new();
let mut used_crtcs = HashSet::new();
for handle in resources.connectors() {
let connector = drm.get_connector(*handle, true)?;
if connector.state() != connector::State::Connected {
for (connector, identity) in connected.into_iter().zip(identities) {
let settings = active_profile
.and_then(|profile| {
profile
.outputs
.iter()
.find(|settings| output_matches(settings, &connector, &identity))
})
.cloned()
.unwrap_or_else(|| default_output_settings(connector.to_string()));
let selected_mode = settings
.enabled
.then(|| select_mode(&connector, settings.mode))
.flatten();
connected_outputs.push(ConnectedOutputInfo {
connector: connector.to_string(),
identity: identity.clone(),
enabled: settings.enabled,
profile: active_profile.map(|profile| profile.name.clone()),
physical_size: connector.size(),
current_mode: selected_mode.map(|mode| connected_mode_info(mode)),
available_modes: connector
.modes()
.iter()
.copied()
.map(connected_mode_info)
.collect(),
scale: settings.scale,
transform: settings.transform,
configured_position: settings.position,
});
if !settings.enabled {
tracing::info!(output = %connector, "output disabled by active profile");
continue;
}
let Some(mode) = connector
.modes()
.iter()
.find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED))
.or_else(|| connector.modes().first())
.copied()
else {
let Some(mode) = selected_mode else {
continue;
};
@@ -666,20 +788,101 @@ fn select_outputs(drm: &DrmDevice) -> io::Result<Vec<(connector::Info, crtc::Han
});
if let Some(crtc) = current_crtc.or(compatible_crtc) {
used_crtcs.insert(crtc);
selections.push((connector, crtc, mode));
selections.push(OutputSelection {
connector,
crtc,
mode,
settings,
});
}
}
if selections.is_empty() {
Err(io::Error::new(
io::ErrorKind::NotFound,
"no connected desktop DRM connector with a usable CRTC",
))
} else {
Ok(selections)
selections.sort_by_key(|selection| selection.settings.position.is_none());
Ok(OutputScan {
selections,
connected_outputs,
})
}
fn connected_mode_info(mode: DrmMode) -> ConnectedModeInfo {
let (width, height) = mode.size();
ConnectedModeInfo {
width,
height,
refresh_millihertz: OutputMode::from(mode).refresh,
preferred: mode.mode_type().contains(ModeTypeFlags::PREFERRED),
}
}
fn output_matches(settings: &OutputSettings, connector: &connector::Info, identity: &str) -> bool {
settings.matcher == connector.to_string() || settings.matcher == identity
}
fn default_output_settings(matcher: String) -> OutputSettings {
OutputSettings {
matcher,
enabled: true,
mode: None,
scale: 1.0,
transform: OutputTransform::Normal,
position: None,
}
}
fn select_mode(
connector: &connector::Info,
requested: Option<OutputModeRequest>,
) -> Option<DrmMode> {
let fallback = || {
connector
.modes()
.iter()
.find(|mode| mode.mode_type().contains(ModeTypeFlags::PREFERRED))
.or_else(|| connector.modes().first())
.copied()
};
let Some(requested) = requested else {
return fallback();
};
let matching = connector
.modes()
.iter()
.filter(|mode| mode.size() == (requested.width, requested.height));
let selected = if let Some(refresh) = requested.refresh_millihertz {
matching
.min_by_key(|mode| {
OutputMode::from(**mode)
.refresh
.unsigned_abs()
.abs_diff(refresh)
})
.filter(|mode| {
OutputMode::from(**mode)
.refresh
.unsigned_abs()
.abs_diff(refresh)
<= 1_000
})
.copied()
} else {
matching
.max_by_key(|mode| OutputMode::from(**mode).refresh)
.copied()
};
if selected.is_none() {
tracing::warn!(
output = %connector,
width = requested.width,
height = requested.height,
refresh_millihertz = ?requested.refresh_millihertz,
"requested output mode is unavailable; using preferred mode"
);
}
selected.or_else(fallback)
}
fn create_direct_output(
state: &mut Ferese,
drm: &mut DrmDevice,
@@ -688,9 +891,10 @@ fn create_direct_output(
connector: connector::Info,
crtc: crtc::Handle,
mode: DrmMode,
settings: &OutputSettings,
) -> Result<DirectOutput, Box<dyn Error>> {
let identity = connector_identity(drm, &connector);
let (output, global) = create_output(state, &connector, mode, identity);
let (output, global) = create_output(state, &connector, mode, identity, settings);
let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?;
let allocator = GbmAllocator::new(
gbm.clone(),
@@ -709,6 +913,7 @@ fn create_direct_output(
Ok(DirectOutput {
connector: connector.handle(),
mode,
settings: settings.clone(),
output,
global,
surface,
@@ -723,6 +928,7 @@ fn create_output(
connector: &connector::Info,
mode: DrmMode,
identity: String,
settings: &OutputSettings,
) -> (Output, GlobalId) {
let name = connector.to_string();
let physical_size = connector.size().unwrap_or((0, 0));
@@ -738,25 +944,47 @@ fn create_output(
let output_mode = OutputMode::from(mode);
let global = output.create_global::<Ferese>(&state.display_handle);
for mode in connector.modes().iter().copied().map(OutputMode::from) {
output.add_mode(mode);
}
output.set_preferred(output_mode);
output.change_current_state(
Some(output_mode),
Some(Transform::Normal),
Some(Scale::Integer(1)),
Some((0, 0).into()),
Some(output_transform(settings.transform)),
Some(Scale::Fractional(settings.scale)),
Some({
let [x, y] = settings.position.unwrap_or([0, 0]);
(x, y).into()
}),
);
let x = state
.space
.outputs()
.filter_map(|output| state.space.output_geometry(output))
.map(|geometry| geometry.loc.x + geometry.size.w)
.max()
.unwrap_or(0);
state.space.map_output(&output, (x, 0));
let position = settings.position.unwrap_or_else(|| {
let x = state
.space
.outputs()
.filter_map(|output| state.space.output_geometry(output))
.map(|geometry| geometry.loc.x + geometry.size.w)
.max()
.unwrap_or(0);
[x, 0]
});
state.space.map_output(&output, (position[0], position[1]));
state.register_output(&output, identity);
(output, global)
}
fn output_transform(transform: OutputTransform) -> Transform {
match transform {
OutputTransform::Normal => Transform::Normal,
OutputTransform::Rotate90 => Transform::_90,
OutputTransform::Rotate180 => Transform::_180,
OutputTransform::Rotate270 => Transform::_270,
OutputTransform::Flipped => Transform::Flipped,
OutputTransform::Flipped90 => Transform::Flipped90,
OutputTransform::Flipped180 => Transform::Flipped180,
OutputTransform::Flipped270 => Transform::Flipped270,
}
}
fn connector_identity(drm: &DrmDevice, connector: &connector::Info) -> String {
let edid = drm
.get_properties(connector.handle())
@@ -775,9 +1003,7 @@ fn connector_identity(drm: &DrmDevice, connector: &connector::Info) -> String {
});
match edid {
Some(edid) if !edid.is_empty() => {
format!("drm:{}:{:016x}", connector, stable_hash(&edid))
}
Some(edid) if !edid.is_empty() => format!("drm-edid:{:016x}", stable_hash(&edid)),
_ => format!("drm:{connector}"),
}
}