use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::{Mutex, MutexGuard};
use drmkit_core::{AtomicCommitFlags, Device};
use drmkit_present::{BackendConfig, GbmScanoutProducer, RestorePolicy, ScanoutBackend, VrrPolicy};
use drmkit_scene::{
AcquiredBuffer, BindingModel, DmaBufDesc, LayerBufferSource, SourceError, SourceFormat,
};
use drmkit_sync::SyncFence;
static CARD_LOCK: Mutex<()> = Mutex::new(());
fn backend_config() -> BackendConfig {
BackendConfig {
connector_id: std::env::var_os("DRMKIT_TEST_CONNECTORS")
.and_then(|_| open_card_for_names())
.unwrap_or(0),
..BackendConfig::default()
}
}
fn open_card_for_names() -> Option<u32> {
let path = std::env::var("DRMKIT_TEST_CARD").unwrap_or_else(|_| "/dev/dri/card0".to_owned());
drmkit_testkit::connector::wanted(&Device::open(&path).ok()?)
}
fn card_guard() -> MutexGuard<'static, ()> {
CARD_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
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().ok()?;
device.enable_atomic().ok()?;
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)
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn a_full_screen_layer_reaches_a_plane_vkms() {
let _guard = card_guard();
let Some(device) = open_card() else { return };
let mut producer = GbmScanoutProducer::new(&device);
let mut backend = match ScanoutBackend::create(&device, &mut producer, &backend_config()) {
Ok(backend) => backend,
Err(error) => {
drmkit_testkit::skipped(&format!("no scanout output here ({error})"));
return;
}
};
let driver = drm::Device::get_driver(&device).expect("the driver's name");
assert_eq!(backend.profile().name, driver.name().to_string_lossy());
assert_ne!(
backend.target().primary_plane_id,
0,
"a target with no primary plane cannot scan anything out"
);
assert!(
drmkit_modeset::ModeInfo::width(&backend.target().mode) > 0,
"a zero-width mode is not something to scan out"
);
let report = backend
.present(&device, AtomicCommitFlags::empty(), None)
.expect("present the first frame");
assert!(report.layers_total >= 1);
assert!(
report.layers_assigned >= 1,
"the full-screen layer must land on a plane, not be composited"
);
assert!(!report.skipped_idle, "an unconditional present never skips");
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn an_unchanged_frame_is_suppressed_but_never_the_first_vkms() {
let _guard = card_guard();
let Some(device) = open_card() else { return };
let mut producer = GbmScanoutProducer::new(&device);
let mut backend = match ScanoutBackend::create(&device, &mut producer, &backend_config()) {
Ok(backend) => backend,
Err(error) => {
drmkit_testkit::skipped(&format!("no scanout output here ({error})"));
return;
}
};
let first = backend
.present_if_changed(&device, false, AtomicCommitFlags::empty(), None)
.expect("the first frame commits regardless");
assert!(!first.skipped_idle, "the first frame is never suppressed");
assert!(first.layers_assigned >= 1);
let idle = backend
.present_if_changed(&device, false, AtomicCommitFlags::empty(), None)
.expect("a suppressed frame is not a failure");
assert!(idle.skipped_idle);
assert_eq!(
idle.layers_total, 0,
"there was no commit, so there is nothing to count"
);
assert!(idle.placements.is_empty());
let changed = backend
.present_if_changed(&device, true, AtomicCommitFlags::empty(), None)
.expect("a changed frame commits again");
assert!(!changed.skipped_idle);
assert!(changed.layers_assigned >= 1);
assert_eq!(backend.frames_committed(), 2);
assert_eq!(backend.frames_skipped(), 1);
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn a_commit_delivers_its_out_fence_vkms() {
let _guard = card_guard();
let Some(device) = open_card() else { return };
let mut producer = GbmScanoutProducer::new(&device);
let mut backend = match ScanoutBackend::create(&device, &mut producer, &backend_config()) {
Ok(backend) => backend,
Err(error) => {
drmkit_testkit::skipped(&format!("no scanout output here ({error})"));
return;
}
};
let mut first_fence = None;
backend
.present(&device, AtomicCommitFlags::empty(), Some(&mut first_fence))
.expect("the modeset frame");
let mut flip_fence = None;
backend
.present(&device, AtomicCommitFlags::empty(), Some(&mut flip_fence))
.expect("the steady-state frame");
assert!(
flip_fence.is_some(),
"this CRTC advertises OUT_FENCE_PTR, so the caller must get the fence"
);
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn variable_refresh_arms_from_the_profile_and_toggles_cleanly_vkms() {
let _guard = card_guard();
let Some(device) = open_card() else { return };
let mut producer = GbmScanoutProducer::new(&device);
let config = BackendConfig {
vrr: VrrPolicy::Auto,
..backend_config()
};
let mut backend = match ScanoutBackend::create(&device, &mut producer, &config) {
Ok(backend) => backend,
Err(error) => {
drmkit_testkit::skipped(&format!("no scanout output here ({error})"));
return;
}
};
assert_eq!(
backend.vrr_capable(),
backend.profile().vrr_capable,
"what Auto decided from must be what the caller can read back"
);
backend
.present(&device, AtomicCommitFlags::empty(), None)
.expect("present with VRR as Auto chose it");
backend.set_vrr(false);
backend
.present(&device, AtomicCommitFlags::empty(), None)
.expect("present with VRR disarmed");
backend.set_vrr(true);
backend
.present(&device, AtomicCommitFlags::empty(), None)
.expect("present with VRR re-armed");
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn a_saved_crtc_is_put_back_on_teardown_vkms() {
use drm::control::Device as _;
let _guard = card_guard();
let Some(device) = open_card() else { return };
let resources = device.resource_handles().expect("resources");
let wanted = drmkit_testkit::connector::wanted(&device);
let Some((connector, crtc, mode)) = resources.connectors().iter().find_map(|handle| {
if wanted.is_some_and(|id| id != u32::from(*handle)) {
return None;
}
let connector = device.get_connector(*handle, false).ok()?;
if connector.state() != drm::control::connector::State::Connected {
return None;
}
let mode = *connector.modes().first()?;
let crtc = connector.encoders().iter().find_map(|encoder| {
let encoder = device.get_encoder(*encoder).ok()?;
resources
.filter_crtcs(encoder.possible_crtcs())
.first()
.copied()
})?;
Some((*handle, crtc, mode))
}) else {
drmkit_testkit::skipped("no connected connector with a mode");
return;
};
let buffer = drmkit_dumb::Buffer::create(
&device,
&drmkit_dumb::Config {
width: u32::from(mode.size().0),
height: u32::from(mode.size().1),
fourcc: drmkit_fmt::fourcc::XRGB8888,
..drmkit_dumb::Config::default()
},
)
.expect("a stand-in framebuffer");
let original = buffer.fb_id().expect("its fb id");
device
.set_crtc(
crtc,
drm::control::framebuffer::Handle::from(
std::num::NonZeroU32::new(original).expect("non-zero"),
)
.into(),
(0, 0),
&[connector],
Some(mode),
)
.expect("establish a known CRTC configuration");
let restored = {
let mut producer = GbmScanoutProducer::new(&device);
let config = BackendConfig {
restore: RestorePolicy::SavedCrtc,
..backend_config()
};
let backend = match ScanoutBackend::create(&device, &mut producer, &config) {
Ok(backend) => backend,
Err(error) => {
drmkit_testkit::skipped(&format!("no scanout output here ({error})"));
return;
}
};
device
.set_crtc(crtc, None, (0, 0), &[], None)
.expect("disable the CRTC");
assert_eq!(
device.get_crtc(crtc).expect("read it back").framebuffer(),
None,
"the CRTC must actually be down before teardown, or this proves nothing"
);
drop(backend);
device.get_crtc(crtc).expect("read it back").framebuffer()
};
assert_eq!(
restored.map(u32::from),
Some(original),
"teardown must put back the framebuffer that was on screen at create"
);
let _ = device.set_crtc(crtc, None, (0, 0), &[], None);
}
type Teardown = (Option<u64>, bool);
struct SeqOnDrop<'a> {
inner: GbmScanoutProducer<'a>,
fd: std::os::fd::RawFd,
crtc_id: Rc<Cell<u32>>,
seqs: Rc<RefCell<Vec<Teardown>>>,
}
impl drmkit_present::ScanoutProducer for SeqOnDrop<'_> {
fn exportable_modifiers(&mut self, fourcc: u32) -> Vec<u64> {
self.inner.exportable_modifiers(fourcc)
}
fn create_buffer(
&mut self,
width: u32,
height: u32,
fourcc: u32,
allowed: &[u64],
) -> Result<Box<dyn LayerBufferSource>, drmkit_present::ProducerError> {
let inner = self.inner.create_buffer(width, height, fourcc, allowed)?;
Ok(Box::new(SeqSource {
inner,
fd: self.fd,
crtc_id: Rc::clone(&self.crtc_id),
seqs: Rc::clone(&self.seqs),
}))
}
}
struct SeqSource {
inner: Box<dyn LayerBufferSource>,
fd: std::os::fd::RawFd,
crtc_id: Rc<Cell<u32>>,
seqs: Rc<RefCell<Vec<Teardown>>>,
}
impl Drop for SeqSource {
fn drop(&mut self) {
let fd = unsafe { std::os::fd::BorrowedFd::borrow_raw(self.fd) };
let seq = drmkit_core::crtc_sequence(&fd, self.crtc_id.get()).ok();
let queued = drmkit_core::event_pending(&fd);
self.seqs.borrow_mut().push((seq, queued));
}
}
impl LayerBufferSource for SeqSource {
fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError> {
self.inner.acquire()
}
fn release(&mut self, acquired: AcquiredBuffer) {
self.inner.release(acquired);
}
fn release_with_fence(&mut self, acquired: AcquiredBuffer, fence: Option<SyncFence>) {
self.inner.release_with_fence(acquired, fence);
}
fn wants_release_fence(&self) -> bool {
self.inner.wants_release_fence()
}
fn has_fresh_content(&self) -> bool {
self.inner.has_fresh_content()
}
fn on_retired(&mut self) {
self.inner.on_retired();
}
fn binding_model(&self) -> BindingModel {
self.inner.binding_model()
}
fn format(&self) -> SourceFormat {
self.inner.format()
}
fn map(
&mut self,
access: drmkit_dumb::MapAccess,
) -> Result<drmkit_dumb::Mapping<'_>, SourceError> {
self.inner.map(access)
}
fn export_dma_buf(&mut self) -> Result<DmaBufDesc<'_>, SourceError> {
self.inner.export_dma_buf()
}
fn bind_to_plane(&mut self, plane_id: u32) -> Result<(), SourceError> {
self.inner.bind_to_plane(plane_id)
}
fn unbind_from_plane(&mut self, plane_id: u32) {
self.inner.unbind_from_plane(plane_id);
}
fn on_session_paused(&mut self) {
self.inner.on_session_paused();
}
fn on_session_resumed(&mut self, device: &Device) -> Result<(), SourceError> {
self.inner.on_session_resumed(device)
}
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn dropping_the_backend_waits_for_the_armed_flip_vkms() {
drop_with_a_flip_armed(AtomicCommitFlags::PAGE_FLIP_EVENT | AtomicCommitFlags::NONBLOCK);
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn dropping_the_backend_after_a_blocking_flip_ends_the_wait_vkms() {
drop_with_a_flip_armed(AtomicCommitFlags::PAGE_FLIP_EVENT);
}
fn drop_with_a_flip_armed(flags: AtomicCommitFlags) {
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering};
use std::time::{Duration, Instant};
use drmkit_modeset::{PageFlip, Timeout};
let _guard = card_guard();
let Some(device) = open_card() else { return };
let crtc_id = Rc::new(Cell::new(0));
let seqs = Rc::new(RefCell::new(Vec::new()));
let mut producer = SeqOnDrop {
inner: GbmScanoutProducer::new(&device),
fd: device.raw_fd(),
crtc_id: Rc::clone(&crtc_id),
seqs: Rc::clone(&seqs),
};
let mut backend = match ScanoutBackend::create(&device, &mut producer, &backend_config()) {
Ok(backend) => backend,
Err(error) => {
drmkit_testkit::skipped(&format!("no scanout output here ({error})"));
return;
}
};
crtc_id.set(backend.target().crtc.id);
let mut flip = PageFlip::new(&device).expect("page flip");
let flips = Arc::new(AtomicU32::new(0));
let counted = Arc::clone(&flips);
flip.set_handler(Box::new(move |_| {
counted.fetch_add(1, Ordering::Relaxed);
}));
backend
.present(&device, AtomicCommitFlags::empty(), None)
.expect("modeset frame");
backend.present(&device, flags, None).expect("armed frame");
let armed = drmkit_core::crtc_sequence(&device, crtc_id.get()).ok();
seqs.borrow_mut().clear();
let started = Instant::now();
drop(backend); let took = started.elapsed();
let seqs = seqs.borrow();
assert!(!seqs.is_empty(), "teardown should drop the layer's source");
for (seq, queued) in seqs.iter() {
let passed = armed.zip(*seq).is_some_and(|(armed, seq)| seq > armed);
assert!(
passed || *queued,
"a buffer was torn down before the flip landed ({seq:?} vs {armed:?}, no event queued)"
);
}
assert!(
took < Duration::from_millis(100),
"the wait must end when the flip lands, took {took:?}"
);
flip.dispatch(Timeout::Bounded(Duration::from_millis(500)))
.expect("dispatch");
assert_eq!(
flips.load(Ordering::Relaxed),
1,
"the wait must leave the event queued for the caller"
);
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn a_pinned_connector_is_the_output_driven_vkms() {
use drm::control::Device as _;
let _guard = card_guard();
let Some(device) = open_card() else { return };
let resources = device.resource_handles().expect("resources");
let Some(last) = resources
.connectors()
.iter()
.filter(|handle| {
device.get_connector(**handle, false).is_ok_and(|info| {
info.state() == drm::control::connector::State::Connected
&& !info.modes().is_empty()
})
})
.map(|handle| u32::from(*handle))
.next_back()
else {
drmkit_testkit::skipped("no display attached");
return;
};
let mut producer = GbmScanoutProducer::new(&device);
let config = BackendConfig {
connector_id: last,
..backend_config()
};
let backend = match ScanoutBackend::create(&device, &mut producer, &config) {
Ok(backend) => backend,
Err(error) => {
drmkit_testkit::skipped(&format!("no scanout output here ({error})"));
return;
}
};
assert_eq!(backend.target().connector_id, last);
}
#[test]
#[ignore = "needs a DRM device"]
fn the_gbm_producer_honors_the_allowed_modifiers_vkms() {
use drmkit_present::ScanoutProducer as _;
const LINEAR: u64 = 0;
const X_TILED: u64 = (1 << 56) | 1;
const INVALID: u64 = (1 << 56) - 1;
let _guard = card_guard();
let Some(device) = open_card() else { return };
let mut producer = GbmScanoutProducer::new(&device);
let xrgb = drmkit_fmt::fourcc::XRGB8888;
let (w, h) = drmkit_testkit::framebuffer::size(&device, 64, 64);
for allowed in [&[][..], &[LINEAR], &[X_TILED, LINEAR]] {
let source = producer
.create_buffer(w, h, xrgb, allowed)
.unwrap_or_else(|error| panic!("{allowed:?}: {error}"));
let modifier = source.format().modifier;
assert!(
allowed.is_empty() || allowed.contains(&modifier) || modifier == INVALID,
"{allowed:?} allocated {modifier:#x}"
);
}
let refused = producer.create_buffer(w, h, xrgb, &[X_TILED]);
assert!(
matches!(refused, Err(drmkit_present::ProducerError::Unsupported)),
"a set the producer can make nothing in must be refused: {:?}",
refused.map(|source| source.format().modifier)
);
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn a_damaged_frame_reaches_the_kernel_with_its_clips_vkms() {
let _guard = card_guard();
let Some(device) = open_card() else { return };
let takes_damage = drmkit_display::DriverProfile::probe(&device)
.map(|profile| profile.fb_damage_clips)
.unwrap_or(false);
let mut producer = Damaging {
inner: GbmScanoutProducer::new(&device),
};
let mut backend = match ScanoutBackend::create(&device, &mut producer, &backend_config()) {
Ok(backend) => backend,
Err(error) => {
drmkit_testkit::skipped(&format!("no scanout output here ({error})"));
return;
}
};
let mut damaged = 0;
for frame in 0..4 {
let report = backend
.present(&device, AtomicCommitFlags::empty(), None)
.unwrap_or_else(|error| panic!("damaged frame {frame}: {error}"));
damaged += report.damaged_layers;
}
if takes_damage {
assert!(
damaged > 0,
"this driver takes FB_DAMAGE_CLIPS, so the frames' damage must reach it"
);
} else {
assert_eq!(
damaged, 0,
"the driver takes no damage property, so nothing should claim a clip"
);
}
}
struct Damaging<'a> {
inner: GbmScanoutProducer<'a>,
}
impl drmkit_present::ScanoutProducer for Damaging<'_> {
fn exportable_modifiers(&mut self, fourcc: u32) -> Vec<u64> {
self.inner.exportable_modifiers(fourcc)
}
fn create_buffer(
&mut self,
width: u32,
height: u32,
fourcc: u32,
allowed: &[u64],
) -> Result<Box<dyn LayerBufferSource>, drmkit_present::ProducerError> {
let inner = self.inner.create_buffer(width, height, fourcc, allowed)?;
Ok(Box::new(DamagingSource { inner }))
}
}
struct DamagingSource {
inner: Box<dyn LayerBufferSource>,
}
impl LayerBufferSource for DamagingSource {
fn acquire(&mut self) -> Result<AcquiredBuffer, SourceError> {
let mut acquired = self.inner.acquire()?;
acquired.damage = vec![drmkit_scene::DamageRect {
x: 8,
y: 8,
w: 16,
h: 16,
}];
Ok(acquired)
}
fn release(&mut self, acquired: AcquiredBuffer) {
self.inner.release(acquired);
}
fn release_with_fence(&mut self, acquired: AcquiredBuffer, fence: Option<SyncFence>) {
self.inner.release_with_fence(acquired, fence);
}
fn wants_release_fence(&self) -> bool {
self.inner.wants_release_fence()
}
fn has_fresh_content(&self) -> bool {
self.inner.has_fresh_content()
}
fn on_retired(&mut self) {
self.inner.on_retired();
}
fn binding_model(&self) -> BindingModel {
self.inner.binding_model()
}
fn format(&self) -> SourceFormat {
self.inner.format()
}
fn map(
&mut self,
access: drmkit_dumb::MapAccess,
) -> Result<drmkit_dumb::Mapping<'_>, SourceError> {
self.inner.map(access)
}
fn export_dma_buf(&mut self) -> Result<DmaBufDesc<'_>, SourceError> {
self.inner.export_dma_buf()
}
fn bind_to_plane(&mut self, plane_id: u32) -> Result<(), SourceError> {
self.inner.bind_to_plane(plane_id)
}
fn unbind_from_plane(&mut self, plane_id: u32) {
self.inner.unbind_from_plane(plane_id);
}
fn on_session_paused(&mut self) {
self.inner.on_session_paused();
}
fn on_session_resumed(&mut self, device: &Device) -> Result<(), SourceError> {
self.inner.on_session_resumed(device)
}
}
struct Recording<'a> {
inner: GbmScanoutProducer<'a>,
exportable: Vec<u64>,
allowed: Vec<u64>,
}
impl drmkit_present::ScanoutProducer for Recording<'_> {
fn exportable_modifiers(&mut self, _fourcc: u32) -> Vec<u64> {
self.exportable.clone()
}
fn create_buffer(
&mut self,
width: u32,
height: u32,
fourcc: u32,
allowed: &[u64],
) -> Result<Box<dyn LayerBufferSource>, drmkit_present::ProducerError> {
self.allowed = allowed.to_vec();
self.inner.create_buffer(width, height, fourcc, allowed)
}
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn nothing_negotiated_falls_back_to_linear_vkms() {
let _guard = card_guard();
let Some(device) = open_card() else { return };
for exportable in [Vec::new(), vec![0x0200_0000_0000_0001]] {
let mut producer = Recording {
inner: GbmScanoutProducer::new(&device),
exportable: exportable.clone(),
allowed: Vec::new(),
};
let backend = match ScanoutBackend::create(&device, &mut producer, &backend_config()) {
Ok(backend) => backend,
Err(error) => {
drmkit_testkit::skipped(&format!("no scanout output here ({error})"));
return;
}
};
assert!(
backend.modifiers().is_empty(),
"{exportable:x?}: nothing was negotiated"
);
drop(backend);
assert_eq!(
producer.allowed,
[0],
"{exportable:x?}: the producer is allowed LINEAR alone"
);
}
}