#![allow(dead_code, reason = "each test file uses a different part of the rig")]
use std::sync::{Mutex, MutexGuard};
use drm::control::Device as ControlDevice;
use drmkit_core::{AtomicCommitFlags, AtomicRequest, Device};
use drmkit_fmt::{FormatTable, fourcc};
use drmkit_planes::PlaneRegistry;
use drmkit_scene::{
CommitKind, CommitReport, DeviceCommitter, DisplayParams, KernelResult, LayerHandle,
LayerScene, Modeset, PlanePropertyMap, Rect, arm_acquire_fences, emit_frame_damaged,
};
static CARD_LOCK: Mutex<()> = Mutex::new(());
pub(crate) fn card_guard() -> MutexGuard<'static, ()> {
CARD_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) fn open_card() -> Option<Device> {
let path = std::env::var("DRMKIT_TEST_CARD").unwrap_or_else(|_| "/dev/dri/card0".to_owned());
drmkit_testkit::announce_card(&path);
let device = match Device::open(&path) {
Ok(device) => device,
Err(error) => {
assert!(
std::env::var_os("DRMKIT_REQUIRE_MASTER").is_none(),
"{path}: {error}, but DRMKIT_REQUIRE_MASTER is set"
);
drmkit_testkit::skipped(&format!("no DRM device at {path} ({error})"));
return None;
}
};
device.enable_universal_planes().expect("universal planes");
device.enable_atomic().expect("atomic");
if device.set_master().is_err() {
assert!(
std::env::var_os("DRMKIT_REQUIRE_MASTER").is_none(),
"not DRM master, but DRMKIT_REQUIRE_MASTER is set"
);
drmkit_testkit::skipped("another client holds DRM master");
return None;
}
Some(device)
}
pub(crate) const LAYER_W: u32 = 64;
pub(crate) const LAYER_H: u32 = 64;
pub(crate) struct Fixture {
pub(crate) device: Device,
registry: PlaneRegistry,
map: PlanePropertyMap,
pub(crate) scene: LayerScene,
crtc_id: u32,
crtc_index: u32,
connector_id: u32,
mode: drmkit_core::Mode,
needs_modeset: bool,
retired_crtcs: Vec<u32>,
}
pub(crate) struct Output {
connector_id: u32,
crtc: drm::control::crtc::Handle,
crtc_index: u32,
mode: drmkit_core::Mode,
}
impl Output {
pub(crate) fn mode_size(&self) -> (u32, u32) {
(
drmkit_modeset::ModeInfo::width(&self.mode),
drmkit_modeset::ModeInfo::height(&self.mode),
)
}
}
fn wanted_connectors() -> Option<Vec<String>> {
let list = std::env::var("DRMKIT_TEST_CONNECTORS").ok()?;
Some(
list.split(',')
.map(|name| name.trim().to_owned())
.filter(|name| !name.is_empty())
.collect(),
)
}
fn find_output(
device: &Device,
resources: &drm::control::ResourceHandles,
avoid_crtc: Option<drm::control::crtc::Handle>,
avoid_connector: Option<u32>,
) -> Option<Output> {
let candidates: Vec<_> = resources
.connectors()
.iter()
.filter_map(|handle| {
let connector = device.get_connector(*handle, false).ok()?;
let id = u32::from(*handle);
(connector.state() == drm::control::connector::State::Connected
&& Some(id) != avoid_connector)
.then_some(connector)
})
.collect();
let name = |c: &drm::control::connector::Info| {
format!("{}-{}", c.interface().as_str(), c.interface_id())
};
let ordered: Vec<_> = match wanted_connectors() {
Some(wanted) => wanted
.iter()
.filter_map(|want| candidates.iter().find(|c| name(c) == *want))
.collect(),
None => candidates.iter().collect(),
};
ordered.into_iter().find_map(|connector| {
let mode = *connector.modes().first()?;
let crtc = connector.encoders().iter().find_map(|e| {
let encoder = device.get_encoder(*e).ok()?;
resources
.filter_crtcs(encoder.possible_crtcs())
.into_iter()
.find(|crtc| Some(*crtc) != avoid_crtc)
})?;
let crtc_index = u32::try_from(resources.crtcs().iter().position(|c| *c == crtc)?).ok()?;
Some(Output {
connector_id: u32::from(connector.handle()),
crtc,
crtc_index,
mode,
})
})
}
pub(crate) fn fixture(device: Device) -> Option<Fixture> {
let resources = device.resource_handles().ok()?;
let mut output = find_output(&device, &resources, None, None);
for _ in 0..30 {
if output.is_some() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
output = find_output(&device, &resources, None, None);
}
let output = output?;
let crtc_id = u32::from(output.crtc);
let registry = PlaneRegistry::probe(&device).ok()?;
let mut map = PlanePropertyMap::new();
for plane in registry.for_crtc(output.crtc_index) {
map.learn_plane(&device, plane.id).ok()?;
}
Some(Fixture {
scene: LayerScene::new(crtc_id),
device,
registry,
map,
crtc_id,
crtc_index: output.crtc_index,
connector_id: output.connector_id,
mode: output.mode,
needs_modeset: true,
retired_crtcs: Vec::new(),
})
}
impl Fixture {
pub(crate) fn add_layer(&mut self, x: i32, y: i32, w: u32, h: u32) -> Option<LayerHandle> {
let source =
drmkit_scene_sources::DumbBufferSource::create(&self.device, w, h, fourcc::XRGB8888)
.ok()?;
let handle = self.scene.add_layer(Box::new(source));
self.scene.layer_mut(handle)?.set_display(DisplayParams {
src_rect: Rect { x: 0, y: 0, w, h },
dst_rect: Rect { x, y, w, h },
..DisplayParams::default()
});
Some(handle)
}
pub(crate) fn move_layer(&mut self, handle: LayerHandle, x: i32, y: i32) {
let layer = self.scene.layer_mut(handle).expect("the layer is there");
let mut display = *layer.display();
display.dst_rect = Rect {
x,
y,
w: display.dst_rect.w,
h: display.dst_rect.h,
};
layer.set_display(display);
}
pub(crate) fn commit(&mut self) -> Result<CommitReport, String> {
self.commit_within(None).map(|(report, _)| report)
}
pub(crate) fn commit_limited(&mut self, limit: usize) -> Result<(CommitReport, usize), String> {
self.commit_within(Some(limit))
}
fn commit_within(&mut self, limit: Option<usize>) -> Result<(CommitReport, usize), String> {
let modeset = if self.needs_modeset {
Some(
Modeset::learn(&self.device, self.crtc_id, self.connector_id, &self.mode)
.map_err(|error| format!("learning the mode: {error}"))?,
)
} else {
None
};
let mut flags = AtomicCommitFlags::empty();
if modeset.is_some() {
flags |= AtomicCommitFlags::ALLOW_MODESET;
}
let mut committer = PlaneLimit {
inner: DeviceCommitter::new(
&self.device,
&self.map,
&self.registry,
self.crtc_index,
AtomicCommitFlags::empty(),
modeset.as_ref(),
),
limit,
extra: None,
refused: 0,
};
let mut build = self
.scene
.build_frame(
&self.registry,
self.crtc_index,
CommitKind::Real { arms_flip: false },
&mut committer,
)
.map_err(|error| format!("building: {error}"))?;
let refused = committer.refused;
let mut request = AtomicRequest::with_capacity(64);
let (_, _damage_blobs) = emit_frame_damaged(
&mut request,
&self.map,
&mut build,
modeset.as_ref(),
Some(&self.device),
)
.map_err(|error| format!("emitting: {error}"))?;
arm_acquire_fences(&mut build, &mut request, &self.map, true)
.map_err(|error| format!("fences: {error}"))?;
let programmed: Vec<_> = build
.plan()
.iter()
.map(|entry| (entry.plane_id, entry.color))
.collect();
if let Some(limit) = limit {
let armed = self.armed_after(&request);
if armed > limit {
self.scene.finalize_frame(build, KernelResult::Rejected);
return Err(format!(
"committing: the frame arms {armed} planes on a CRTC that lights {limit}"
));
}
}
match request.commit(&self.device, flags) {
Ok(()) => {
for (plane_id, color) in programmed {
self.map.note_color_committed(plane_id, color);
}
self.needs_modeset = false;
Ok((self.scene.finalize_frame(build, KernelResult::Ok), refused))
}
Err(error) => {
self.scene.finalize_frame(build, KernelResult::Rejected);
Err(format!("committing: {error}"))
}
}
}
fn armed_after(&self, request: &AtomicRequest) -> usize {
self.registry
.for_crtc(self.crtc_index)
.filter(|plane| {
let written = |tag| {
let property = self.map.property_id(plane.id, tag)?;
request
.writes()
.iter()
.rev()
.find(|w| w.object_id == plane.id && w.property_id == property)
.map(|w| w.value)
};
let fb = written(drmkit_planes::PropTag::FbId)
.or_else(|| self.plane_property(plane.id, "FB_ID"))
.unwrap_or(0);
let crtc = written(drmkit_planes::PropTag::CrtcId)
.or_else(|| self.plane_property(plane.id, "CRTC_ID"))
.unwrap_or(0);
fb != 0 && crtc == u64::from(self.crtc_id)
})
.count()
}
pub(crate) fn enable_full_screen_composition(&mut self) -> Option<()> {
let (w, h) = self.mode_size();
self.scene.enable_composition(&self.device, w, h).ok()
}
pub(crate) fn enable_composition_on_primary(&mut self) -> Option<(u32, u32, u32)> {
let (w, h) = self.mode_size();
let primary = self
.registry
.for_crtc(self.crtc_index)
.find(|plane| plane.plane_type == drmkit_planes::PlaneType::Primary)?
.clone();
self.scene
.enable_composition_on(&self.device, w, h, &primary)
.ok()?;
let negotiated = drmkit_scene::canvas_format_for_plane(&primary)?;
Some((primary.id, self.scene.canvas_fourcc()?, negotiated))
}
pub(crate) fn paint(&mut self, handle: LayerHandle, xrgb: u32) -> Option<()> {
let layer = self.scene.layer_mut(handle)?;
let mut mapping = layer.source_mut().map(drmkit_dumb::MapAccess::Write).ok()?;
let stride = mapping.stride() as usize;
let width = mapping.width() as usize;
let height = mapping.height() as usize;
for row in mapping.pixels_mut().chunks_mut(stride).take(height) {
for pixel in row[..width * 4].chunks_exact_mut(4) {
pixel.copy_from_slice(&(0xFF00_0000 | xrgb).to_le_bytes());
}
}
Some(())
}
pub(crate) fn set_zpos(&mut self, handle: LayerHandle, zpos: u64) {
let layer = self.scene.layer_mut(handle).expect("the layer");
let mut display = *layer.display();
display.zpos = Some(zpos);
layer.set_display(display);
}
pub(crate) fn pixel_at(&self, x: u32, y: u32) -> Option<u32> {
let image = match drmkit_capture::snapshot(&self.device, self.crtc_id) {
Ok(image) => image,
Err(drmkit_capture::CaptureError::NothingReadable) => return None,
Err(error) => panic!("snapshot: {error}"),
};
Some(image.pixels()[(y * image.width() + x) as usize] & 0x00FF_FFFF)
}
pub(crate) fn eligible_planes(&self) -> usize {
let planes = self
.registry
.force_disable_candidates(self.crtc_index)
.count();
self.registry
.blend_stage_limit(self.crtc_index)
.map_or(planes, |limit| planes.min(limit))
}
pub(crate) fn primary_only(&self) -> bool {
primary_only(&self.registry, self.crtc_index)
}
pub(crate) fn skip_if_primary_only(&self, needs: &str) -> bool {
skip_if_primary_only(&self.registry, self.crtc_index, needs)
}
pub(crate) fn add_placeable_layer(&mut self) -> Option<LayerHandle> {
let (w, h) = if self.primary_only() {
self.mode_size()
} else {
(LAYER_W, LAYER_H)
};
self.add_layer(0, 0, w, h)
}
pub(crate) fn an_overlay_plane(&self) -> Option<u32> {
self.registry
.for_crtc(self.crtc_index)
.find(|plane| {
plane.plane_type == drmkit_planes::PlaneType::Overlay
&& plane.scans_out_linear(fourcc::XRGB8888)
})
.map(|plane| plane.id)
}
pub(crate) fn planes(&self) -> impl Iterator<Item = &drmkit_planes::PlaneCapabilities> {
self.registry.for_crtc(self.crtc_index)
}
pub(crate) fn plane_framebuffer(&self, plane_id: u32) -> Option<u32> {
u32::try_from(self.plane_property(plane_id, "FB_ID")?).ok()
}
pub(crate) fn plane_property(&self, plane_id: u32, name: &str) -> Option<u64> {
let handle = drm::control::plane::Handle::from(std::num::NonZeroU32::new(plane_id)?);
let props = self.device.get_properties(handle).ok()?;
let (handles, values) = props.as_props_and_values();
for (property, value) in handles.iter().zip(values.iter()) {
let Ok(info) = self.device.get_property(*property) else {
continue;
};
if info.name().to_string_lossy() == name {
return Some(*value);
}
}
None
}
pub(crate) fn lit_planes(&self, crtc_id: u32) -> Vec<u32> {
let Ok(planes) = self.device.plane_handles() else {
return Vec::new();
};
planes
.iter()
.filter_map(|handle| {
let info = self.device.get_plane(*handle).ok()?;
let on_crtc = info.crtc().map(u32::from) == Some(crtc_id);
(info.framebuffer().is_some() && on_crtc).then(|| u32::from(*handle))
})
.collect()
}
pub(crate) fn another_output(&self) -> Option<Output> {
let resources = self.device.resource_handles().ok()?;
let current = drm::control::crtc::Handle::from(std::num::NonZeroU32::new(self.crtc_id)?);
find_output(
&self.device,
&resources,
Some(current),
Some(self.connector_id),
)
}
pub(crate) fn a_plane_shared_with(&self, other: &Output) -> Option<u32> {
let shared: Vec<_> = self
.registry
.for_crtc(self.crtc_index)
.filter(|plane| plane.compatible_with_crtc(other.crtc_index))
.filter(|plane| plane.plane_type != drmkit_planes::PlaneType::Cursor)
.collect();
shared
.iter()
.find(|plane| plane.plane_type == drmkit_planes::PlaneType::Overlay)
.or_else(|| shared.first())
.map(|plane| plane.id)
}
pub(crate) fn commit_detach(&mut self) -> Result<bool, String> {
drmkit_scene::commit_detach(&self.device, &mut self.scene)
.map_err(|error| format!("detaching: {error}"))
}
pub(crate) fn switch_to(&mut self, output: &Output) -> Option<()> {
for plane in self.registry.for_crtc(output.crtc_index) {
self.map.learn_plane(&self.device, plane.id).ok()?;
}
self.retired_crtcs.push(self.crtc_id);
self.crtc_id = u32::from(output.crtc);
self.crtc_index = output.crtc_index;
self.connector_id = output.connector_id;
self.mode = output.mode;
self.needs_modeset = true;
Some(())
}
pub(crate) fn mode_size(&self) -> (u32, u32) {
(
drmkit_modeset::ModeInfo::width(&self.mode),
drmkit_modeset::ModeInfo::height(&self.mode),
)
}
pub(crate) const fn crtc_id(&self) -> u32 {
self.crtc_id
}
pub(crate) const fn force_modeset(&mut self) {
self.needs_modeset = true;
}
pub(crate) fn teardown(&mut self) {
for crtc_id in std::iter::once(self.crtc_id).chain(self.retired_crtcs.drain(..)) {
let _ = self.device.set_crtc(
drm::control::crtc::Handle::from(
std::num::NonZeroU32::new(crtc_id).expect("non-zero"),
),
None,
(0, 0),
&[],
None,
);
}
}
}
#[allow(
unused_imports,
reason = "not every test file in this crate picks a CRTC"
)]
pub(crate) use drmkit_testkit::crtc::pick_crtc;
struct PlaneLimit<'a> {
inner: DeviceCommitter<'a>,
limit: Option<usize>,
extra: Option<u32>,
refused: usize,
}
impl drmkit_planes::TestCommitter for PlaneLimit<'_> {
fn test_assignment(
&mut self,
assignment: &[(u32, drmkit_planes::LayerRef<'_>)],
) -> Result<(), drmkit_planes::TestFailure> {
let planes = assignment.len() + usize::from(self.extra.is_some());
if self.limit.is_some_and(|limit| planes > limit) {
self.refused += 1;
return Err(drmkit_planes::TestFailure::Rejected);
}
self.inner.test_assignment(assignment)
}
fn set_extra_plane(&mut self, plane_id: u32, layer: drmkit_planes::Layer) {
self.extra = Some(plane_id);
self.inner.set_extra_plane(plane_id, layer);
}
fn clear_extra_plane(&mut self) {
self.extra = None;
self.inner.clear_extra_plane();
}
}
pub(crate) fn primary_only(registry: &PlaneRegistry, crtc_index: u32) -> bool {
!registry
.for_crtc(crtc_index)
.any(|plane| plane.plane_type == drmkit_planes::PlaneType::Overlay)
}
pub(crate) fn probed_format_table(probed: Option<&PlaneRegistry>, plane_id: u32) -> FormatTable {
probed
.and_then(|registry| registry.by_id(plane_id))
.map(|plane| plane.format_table.clone())
.unwrap_or_default()
}
pub(crate) fn current_mode_size(device: &Device, crtc_id: u32) -> Option<(u32, u32)> {
let crtc = drm::control::crtc::Handle::from(std::num::NonZeroU32::new(crtc_id)?);
let mode = device.get_crtc(crtc).ok()?.mode()?;
let (w, h) = mode.size();
Some((u32::from(w), u32::from(h)))
}
pub(crate) fn skip_if_primary_only(registry: &PlaneRegistry, crtc_index: u32, needs: &str) -> bool {
let skip = primary_only(registry, crtc_index);
if skip {
drmkit_testkit::skipped(&format!(
"only a full-screen primary on this CRTC; the case needs {needs}"
));
}
skip
}