use drm::control::Device as _;
use drmkit_core::{AtomicRequest, Device};
use drmkit_cursor::{Cursor, PlanePath, Renderer, RendererConfig, Rotation};
use drmkit_planes::{PlaneCapabilities, PlaneRegistry, PlaneType};
static CARD_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn card_guard() -> std::sync::MutexGuard<'static, ()> {
CARD_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn fixture() -> Option<(Device, PlaneRegistry, u32, u32)> {
let path = std::env::var("DRMKIT_TEST_CARD").unwrap_or_else(|_| "/dev/dri/card0".to_owned());
drmkit_testkit::announce_card(&path);
let Ok(device) = Device::open(&path) else {
drmkit_testkit::skipped(&format!("{path} did not open"));
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;
}
let resources = device.resource_handles().expect("resources");
let crtcs = resources.crtcs();
let index = match drmkit_testkit::connector::wanted(&device) {
Some(connector) => crtc_for(&device, &resources, connector)?,
None => resources
.connectors()
.iter()
.find_map(|handle| {
let connector = device.get_connector(*handle, false).ok()?;
if connector.state() != drm::control::connector::State::Connected {
return None;
}
let encoder = device.get_encoder(connector.current_encoder()?).ok()?;
let crtc = encoder.crtc()?;
crtcs.iter().position(|candidate| *candidate == crtc)
})
.unwrap_or(0),
};
let crtc_id: u32 = (*crtcs.get(index)?).into();
let registry = PlaneRegistry::probe(&device).expect("plane registry");
Some((
device,
registry,
crtc_id,
u32::try_from(index).expect("small"),
))
}
fn square() -> Cursor {
Cursor::from_argb(&vec![0xffff_00ff; 16 * 16], 16, 16, 8, 8).expect("cursor")
}
fn config(crtc_id: u32, crtc_index: u32) -> RendererConfig {
RendererConfig {
crtc_id,
crtc_index,
..RendererConfig::default()
}
}
fn crtc_for(
device: &Device,
resources: &drm::control::ResourceHandles,
connector: u32,
) -> Option<usize> {
let handle = *resources
.connectors()
.iter()
.find(|handle| u32::from(**handle) == connector)?;
let info = device.get_connector(handle, false).ok()?;
let crtcs = resources.crtcs();
if let Some(crtc) = info
.current_encoder()
.and_then(|encoder| device.get_encoder(encoder).ok())
.and_then(|encoder| encoder.crtc())
{
return crtcs.iter().position(|candidate| *candidate == crtc);
}
info.encoders().iter().find_map(|encoder| {
let encoder = device.get_encoder(*encoder).ok()?;
let allowed = resources.filter_crtcs(encoder.possible_crtcs());
crtcs
.iter()
.position(|candidate| allowed.contains(candidate))
})
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn a_renderer_settles_on_a_plane_and_a_size_vkms() {
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let renderer = match Renderer::create(&device, ®istry, &config(crtc_id, crtc_index)) {
Ok(renderer) => renderer,
Err(error) => {
drmkit_testkit::skipped(&format!("no usable cursor path here: {error}"));
return;
}
};
let plane = renderer.plane();
let (w, h) = renderer.size();
println!(
"note: plane {} via {:?}, buffer {w}x{h}",
plane.plane_id, plane.path
);
assert_ne!(plane.plane_id, 0, "an atomic path names a plane");
assert!(
matches!(
plane.path,
PlanePath::AtomicCursor | PlanePath::AtomicOverlay
),
"legacy is refused rather than half-supported: {:?}",
plane.path
);
assert_eq!(w, h, "a cursor buffer is square");
assert!(w >= 64, "never below the floor: {w}");
let cap = |which| {
drm::Device::get_driver_capability(&device, which)
.ok()
.and_then(|v| u32::try_from(v).ok())
.filter(|v| *v != 0)
};
if let (Some(cap_w), Some(cap_h)) = (
cap(drm::DriverCapability::CursorWidth),
cap(drm::DriverCapability::CursorHeight),
) {
let advertised = cap_w.min(cap_h);
let expected = [256, 128, 64]
.into_iter()
.find(|rung| *rung <= advertised)
.unwrap_or(64);
assert!(
w >= expected.min(advertised),
"this card advertises {advertised}x{advertised} and the buffer is \
{w}x{h}; a cursor pinned at the 64px floor means the driver cap \
is not being read"
);
}
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn a_cursor_is_drawn_and_staged_vkms() {
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let Ok(mut renderer) = Renderer::create(&device, ®istry, &config(crtc_id, crtc_index))
else {
drmkit_testkit::skipped("no usable cursor path here");
return;
};
renderer.set_cursor(square()).expect("set cursor");
renderer.move_to(100, 200);
renderer.prepare().expect("prepare");
let mut request = AtomicRequest::new();
let written = renderer.stage_into(&mut request).expect("stage");
assert!(written >= 10, "wrote only {written} properties");
let plane_id = renderer.plane().plane_id;
let store = {
let mut store = drmkit_core::PropertyStore::new();
store
.cache_properties(&device, plane_id, drmkit_core::ObjectType::Plane)
.expect("props");
store
};
let value = |name: &str| {
let id = store.property_id(plane_id, name).expect(name);
request
.writes()
.iter()
.find(|w| w.object_id == plane_id && w.property_id == id)
.map(|w| w.value)
};
let (buffer_w, _) = renderer.size();
let centering = i64::from(buffer_w - 16) / 2;
let expected_x = (100 - (8 + centering)).max(0);
let expected_y = (200 - (8 + centering)).max(0);
let as_property = |v: i64| Some(v.cast_unsigned());
assert_eq!(value("CRTC_X"), as_property(expected_x));
assert_eq!(value("CRTC_Y"), as_property(expected_y));
assert_ne!(value("FB_ID"), Some(0), "a visible cursor names a buffer");
assert_eq!(value("CRTC_ID"), Some(u64::from(crtc_id)));
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn hiding_disarms_the_plane_vkms() {
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let Ok(mut renderer) = Renderer::create(&device, ®istry, &config(crtc_id, crtc_index))
else {
drmkit_testkit::skipped("no usable cursor path here");
return;
};
renderer.set_cursor(square()).expect("set cursor");
renderer.set_visible(false);
let mut request = AtomicRequest::new();
renderer.stage_into(&mut request).expect("stage");
let plane_id = renderer.plane().plane_id;
let mut store = drmkit_core::PropertyStore::new();
store
.cache_properties(&device, plane_id, drmkit_core::ObjectType::Plane)
.expect("props");
for name in ["FB_ID", "CRTC_ID"] {
let id = store.property_id(plane_id, name).expect(name);
let value = request
.writes()
.iter()
.find(|w| w.object_id == plane_id && w.property_id == id)
.map(|w| w.value);
assert_eq!(value, Some(0), "{name} should be cleared while hidden");
}
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn an_animation_redraws_only_when_the_frame_changes_vkms() {
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let Ok(mut renderer) = Renderer::create(&device, ®istry, &config(crtc_id, crtc_index))
else {
drmkit_testkit::skipped("no usable cursor path here");
return;
};
let frames = vec![
drmkit_cursor::Frame {
pixels: vec![0xffff_0000; 64],
width: 8,
height: 8,
xhot: 0,
yhot: 0,
delay: std::time::Duration::from_millis(100),
},
drmkit_cursor::Frame {
pixels: vec![0xff00_ff00; 64],
width: 8,
height: 8,
xhot: 0,
yhot: 0,
delay: std::time::Duration::from_millis(100),
},
];
let animated = Cursor::from_frames(frames).expect("animated cursor");
renderer.set_cursor(animated).expect("set cursor");
let mut redraws = 0;
for step in 0..12u64 {
if renderer
.tick(std::time::Duration::from_millis(step * 16))
.expect("tick")
{
redraws += 1;
}
}
assert_eq!(
redraws, 1,
"one frame boundary is crossed in 192ms, so one redraw"
);
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn a_static_cursor_never_redraws_vkms() {
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let Ok(mut renderer) = Renderer::create(&device, ®istry, &config(crtc_id, crtc_index))
else {
drmkit_testkit::skipped("no usable cursor path here");
return;
};
renderer.set_cursor(square()).expect("set cursor");
for step in 0..8u64 {
assert!(
!renderer
.tick(std::time::Duration::from_millis(step * 500))
.expect("tick"),
"a static cursor has nothing to advance to"
);
}
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn rotation_is_asked_of_the_hardware_only_when_it_can_do_it_vkms() {
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let Ok(mut renderer) = Renderer::create(&device, ®istry, &config(crtc_id, crtc_index))
else {
drmkit_testkit::skipped("no usable cursor path here");
return;
};
renderer.set_cursor(square()).expect("set cursor");
renderer.set_rotation(Rotation::Quarter).expect("rotate");
let plane_id = renderer.plane().plane_id;
let mut store = drmkit_core::PropertyStore::new();
store
.cache_properties(&device, plane_id, drmkit_core::ObjectType::Plane)
.expect("props");
let Ok(id) = store.property_id(plane_id, "rotation") else {
println!("note: this plane has no rotation property, so the turn is in software");
return;
};
let mut request = AtomicRequest::new();
renderer.stage_into(&mut request).expect("stage");
let value = request
.writes()
.iter()
.find(|w| w.object_id == plane_id && w.property_id == id)
.map(|w| w.value)
.expect("rotation written");
let can_turn = registry
.by_id(plane_id)
.is_some_and(|plane| plane.rotation_bits & Rotation::Quarter.drm_mask() != 0);
let expected = if can_turn {
Rotation::Quarter.drm_mask()
} else {
Rotation::None.drm_mask()
};
assert_eq!(
value, expected,
"asking the hardware to turn pixels that are already turned turns them twice"
);
}
fn primary_only(registry: &PlaneRegistry, crtc_index: u32) -> PlaneRegistry {
let planes: Vec<PlaneCapabilities> = registry
.all()
.iter()
.filter(|plane| plane.plane_type == PlaneType::Primary)
.cloned()
.collect();
assert!(
planes
.iter()
.any(|p| p.possible_crtcs & (1 << crtc_index) != 0),
"the fixture needs a primary plane on this CRTC"
);
PlaneRegistry::from_capabilities(planes)
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn the_legacy_path_probes_before_promising_anything_vkms() {
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let legacy_only = primary_only(®istry, crtc_index);
match Renderer::create(&device, &legacy_only, &config(crtc_id, crtc_index)) {
Ok(renderer) => {
assert_eq!(renderer.plane().path, PlanePath::Legacy);
assert_eq!(renderer.plane().plane_id, 0, "legacy addresses the CRTC");
let (w, h) = renderer.size();
assert_eq!((w, h), (64, 64), "legacy is 64 on every driver");
println!("note: this driver has a legacy cursor");
}
Err(error) => {
println!("note: this driver has no legacy cursor ({error})");
}
}
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn legacy_refuses_rotation_rather_than_pretending_vkms() {
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let legacy_only = primary_only(®istry, crtc_index);
let mut rotated = config(crtc_id, crtc_index);
rotated.rotation = Rotation::Quarter;
assert!(
Renderer::create(&device, &legacy_only, &rotated).is_err(),
"a rotated cursor on the legacy path must be refused"
);
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn legacy_refuses_when_the_caller_forbids_it_vkms() {
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let legacy_only = primary_only(®istry, crtc_index);
let mut strict = config(crtc_id, crtc_index);
strict.allow_legacy = false;
assert!(matches!(
Renderer::create(&device, &legacy_only, &strict),
Err(drmkit_cursor::CursorError::NoPlane)
));
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn a_legacy_cursor_installs_once_and_then_moves_vkms() {
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let legacy_only = primary_only(®istry, crtc_index);
let Ok(mut renderer) = Renderer::create(&device, &legacy_only, &config(crtc_id, crtc_index))
else {
drmkit_testkit::skipped("this driver has no legacy cursor");
return;
};
renderer.set_cursor(square()).expect("set cursor");
renderer.move_to(10, 10);
renderer.commit().expect("install");
for (x, y) in [(20, 20), (300, 400), (0, 0)] {
renderer.move_to(x, y);
renderer.commit().expect("move");
}
renderer.set_visible(false);
renderer.commit().expect("hide");
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn a_paused_renderer_comes_back_with_its_cursor_vkms() {
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let Ok(mut renderer) = Renderer::create(&device, ®istry, &config(crtc_id, crtc_index))
else {
drmkit_testkit::skipped("no usable cursor path here");
return;
};
renderer.set_cursor(square()).expect("set cursor");
renderer.move_to(120, 240);
let plane_before = renderer.plane().plane_id;
let size_before = renderer.size();
let paused = renderer.pause();
assert!(paused.has_cursor(), "the cursor outlives the descriptor");
let resumed_device = {
let path =
std::env::var("DRMKIT_TEST_CARD").unwrap_or_else(|_| "/dev/dri/card0".to_owned());
let device = Device::open(&path).expect("reopen");
device.enable_universal_planes().expect("universal planes");
device.enable_atomic().expect("atomic");
device
};
let mut renderer = paused.resume(&resumed_device).expect("resume");
assert_eq!(renderer.plane().plane_id, plane_before, "same plane");
assert_eq!(renderer.size(), size_before, "same size, not re-probed");
let mut request = AtomicRequest::new();
let written = renderer.stage_into(&mut request).expect("stage");
assert!(written >= 10);
let plane_id = renderer.plane().plane_id;
let mut store = drmkit_core::PropertyStore::new();
store
.cache_properties(&resumed_device, plane_id, drmkit_core::ObjectType::Plane)
.expect("props");
let fb = store.property_id(plane_id, "FB_ID").expect("FB_ID");
let value = request
.writes()
.iter()
.find(|w| w.object_id == plane_id && w.property_id == fb)
.map(|w| w.value)
.expect("FB_ID written");
assert_ne!(
value, 0,
"the resumed renderer names a framebuffer on the new descriptor"
);
let _ = renderer.tick(std::time::Duration::ZERO);
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn pausing_without_a_cursor_is_fine_vkms() {
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let Ok(renderer) = Renderer::create(&device, ®istry, &config(crtc_id, crtc_index)) else {
drmkit_testkit::skipped("no usable cursor path here");
return;
};
let paused = renderer.pause();
assert!(!paused.has_cursor());
let resumed = paused.resume(&device).expect("resume");
assert_ne!(resumed.plane().plane_id, 0);
}
fn drm_mappings() -> usize {
let maps = std::fs::read_to_string("/proc/self/maps").unwrap_or_default();
maps.lines()
.filter(|line| line.contains("/dev/dri/"))
.count()
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn repeated_pauses_do_not_leak_mappings_vkms() {
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let Ok(mut renderer) = Renderer::create(&device, ®istry, &config(crtc_id, crtc_index))
else {
drmkit_testkit::skipped("no usable cursor path here");
return;
};
renderer.set_cursor(square()).expect("set cursor");
renderer = renderer.pause().resume(&device).expect("resume");
let baseline = drm_mappings();
for _ in 0..16 {
renderer = renderer.pause().resume(&device).expect("resume");
}
let after = drm_mappings();
println!("note: {baseline} DRM mappings before, {after} after 16 cycles");
assert!(
after <= baseline,
"16 pause/resume cycles grew DRM mappings from {baseline} to {after}"
);
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn a_preferred_legacy_cursor_reserves_the_cursor_plane_vkms() {
use drm::control::Device as _;
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let Some(cursor_plane) = registry
.for_crtc(crtc_index)
.find(|plane| plane.plane_type == drmkit_planes::PlaneType::Cursor)
.map(|plane| plane.id)
else {
drmkit_testkit::skipped("no cursor plane on this CRTC");
return;
};
let atomic = Renderer::create(&device, ®istry, &config(crtc_id, crtc_index))
.expect("an atomic cursor");
assert_eq!(atomic.plane().path, PlanePath::AtomicCursor);
assert_eq!(atomic.reserved_plane_id(), atomic.plane().plane_id);
drop(atomic);
let preferred = RendererConfig {
prefer_legacy: true,
..config(crtc_id, crtc_index)
};
let Ok(mut renderer) = Renderer::create(&device, ®istry, &preferred) else {
drmkit_testkit::skipped("this driver has no legacy cursor");
return;
};
assert_eq!(renderer.plane().path, PlanePath::Legacy);
assert_eq!(renderer.reserved_plane_id(), cursor_plane);
assert_eq!(
renderer.set_rotation(drmkit_cursor::Rotation::Quarter),
Err(drmkit_cursor::CursorError::Unsupported)
);
renderer
.set_rotation(drmkit_cursor::Rotation::None)
.expect("upright is always fine");
renderer.set_cursor(square()).expect("set cursor");
renderer.move_to(10, 10);
renderer.commit().expect("install");
let handle = drm::control::plane::Handle::from(
std::num::NonZeroU32::new(cursor_plane).expect("non-zero"),
);
let lit = device
.get_plane(handle)
.expect("the cursor plane")
.framebuffer()
.is_some();
renderer.set_visible(false);
renderer.commit().expect("hide");
assert!(lit, "the legacy cursor did not land on the cursor plane");
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn a_cursor_off_the_top_left_corner_commits_vkms() {
use drm::control::Device as _;
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let resources = device.resource_handles().expect("resources");
let crtc = resources.crtcs()[crtc_index as usize];
let wanted = drmkit_testkit::connector::wanted(&device);
let Some((connector, mode)) = resources.connectors().iter().find_map(|handle| {
if wanted.is_some_and(|id| id != u32::from(*handle)) {
return None;
}
let info = device.get_connector(*handle, false).ok()?;
if info.state() != drm::control::connector::State::Connected {
return None;
}
Some((*handle, *info.modes().first()?))
}) else {
drmkit_testkit::skipped("no connected connector");
return;
};
let (width, height) = (u32::from(mode.size().0), u32::from(mode.size().1));
let primary = drmkit_dumb::Buffer::create(
&device,
&drmkit_dumb::Config {
width,
height,
fourcc: drmkit_fmt::fourcc::XRGB8888,
bpp: 32,
add_fb: true,
},
)
.expect("primary buffer");
let fb = drm::control::framebuffer::Handle::from(
core::num::NonZeroU32::new(primary.fb_id().expect("framebuffer")).expect("non-zero"),
);
device
.set_crtc(crtc, Some(fb), (0, 0), &[connector], Some(mode))
.expect("light the output");
let result = (|| {
let mut renderer =
Renderer::create(&device, ®istry, &config(crtc_id, crtc_index)).ok()?;
renderer.set_cursor(square()).expect("set cursor");
renderer.move_to(1, 1);
let committed = renderer.commit();
let plane_id = renderer.plane().plane_id;
let mut store = drmkit_core::PropertyStore::new();
let origin = store
.cache_properties(&device, plane_id, drmkit_core::ObjectType::Plane)
.ok()
.and_then(|()| {
Some((
store.property_value(plane_id, "CRTC_X").ok()?,
store.property_value(plane_id, "CRTC_Y").ok()?,
))
});
Some((committed, renderer.plane().path, origin))
})();
let _ = device.set_crtc(crtc, None, (0, 0), &[], None);
drop(primary);
match result {
None => drmkit_testkit::skipped("no usable cursor path here"),
Some((committed, path, origin)) => {
committed.expect("a cursor at the corner commits");
if path != drmkit_cursor::PlanePath::Legacy {
assert_eq!(origin, Some((0, 0)), "the plane was left off the corner");
}
}
}
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn dropping_a_legacy_renderer_takes_its_cursor_off_vkms() {
use drm::control::Device as _;
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let Some(cursor_plane) = registry
.all()
.iter()
.find(|p| p.plane_type == PlaneType::Cursor && p.possible_crtcs & (1 << crtc_index) != 0)
.map(|p| p.id)
else {
drmkit_testkit::skipped("no cursor plane to watch");
return;
};
let legacy_only = primary_only(®istry, crtc_index);
let Ok(mut renderer) = Renderer::create(&device, &legacy_only, &config(crtc_id, crtc_index))
else {
drmkit_testkit::skipped("this driver has no legacy cursor");
return;
};
renderer.set_cursor(square()).expect("set cursor");
renderer.move_to(10, 10);
renderer.commit().expect("install");
let shown = |device: &Device| {
let handle = drm::control::plane::Handle::from(
core::num::NonZeroU32::new(cursor_plane).expect("non-zero"),
);
device
.get_plane(handle)
.expect("plane")
.framebuffer()
.is_some()
};
assert!(
shown(&device),
"the install put a sprite on the cursor plane"
);
drop(renderer);
assert!(!shown(&device), "the sprite outlived its renderer");
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn a_new_legacy_cursor_is_installed_again_vkms() {
use drm::control::Device as _;
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let Some(cursor_plane) = registry
.all()
.iter()
.find(|p| p.plane_type == PlaneType::Cursor && p.possible_crtcs & (1 << crtc_index) != 0)
.map(|p| p.id)
else {
drmkit_testkit::skipped("no cursor plane to watch");
return;
};
let legacy_only = primary_only(®istry, crtc_index);
let Ok(mut renderer) = Renderer::create(&device, &legacy_only, &config(crtc_id, crtc_index))
else {
drmkit_testkit::skipped("this driver has no legacy cursor");
return;
};
let framebuffer = |device: &Device| {
let handle = drm::control::plane::Handle::from(
core::num::NonZeroU32::new(cursor_plane).expect("non-zero"),
);
device.get_plane(handle).expect("plane").framebuffer()
};
renderer.set_cursor(square()).expect("set cursor");
renderer.move_to(10, 10);
renderer.commit().expect("install");
let first = framebuffer(&device);
assert!(
first.is_some(),
"the install put a sprite on the cursor plane"
);
let other = Cursor::from_argb(&vec![0xff00_ff00; 16 * 16], 16, 16, 2, 2).expect("cursor");
renderer.set_cursor(other).expect("set another cursor");
renderer.move_to(20, 20);
renderer.commit().expect("move with the new shape");
assert_ne!(
framebuffer(&device),
first,
"the new shape was only moved, never handed to the CRTC"
);
renderer.set_visible(false);
renderer.commit().expect("hide");
}
#[test]
#[ignore = "needs a DRM device and DRM master"]
fn the_cursor_leaves_an_overlay_this_crtc_shows_vkms() {
use drm::control::Device as _;
let _guard = card_guard();
let Some((device, registry, crtc_id, crtc_index)) = fixture() else {
return;
};
let on_crtc = |p: &&PlaneCapabilities| p.possible_crtcs & (1 << crtc_index) != 0;
let overlays: Vec<u32> = registry
.all()
.iter()
.filter(on_crtc)
.filter(|p| p.plane_type == PlaneType::Overlay)
.map(|p| p.id)
.collect();
if overlays.len() < 2 {
drmkit_testkit::skipped("needs two overlays on this CRTC");
return;
}
let without_cursor = PlaneRegistry::from_capabilities(
registry
.all()
.iter()
.filter(|p| p.plane_type != PlaneType::Cursor)
.cloned()
.collect(),
);
let resources = device.resource_handles().expect("resources");
let crtc = resources.crtcs()[crtc_index as usize];
let Some((connector, mode)) = resources.connectors().iter().find_map(|handle| {
let info = device.get_connector(*handle, false).ok()?;
(info.state() == drm::control::connector::State::Connected)
.then(|| Some((*handle, *info.modes().first()?)))
.flatten()
}) else {
drmkit_testkit::skipped("no connected connector");
return;
};
let (width, height) = (u32::from(mode.size().0), u32::from(mode.size().1));
let buffer = |w: u32, h: u32, fourcc: u32| {
drmkit_dumb::Buffer::create(
&device,
&drmkit_dumb::Config {
width: w,
height: h,
fourcc,
bpp: 32,
add_fb: true,
},
)
.expect("buffer")
};
let fb_of = |b: &drmkit_dumb::Buffer| {
drm::control::framebuffer::Handle::from(
core::num::NonZeroU32::new(b.fb_id().expect("framebuffer")).expect("non-zero"),
)
};
let primary = buffer(width, height, drmkit_fmt::fourcc::XRGB8888);
let layer = buffer(64, 64, drmkit_fmt::fourcc::ARGB8888);
device
.set_crtc(
crtc,
Some(fb_of(&primary)),
(0, 0),
&[connector],
Some(mode),
)
.expect("light the output");
let shown = drm::control::plane::Handle::from(
core::num::NonZeroU32::new(overlays[0]).expect("non-zero"),
);
device
.set_plane(
shown,
crtc,
Some(fb_of(&layer)),
0,
(0, 0, 64, 64),
(0, 0, 64 << 16, 64 << 16),
)
.expect("show a layer on the first overlay");
let chosen = Renderer::create(&device, &without_cursor, &config(crtc_id, crtc_index))
.map(|renderer| renderer.plane().plane_id);
let _ = device.set_plane(shown, crtc, None, 0, (0, 0, 0, 0), (0, 0, 0, 0));
let _ = device.set_crtc(crtc, None, (0, 0), &[], None);
drop((primary, layer));
let chosen = chosen.expect("a free overlay is there to take");
assert_ne!(
chosen, overlays[0],
"the cursor took the overlay this CRTC was showing"
);
}