use denise::{Frame, MAX_DAMAGE_RECTS, PixelFormat, Rect, Size, Surface, SurfaceError};
use drm::Device as _;
use drm::DriverCapability;
use drm::buffer::Buffer as _;
use drm::control::{
Device as ControlDevice, Event, Mode, PageFlipFlags, connector, crtc, dumbbuffer::DumbBuffer,
framebuffer,
};
use drm_fourcc::DrmFourcc;
use crate::device::Card;
use crate::error::DrmError;
use crate::mode::{self, ModePreference, OutputPreference};
use crate::swapchain::Swapchain;
const BPP: u32 = 32;
const DEPTH: u32 = 24;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PresentMode {
#[default]
Immediate,
Vsync,
}
#[derive(Clone, Copy, Debug)]
pub struct SurfaceConfig {
pub output: OutputPreference,
pub mode: ModePreference,
pub buffers: usize,
pub present_mode: PresentMode,
}
impl Default for SurfaceConfig {
fn default() -> Self {
Self {
output: OutputPreference::Auto,
mode: ModePreference::Preferred,
buffers: 2,
present_mode: PresentMode::Vsync,
}
}
}
#[derive(Debug)]
struct Scanout {
dumb: DumbBuffer,
fb: framebuffer::Handle,
ptr: *mut u32,
words: usize,
bytes: usize,
}
impl Scanout {
fn new(card: &Card, size: Size) -> Result<Self, DrmError> {
let mut dumb = card
.create_dumb_buffer((size.width, size.height), DrmFourcc::Xrgb8888, BPP)
.map_err(|source| DrmError::Allocate {
width: size.width,
height: size.height,
source,
})?;
let fb = card
.add_framebuffer(&dumb, DEPTH, BPP)
.map_err(DrmError::AddFramebuffer)?;
let (ptr, bytes) = {
let mut mapping = card.map_dumb_buffer(&mut dumb).map_err(DrmError::Map)?;
let slice: &mut [u8] = &mut mapping;
let ptr = slice.as_mut_ptr();
let bytes = slice.len();
core::mem::forget(mapping);
(ptr, bytes)
};
let mut scanout = Self {
dumb,
fb,
ptr: ptr.cast::<u32>(),
words: bytes / 4,
bytes,
};
scanout.pixels_mut().fill(0);
Ok(scanout)
}
fn pixels_mut(&mut self) -> &mut [u32] {
unsafe { core::slice::from_raw_parts_mut(self.ptr, self.words) }
}
}
const TEAR_FREE_NUMERATOR: u32 = 1;
const TEAR_FREE_DENOMINATOR: u32 = 4;
fn flip_flags_for(mode: PresentMode, damage: &[Rect], surface: Size) -> PageFlipFlags {
let synced = PageFlipFlags::EVENT;
let immediate = PageFlipFlags::EVENT | PageFlipFlags::ASYNC;
if mode == PresentMode::Vsync {
return synced;
}
if damage.is_empty() {
return synced;
}
let rows = damaged_rows(damage, surface);
if rows * TEAR_FREE_DENOMINATOR >= surface.height * TEAR_FREE_NUMERATOR {
synced
} else {
immediate
}
}
fn damaged_rows(damage: &[Rect], surface: Size) -> u32 {
if damage.len() > MAX_DAMAGE_RECTS {
return surface.height;
}
let bottom_edge = surface.height as i32;
let mut spans = [(0i32, 0i32); MAX_DAMAGE_RECTS];
let mut len = 0;
for rect in damage {
let top = rect.y.clamp(0, bottom_edge);
let bottom = rect.bottom().clamp(0, bottom_edge);
if bottom <= top {
continue;
}
let mut i = len;
while i > 0 && spans[i - 1].0 > top {
spans[i] = spans[i - 1];
i -= 1;
}
spans[i] = (top, bottom);
len += 1;
}
let mut rows: u32 = 0;
let mut i = 0;
while i < len {
let (start, mut end) = spans[i];
i += 1;
while i < len && spans[i].0 <= end {
end = end.max(spans[i].1);
i += 1;
}
rows += (end - start) as u32;
}
rows
}
#[derive(Debug)]
pub struct DrmSurface {
pub(crate) card: Card,
pub(crate) crtc: crtc::Handle,
connector: connector::Handle,
buffers: Vec<Scanout>,
swapchain: Swapchain,
size: Size,
stride: u32,
flip_pending: bool,
present_mode: PresentMode,
saved_crtc: Option<crtc::Info>,
mode_name: String,
pub(crate) cursor: Option<crate::cursor::CursorBuffer>,
}
impl DrmSurface {
pub fn new(card: Card, config: SurfaceConfig) -> Result<Self, DrmError> {
card.become_master()?;
let (handles, infos) = card.connectors()?;
let selection = mode::select(&infos, config.output, config.mode)?;
let connector = handles[selection.connector];
let crtc = card.crtc_for(connector)?;
let info = card
.get_connector(connector, false)
.map_err(DrmError::Resources)?;
let mode: Mode = info.modes()[selection.mode];
let (width, height) = mode.size();
let size = Size::new(u32::from(width), u32::from(height));
let saved_crtc = card.get_crtc(crtc).ok();
let mut buffers = Vec::with_capacity(config.buffers);
for _ in 0..Swapchain::new(config.buffers).count() {
buffers.push(Scanout::new(&card, size)?);
}
let pitch = buffers[0].dumb.pitch();
if !pitch.is_multiple_of(4) {
return Err(DrmError::UnalignedPitch { pitch });
}
let async_capable = card
.get_driver_capability(DriverCapability::ASyncPageFlip)
.is_ok_and(|supported| supported != 0);
let present_mode = match config.present_mode {
PresentMode::Immediate if async_capable => PresentMode::Immediate,
_ => PresentMode::Vsync,
};
card.set_crtc(crtc, Some(buffers[0].fb), (0, 0), &[connector], Some(mode))
.map_err(|source| DrmError::SetMode {
mode: format!("{width}x{height}"),
crtc: u32::from(crtc),
source,
})?;
let mut swapchain = Swapchain::new(config.buffers);
swapchain.presented();
Ok(Self {
card,
crtc,
connector,
buffers,
swapchain,
size,
stride: pitch / 4,
flip_pending: false,
present_mode,
saved_crtc,
mode_name: format!("{width}x{height}@{}", mode.vrefresh()),
cursor: None,
})
}
pub fn open(config: SurfaceConfig) -> Result<Self, DrmError> {
Self::new(Card::open_first()?, config)
}
pub fn card(&self) -> &Card {
&self.card
}
pub fn crtc(&self) -> drm::control::crtc::Handle {
self.crtc
}
pub fn mode_name(&self) -> &str {
&self.mode_name
}
pub fn stride(&self) -> u32 {
self.stride
}
pub fn buffer_count(&self) -> usize {
self.buffers.len()
}
pub fn present_mode(&self) -> PresentMode {
self.present_mode
}
fn wait_for_flip(&mut self) -> Result<(), DrmError> {
while self.flip_pending {
let events = self.card.receive_events().map_err(DrmError::WaitVblank)?;
for event in events {
if matches!(event, Event::PageFlip(_)) {
self.flip_pending = false;
}
}
}
Ok(())
}
}
impl Surface for DrmSurface {
fn size(&self) -> Size {
self.size
}
fn scale_factor(&self) -> f32 {
1.0
}
fn format(&self) -> PixelFormat {
PixelFormat::Xrgb8888
}
fn acquire(&mut self) -> Result<Frame<'_>, SurfaceError> {
self.wait_for_flip()?;
let index = self.swapchain.current();
let age = self.swapchain.age();
let size = self.size;
let stride = self.stride;
Frame::new(
self.buffers[index].pixels_mut(),
size,
stride,
PixelFormat::Xrgb8888,
age,
)
}
fn present(&mut self, damage: &[Rect]) -> Result<(), SurfaceError> {
let index = self.swapchain.current();
let fb = self.buffers[index].fb;
let flags = flip_flags_for(self.present_mode, damage, self.size);
self.card
.page_flip(self.crtc, fb, flags, None)
.map_err(DrmError::PageFlip)?;
self.flip_pending = true;
self.swapchain.presented();
Ok(())
}
}
impl Drop for DrmSurface {
fn drop(&mut self) {
let _ = self.wait_for_flip();
if let Some(saved) = self.saved_crtc.as_ref() {
let _ = self.card.set_crtc(
self.crtc,
saved.framebuffer(),
saved.position(),
&[self.connector],
saved.mode(),
);
}
if let Some(cursor) = self.cursor.take() {
#[allow(deprecated)]
let _ = self
.card
.set_cursor(self.crtc, None::<&drm::control::dumbbuffer::DumbBuffer>);
cursor.release(&self.card);
}
for buffer in self.buffers.drain(..) {
unsafe {
let _ = rustix::mm::munmap(buffer.ptr.cast::<core::ffi::c_void>(), buffer.bytes);
}
let _ = self.card.destroy_framebuffer(buffer.fb);
let _ = self.card.destroy_dumb_buffer(buffer.dumb);
}
self.card.release_master();
}
}
#[cfg(test)]
mod tests {
use super::*;
const SCREEN: Size = Size::new(1920, 1080);
#[test]
fn a_small_frame_still_flips_immediately() {
let button = [Rect::new(40, 700, 220, 48)];
assert_eq!(
flip_flags_for(PresentMode::Immediate, &button, SCREEN),
PageFlipFlags::EVENT | PageFlipFlags::ASYNC
);
}
#[test]
fn a_scrolled_viewport_waits_for_vblank() {
let viewport = [Rect::new(320, 60, 1560, 1000)];
assert_eq!(
flip_flags_for(PresentMode::Immediate, &viewport, SCREEN),
PageFlipFlags::EVENT,
"a frame that moved everything must not tear"
);
}
#[test]
fn a_narrow_column_down_the_screen_is_not_a_small_frame() {
let sidebar = [Rect::new(12, 52, 300, 1016)];
assert_eq!(
flip_flags_for(PresentMode::Immediate, &sidebar, SCREEN),
PageFlipFlags::EVENT,
"14.7% of the pixels, 94% of the rows: a tear crosses the lot"
);
}
#[test]
fn a_wide_shallow_band_may_still_tear() {
let toolbar = [Rect::new(0, 0, 1920, 40)];
assert_eq!(
flip_flags_for(PresentMode::Immediate, &toolbar, SCREEN),
PageFlipFlags::EVENT | PageFlipFlags::ASYNC
);
}
#[test]
fn scattered_damage_is_judged_by_what_it_covers() {
let corners = [Rect::new(0, 0, 60, 40), Rect::new(1860, 1040, 60, 40)];
assert_eq!(
flip_flags_for(PresentMode::Immediate, &corners, SCREEN),
PageFlipFlags::EVENT | PageFlipFlags::ASYNC,
"eighty rows in two places, not the thousand between them"
);
let neighbours = [Rect::new(40, 700, 220, 48), Rect::new(280, 700, 220, 48)];
assert_eq!(
flip_flags_for(PresentMode::Immediate, &neighbours, SCREEN),
PageFlipFlags::EVENT | PageFlipFlags::ASYNC,
"two buttons side by side are still two buttons"
);
}
#[test]
fn a_spinner_and_a_pointer_far_apart_are_two_small_things() {
let spinner = Rect::new(736, 46, 48, 48);
let cursor = Rect::new(910, 812, 16, 24);
let hovered = Rect::new(820, 780, 220, 48);
assert_eq!(
flip_flags_for(PresentMode::Immediate, &[spinner, cursor, hovered], SCREEN),
PageFlipFlags::EVENT | PageFlipFlags::ASYNC,
"a spinner, a cursor and a highlight cover well under a quarter"
);
}
#[test]
fn coverage_still_catches_the_frames_bounds_caught() {
let sidebar = [Rect::new(12, 52, 300, 1016)];
assert_eq!(damaged_rows(&sidebar, SCREEN), 1016);
let viewport = [Rect::new(320, 60, 1560, 1000)];
assert_eq!(damaged_rows(&viewport, SCREEN), 1000);
}
#[test]
fn overlapping_and_unsorted_rows_are_counted_once() {
let stacked = [
Rect::new(0, 300, 100, 100),
Rect::new(0, 100, 100, 100),
Rect::new(0, 350, 100, 100),
];
assert_eq!(
damaged_rows(&stacked, SCREEN),
250,
"100 at 100..200, then 150 at 300..450"
);
let abutting = [Rect::new(0, 100, 100, 50), Rect::new(0, 150, 100, 50)];
assert_eq!(damaged_rows(&abutting, SCREEN), 100, "one run, not two");
}
#[test]
fn rows_outside_the_surface_do_not_count() {
let overhang = [Rect::new(0, 1000, 100, 400)];
assert_eq!(damaged_rows(&overhang, SCREEN), 80);
let above = [Rect::new(0, -50, 100, 60)];
assert_eq!(damaged_rows(&above, SCREEN), 10);
let offscreen = [Rect::new(0, 1080, 100, 40)];
assert_eq!(damaged_rows(&offscreen, SCREEN), 0);
}
#[test]
fn an_oversized_list_is_treated_as_everything() {
let many = [Rect::new(0, 0, 8, 8); MAX_DAMAGE_RECTS + 1];
assert_eq!(damaged_rows(&many, SCREEN), SCREEN.height);
assert_eq!(
flip_flags_for(PresentMode::Immediate, &many, SCREEN),
PageFlipFlags::EVENT
);
}
#[test]
fn damage_nobody_declared_is_treated_as_everything() {
assert_eq!(
flip_flags_for(PresentMode::Immediate, &[], SCREEN),
PageFlipFlags::EVENT
);
}
#[test]
fn vsync_is_never_overridden() {
for damage in [&[][..], &[Rect::new(0, 0, 4, 4)][..]] {
assert_eq!(
flip_flags_for(PresentMode::Vsync, damage, SCREEN),
PageFlipFlags::EVENT
);
}
}
#[test]
fn the_threshold_is_a_quarter_of_the_rows() {
let screen = Size::new(1000, 1000);
let just_under = [Rect::new(0, 0, 8, 249)];
let just_over = [Rect::new(0, 0, 8, 250)];
assert_eq!(
flip_flags_for(PresentMode::Immediate, &just_under, screen),
PageFlipFlags::EVENT | PageFlipFlags::ASYNC
);
assert_eq!(
flip_flags_for(PresentMode::Immediate, &just_over, screen),
PageFlipFlags::EVENT
);
}
}