use super::*;
use std::sync::Mutex;
use drmkit_fmt::fourcc;
static CARD_LOCK: Mutex<()> = Mutex::new(());
fn card_guard() -> std::sync::MutexGuard<'static, ()> {
CARD_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn test_card() -> String {
std::env::var("DRMKIT_TEST_CARD").unwrap_or_else(|_| "/dev/dri/card0".to_owned())
}
fn open_card() -> drmkit_core::Device {
drmkit_core::Device::open(test_card()).expect("open test card")
}
#[test]
fn mapping_has_no_destructor() {
assert!(
!std::mem::needs_drop::<Mapping<'_>>(),
"Mapping must have no drop glue: invariant 6 requires the guard's \
destructor to be a no-op, and DumbScanoutSink's pacing depends on \
map() costing nothing per frame"
);
}
#[test]
fn empty_buffer_yields_an_empty_mapping() {
let mut buffer = Buffer::new();
let mapping = buffer.map(MapAccess::ReadWrite);
assert!(mapping.is_empty());
assert!(mapping.pixels().is_empty());
assert_eq!(mapping.stride(), 0);
assert_eq!(mapping.width(), 0);
assert_eq!(mapping.height(), 0);
}
#[test]
fn mapping_records_the_access_mode() {
let mut buffer = Buffer::new();
for access in [MapAccess::Read, MapAccess::Write, MapAccess::ReadWrite] {
assert_eq!(buffer.map(access).access(), access);
}
assert_eq!(MapAccess::default(), MapAccess::ReadWrite);
}
#[test]
fn default_buffer_is_empty() {
let buffer = Buffer::new();
assert!(buffer.is_empty());
assert_eq!(buffer.width(), 0);
assert_eq!(buffer.height(), 0);
assert_eq!(buffer.stride(), 0);
assert_eq!(buffer.size_bytes(), 0);
assert_eq!(buffer.gem_handle(), None);
assert_eq!(buffer.fb_id(), None);
assert!(buffer.data().is_empty());
assert!(Buffer::default().is_empty());
}
#[test]
fn forget_on_empty_is_safe() {
let mut buffer = Buffer::new();
buffer.forget();
buffer.forget(); assert!(buffer.is_empty());
}
#[test]
fn move_preserves_an_empty_buffer() {
let source = Buffer::new();
let moved = source;
assert!(moved.is_empty());
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn create_rejects_invalid_configs_vkms() {
let _guard = card_guard();
let device = open_card();
let base = Config {
width: 64,
height: 64,
fourcc: fourcc::XRGB8888,
..Config::default()
};
for (config, what) in [
(Config { width: 0, ..base }, "zero width"),
(Config { height: 0, ..base }, "zero height"),
(Config { fourcc: 0, ..base }, "zero format"),
(Config { bpp: 0, ..base }, "zero bpp"),
(
Config {
width: 16385,
..base
},
"width past 16384",
),
(
Config {
height: 16385,
..base
},
"height past 16384",
),
] {
assert!(
matches!(
Buffer::create(&device, &config),
Err(DumbError::InvalidConfig { .. })
),
"{what} must be rejected"
);
}
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn create_rejects_an_unknown_fourcc_vkms() {
let _guard = card_guard();
let device = open_card();
let config = Config {
width: 64,
height: 64,
fourcc: 0xdead_beef,
..Config::default()
};
assert!(matches!(
Buffer::create(&device, &config),
Err(DumbError::InvalidConfig { .. })
));
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn create_planar_rejects_unsupported_formats_vkms() {
let _guard = card_guard();
let device = open_card();
assert!(matches!(
Buffer::create_planar(&device, fourcc::XRGB8888, 64, 64),
Err(DumbError::UnsupportedFormat { .. })
));
assert!(
matches!(
Buffer::create_planar(&device, fourcc::YUV420, 64, 64),
Err(DumbError::UnsupportedFormat { .. })
),
"fully planar 4:2:0 is not in the semi-planar set"
);
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn create_allocates_maps_and_registers_a_framebuffer_vkms() {
let _guard = card_guard();
let device = open_card();
let mut buffer = Buffer::create(
&device,
&Config {
width: 64,
height: 64,
fourcc: fourcc::XRGB8888,
..Config::default()
},
)
.expect("allocate");
assert!(!buffer.is_empty());
assert_eq!(buffer.width(), 64);
assert_eq!(buffer.height(), 64);
assert!(buffer.stride() >= 64 * 4, "stride covers at least the row");
assert!(
buffer.size_bytes() >= buffer.stride() as usize * 64,
"the mapping covers the whole allocation"
);
assert!(buffer.gem_handle().is_some_and(|h| h != 0));
assert!(buffer.fb_id().is_some_and(|f| f != 0));
{
let mut view = buffer.map(MapAccess::Write);
assert_eq!(view.width(), 64);
assert_eq!(view.height(), 64);
view.row_mut(0).expect("row 0").fill(0xab);
view.row_mut(63).expect("row 63").fill(0xcd);
assert!(view.row_mut(64).is_none(), "past the bottom");
}
let view = buffer.map(MapAccess::Read);
assert!(view.row(0).expect("row 0").iter().all(|b| *b == 0xab));
assert!(view.row(63).expect("row 63").iter().all(|b| *b == 0xcd));
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn create_without_a_framebuffer_vkms() {
let _guard = card_guard();
let device = open_card();
let buffer = Buffer::create(
&device,
&Config {
width: 64,
height: 64,
fourcc: fourcc::ARGB8888,
add_fb: false,
..Config::default()
},
)
.expect("allocate");
assert!(buffer.gem_handle().is_some(), "the GEM handle still exists");
assert_eq!(buffer.fb_id(), None, "but no framebuffer was registered");
assert!(!buffer.data().is_empty());
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn create_planar_over_allocates_for_chroma_vkms() {
let _guard = card_guard();
let device = open_card();
let buffer = match Buffer::create_planar(&device, fourcc::NV12, 64, 64) {
Ok(buffer) => buffer,
Err(DumbError::Io(e)) => {
println!("note: driver rejected NV12 dumb framebuffer: {e}");
return;
}
Err(e) => panic!("unexpected failure: {e}"),
};
assert_eq!(buffer.height(), 64, "reports the image height");
let y_plane = buffer.stride() as usize * 64;
assert!(
buffer.size_bytes() >= y_plane + y_plane / 2,
"the allocation covers Y plus a half-height chroma plane: got {} for a \
{}-byte Y plane",
buffer.size_bytes(),
y_plane
);
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn forget_drops_handles_without_ioctls_vkms() {
let _guard = card_guard();
let device = open_card();
let mut buffer = Buffer::create(
&device,
&Config {
width: 32,
height: 32,
fourcc: fourcc::XRGB8888,
..Config::default()
},
)
.expect("allocate");
assert!(!buffer.is_empty());
buffer.forget();
assert!(buffer.is_empty());
assert_eq!(buffer.gem_handle(), None);
assert_eq!(buffer.fb_id(), None);
assert!(buffer.data().is_empty());
assert_eq!(buffer.size_bytes(), 0, "the mapping was torn down");
drop(buffer);
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn repeated_allocation_does_not_leak_vkms() {
let _guard = card_guard();
let device = open_card();
let config = Config {
width: 64,
height: 64,
fourcc: fourcc::XRGB8888,
..Config::default()
};
let allocate_and_drop = |times: usize| {
for _ in 0..times {
let buffer = Buffer::create(&device, &config).expect("allocate");
assert!(!buffer.is_empty());
drop(buffer);
}
};
let count = || {
std::fs::read_dir("/proc/self/fd")
.expect("/proc/self/fd")
.count()
};
allocate_and_drop(8);
let after_few = count();
allocate_and_drop(120);
let after_many = count();
let growth = after_many.saturating_sub(after_few);
assert!(
growth < 16,
"120 further allocate/drop cycles grew the fd table by {growth}"
);
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn create_planar_p010_allocates_and_registers_a_framebuffer_vkms() {
let _guard = card_guard();
let device = open_card();
let mut buffer = match Buffer::create_planar(&device, fourcc::P010, 64, 64) {
Ok(buffer) => buffer,
Err(error) => {
println!("note: skipped -- this driver refuses a P010 dumb buffer ({error})");
return;
}
};
assert!(buffer.gem_handle().is_some());
assert!(
buffer.fb_id().is_some_and(|id| id != 0),
"a P010 buffer the kernel would not register as a framebuffer cannot \
be scanned out, whatever the allocation says"
);
assert!(
buffer.stride() >= 64 * 2,
"two bytes per sample, not one -- a stride sized for 8-bit is half \
what P010 needs, and the driver reads the rest of every row from \
whatever follows the allocation"
);
assert_eq!(
buffer.height(),
64,
"the image height, not the allocated rows"
);
let stride = buffer.stride() as usize;
let mapping = buffer.data_mut();
assert!(
mapping.len() >= stride * 64,
"the luma plane alone needs height * stride"
);
for row in 0..64usize {
for column in 0..64usize {
let at = row * stride + column * 2;
if let Some(sample) = mapping.get_mut(at..at + 2) {
sample[0] = 0x00;
sample[1] = 0x40;
}
}
}
assert_eq!(mapping[1], 0x40, "the write reached the mapping");
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn report_whether_any_plane_scans_out_p010_vkms() {
use drm::control::Device as _;
let _guard = card_guard();
let device = open_card();
device.enable_universal_planes().ok();
let Ok(handles) = device.plane_handles() else {
println!("note: skipped -- planes could not be listed");
return;
};
let mut takers = 0usize;
for handle in &handles {
let Ok(info) = device.get_plane(*handle) else {
continue;
};
if info.formats().contains(&fourcc::P010) {
takers += 1;
}
}
println!("note: {takers} of {} planes advertise P010", handles.len());
}
#[test]
#[ignore = "needs a DRM device; run under the vkms lane with --include-ignored"]
fn dropping_a_buffer_after_its_device_closed_spares_the_next_one_vkms() {
use std::os::fd::{AsFd as _, AsRawFd as _};
let _guard = card_guard();
let config = Config {
width: 16,
height: 16,
fourcc: fourcc::XRGB8888,
..Config::default()
};
let before = open_card();
let number = before.as_fd().as_raw_fd();
let old = Buffer::create(&before, &config).expect("allocate on the old open");
drop(before);
let after = open_card();
if after.as_fd().as_raw_fd() != number {
println!("note: skipped -- the descriptor number was not reused");
return;
}
let new = Buffer::create(&after, &config).expect("allocate on the new open");
let handle = new.gem_handle().expect("a live buffer has a handle");
if Some(handle) != old.gem_handle() {
println!("note: the GEM handle was not reused");
}
drop(old);
assert!(
drm_ffi::mode::dumbbuffer::map(after.as_fd(), handle, 0, 0).is_ok(),
"dropping the old buffer destroyed GEM handle {handle} on the device that replaced its own"
);
}