use drm::control::Device as _;
use drm::control::connector::{Interface, State};
use drm::control::{Mode, connector, crtc};
use drmkit_core::{Device, Result};
use drmkit_fmt::FormatTable;
use drmkit_modeset::select_preferred_mode;
use drmkit_planes::{PlaneRegistry, PlaneType};
use crate::profile::errno;
pub mod rank {
use drm::control::connector::Interface;
pub const MAIN: &[Interface] = &[
Interface::EmbeddedDisplayPort,
Interface::LVDS,
Interface::DSI,
Interface::DPI,
Interface::SPI,
Interface::HDMIA,
Interface::HDMIB,
Interface::DisplayPort,
Interface::DVID,
Interface::DVII,
Interface::DVIA,
Interface::VGA,
Interface::Virtual,
];
pub const INTERNAL: &[Interface] = &[
Interface::EmbeddedDisplayPort,
Interface::LVDS,
Interface::DSI,
Interface::DPI,
Interface::SPI,
];
pub const EXTERNAL: &[Interface] = &[
Interface::HDMIA,
Interface::HDMIB,
Interface::DisplayPort,
Interface::DVID,
Interface::DVII,
Interface::DVIA,
Interface::VGA,
Interface::Virtual,
];
}
#[must_use]
pub fn rank_pick(candidates: &[Interface], ranks: &[Interface]) -> Option<usize> {
ranks
.iter()
.find_map(|wanted| candidates.iter().position(|kind| kind == wanted))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct CrtcChoice {
pub id: u32,
pub index: u32,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct ScanoutTarget {
pub connector_id: u32,
pub crtc: CrtcChoice,
pub primary_plane_id: u32,
pub mode: Mode,
pub primary_formats: FormatTable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ScanoutError {
#[error("no connected connector with any mode")]
NothingConnected,
#[error("connector {connector_id} is not connected or has no mode")]
ConnectorUnavailable {
connector_id: u32,
},
#[error("connector {connector_id} has a display but no CRTC its encoders can drive")]
NoCrtc {
connector_id: u32,
},
#[error("CRTC {crtc_id} has no primary plane: was universal planes enabled?")]
NoPrimaryPlane {
crtc_id: u32,
},
}
impl ScanoutTarget {
pub fn discover(device: &Device) -> Result<std::result::Result<Self, ScanoutError>> {
Self::discover_ranked(device, rank::MAIN)
}
pub fn discover_ranked(
device: &Device,
ranks: &[Interface],
) -> Result<std::result::Result<Self, ScanoutError>> {
Self::discover_among(device, ranks, None)
}
pub fn discover_connector(
device: &Device,
connector_id: u32,
) -> Result<std::result::Result<Self, ScanoutError>> {
if connector_id == 0 {
return Self::discover(device);
}
Self::discover_among(device, rank::MAIN, Some(connector_id))
}
fn discover_among(
device: &Device,
ranks: &[Interface],
only: Option<u32>,
) -> Result<std::result::Result<Self, ScanoutError>> {
let _ = device.enable_universal_planes();
let resources = device.resource_handles().map_err(|error| errno(&error))?;
let mut eligible = Vec::new();
for handle in resources.connectors() {
if only.is_some_and(|id| id != u32::from(*handle)) {
continue;
}
let Ok(info) = device.get_connector(*handle, true) else {
continue;
};
if info.state() == State::Connected && !info.modes().is_empty() {
eligible.push(info);
}
}
if eligible.is_empty() {
return Ok(Err(only
.map_or(ScanoutError::NothingConnected, |connector_id| {
ScanoutError::ConnectorUnavailable { connector_id }
})));
}
let kinds: Vec<Interface> = eligible.iter().map(connector::Info::interface).collect();
let chosen = rank_pick(&kinds, ranks).unwrap_or(0);
let connector = &eligible[chosen];
let connector_id: u32 = connector.handle().into();
let Ok(mode) = select_preferred_mode(connector.modes()) else {
return Ok(Err(ScanoutError::NothingConnected));
};
let Some(crtc) = crtc_for_connector(device, &resources, connector) else {
return Ok(Err(ScanoutError::NoCrtc { connector_id }));
};
let registry = PlaneRegistry::probe(device)?;
let Some(primary) = primary_plane_for_crtc(®istry, crtc.index) else {
return Ok(Err(ScanoutError::NoPrimaryPlane { crtc_id: crtc.id }));
};
Ok(Ok(Self {
connector_id,
crtc,
primary_plane_id: primary.id,
mode,
primary_formats: primary.format_table.clone(),
}))
}
}
#[must_use]
pub fn primary_plane_for_crtc(
registry: &PlaneRegistry,
crtc_index: u32,
) -> Option<&drmkit_planes::PlaneCapabilities> {
registry
.for_crtc(crtc_index)
.find(|plane| plane.plane_type == PlaneType::Primary)
}
#[must_use]
pub fn crtc_for_connector(
device: &Device,
resources: &drm::control::ResourceHandles,
connector: &connector::Info,
) -> Option<CrtcChoice> {
let index_of = |crtc: crtc::Handle| {
resources
.crtcs()
.iter()
.position(|candidate| *candidate == crtc)
.and_then(|index| u32::try_from(index).ok())
};
let bound = connector
.current_encoder()
.and_then(|encoder| device.get_encoder(encoder).ok())
.and_then(|encoder| encoder.crtc())
.and_then(|crtc| Some((crtc, index_of(crtc)?)));
if let Some((crtc, index)) = bound {
return Some(CrtcChoice {
id: crtc.into(),
index,
});
}
for handle in connector.encoders() {
let Ok(encoder) = device.get_encoder(*handle) else {
continue;
};
let allowed = resources.filter_crtcs(encoder.possible_crtcs());
for crtc in allowed {
if let Some(index) = index_of(crtc) {
return Some(CrtcChoice {
id: crtc.into(),
index,
});
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::primary_plane_for_crtc;
use super::{rank, rank_pick};
use drm::control::connector::Interface;
use drmkit_planes::{PlaneCapabilities, PlaneRegistry, PlaneType};
fn plane(id: u32, plane_type: PlaneType, crtcs: u32) -> PlaneCapabilities {
PlaneCapabilities {
id,
plane_type,
possible_crtcs: crtcs,
formats: vec![drmkit_fmt::fourcc::ARGB8888],
..PlaneCapabilities::default()
}
}
fn registry() -> PlaneRegistry {
PlaneRegistry::from_capabilities(vec![
plane(10, PlaneType::Overlay, 0b01),
plane(11, PlaneType::Primary, 0b01),
plane(12, PlaneType::Cursor, 0b01),
plane(13, PlaneType::Primary, 0b10),
])
}
#[test]
fn each_crtc_gets_its_own_primary() {
let registry = registry();
assert_eq!(
primary_plane_for_crtc(®istry, 0).map(|plane| plane.id),
Some(11)
);
assert_eq!(
primary_plane_for_crtc(®istry, 1).map(|plane| plane.id),
Some(13)
);
}
#[test]
fn an_overlay_is_never_mistaken_for_a_primary() {
let registry = registry();
assert_ne!(
primary_plane_for_crtc(®istry, 0).map(|plane| plane.id),
Some(10)
);
}
#[test]
fn a_crtc_with_nothing_wired_to_it_has_no_primary() {
assert!(primary_plane_for_crtc(®istry(), 2).is_none());
}
#[test]
fn a_registry_with_no_primary_at_all_yields_none() {
let registry = PlaneRegistry::from_capabilities(vec![
plane(10, PlaneType::Overlay, 0b11),
plane(11, PlaneType::Overlay, 0b11),
]);
assert!(primary_plane_for_crtc(®istry, 0).is_none());
assert!(primary_plane_for_crtc(®istry, 1).is_none());
}
#[test]
fn an_internal_panel_wins_under_the_main_rank() {
let candidates = [Interface::HDMIA, Interface::EmbeddedDisplayPort];
assert_eq!(rank_pick(&candidates, rank::MAIN), Some(1));
}
#[test]
fn cable_out_is_taken_when_there_is_no_panel() {
let candidates = [Interface::VGA, Interface::HDMIA];
assert_eq!(rank_pick(&candidates, rank::MAIN), Some(1), "HDMI over VGA");
}
#[test]
fn the_internal_rank_ignores_external_connectors() {
let candidates = [Interface::HDMIA, Interface::DSI, Interface::DisplayPort];
assert_eq!(rank_pick(&candidates, rank::INTERNAL), Some(1));
}
#[test]
fn the_internal_rank_finds_nothing_when_only_cables_are_attached() {
let candidates = [Interface::HDMIA, Interface::DisplayPort];
assert_eq!(rank_pick(&candidates, rank::INTERNAL), None);
}
#[test]
fn the_external_rank_ignores_embedded_panels() {
let candidates = [Interface::EmbeddedDisplayPort, Interface::HDMIA];
assert_eq!(rank_pick(&candidates, rank::EXTERNAL), Some(1));
}
#[test]
fn a_tie_goes_to_the_earlier_connector() {
let candidates = [Interface::HDMIA, Interface::HDMIA];
assert_eq!(rank_pick(&candidates, rank::MAIN), Some(0));
}
#[test]
fn nothing_to_choose_from_chooses_nothing() {
assert_eq!(rank_pick(&[], rank::MAIN), None);
assert_eq!(rank_pick(&[Interface::HDMIA], &[]), None);
}
#[test]
fn a_connector_kind_the_rank_does_not_name_is_skipped() {
let candidates = [Interface::Composite, Interface::TV];
assert_eq!(rank_pick(&candidates, rank::MAIN), None);
}
#[test]
fn a_caller_can_impose_its_own_order() {
let candidates = [Interface::HDMIA, Interface::EmbeddedDisplayPort];
let ranks = [Interface::HDMIA, Interface::EmbeddedDisplayPort];
assert_eq!(rank_pick(&candidates, &ranks), Some(0));
}
#[test]
fn a_virtual_display_is_still_a_display() {
assert_eq!(rank_pick(&[Interface::Virtual], rank::MAIN), Some(0));
assert_eq!(rank_pick(&[Interface::Virtual], rank::EXTERNAL), Some(0));
}
#[test]
fn a_real_panel_still_outranks_a_virtual_one() {
let candidates = [Interface::Virtual, Interface::EmbeddedDisplayPort];
assert_eq!(rank_pick(&candidates, rank::MAIN), Some(1));
}
}