use std::time::Duration;
use drmkit_core::{AtomicCommitFlags, AtomicRequest, Device, ObjectType, PropertyStore};
use drmkit_dumb::{Buffer, Config, MapAccess};
use drmkit_planes::PlaneRegistry;
use crate::CursorError;
use crate::cursor::Cursor;
use crate::pingpong::PingPong;
use crate::plane::{PlanePath, SelectedPlane, select_plane};
use crate::size::{preferred_size, probe_size};
use crate::sprite::{self, Placement, Rotation};
use crate::stage::{PlaneState, stage};
#[derive(Debug, Clone, Copy)]
#[allow(
clippy::exhaustive_structs,
reason = "built by callers with `..Default::default()`, which a new field does not break"
)]
pub struct RendererConfig {
pub crtc_id: u32,
pub crtc_index: u32,
pub preferred_size: u32,
pub forced_plane_id: u32,
pub allow_legacy: bool,
pub prefer_legacy: bool,
pub rotation: Rotation,
}
impl Default for RendererConfig {
fn default() -> Self {
Self {
crtc_id: 0,
crtc_index: 0,
preferred_size: 0,
forced_plane_id: 0,
allow_legacy: true,
prefer_legacy: false,
rotation: Rotation::None,
}
}
}
#[derive(Debug)]
pub struct Renderer<'a> {
device: &'a Device,
crtc_id: u32,
plane: SelectedPlane,
rotation_mask: u64,
props: PropertyStore,
buffers: [Buffer; 2],
which: PingPong,
cursor: Option<Cursor>,
placement: Option<Placement>,
drawn_frame: Option<usize>,
position: (i32, i32),
visible: bool,
rotation: Rotation,
installed: bool,
}
impl<'a> Renderer<'a> {
pub fn create(
device: &'a Device,
registry: &PlaneRegistry,
config: &RendererConfig,
) -> Result<Self, CursorError> {
let forced = (config.forced_plane_id != 0).then_some(config.forced_plane_id);
let bound_elsewhere =
|plane_id: u32| plane_bound_elsewhere(device, plane_id, config.crtc_id);
let plane = select_plane(
registry,
config.crtc_index,
forced,
config.allow_legacy,
config.prefer_legacy,
&bound_elsewhere,
)?;
if plane.path == PlanePath::Legacy {
return Self::create_legacy(device, config, plane);
}
let mut props = PropertyStore::new();
props
.cache_properties(device, plane.plane_id, ObjectType::Plane)
.map_err(|error| CursorError::Io(error.to_string()))?;
let rotation_mask = registry
.by_id(plane.plane_id)
.map_or(0, |caps| caps.rotation_bits);
let cap = cursor_cap(device);
let preferred = preferred_size(config.preferred_size, plane.path, cap);
let mut buffers: Option<[Buffer; 2]> = None;
let size = probe_size(preferred, |candidate| {
match allocate_pair(device, candidate, plane.fourcc) {
Ok(pair) => {
let accepted = pair[0].fb_id().is_some_and(|fb_id| {
test_plane(device, &props, &plane, config.crtc_id, fb_id, candidate)
});
if accepted {
buffers = Some(pair);
}
accepted
}
Err(_) => false,
}
});
let buffers = match buffers {
Some(pair) => pair,
None => allocate_pair(device, size, plane.fourcc)?,
};
Ok(Self {
device,
crtc_id: config.crtc_id,
plane,
rotation_mask,
props,
buffers,
which: PingPong::new(),
cursor: None,
placement: None,
drawn_frame: None,
position: (0, 0),
visible: true,
rotation: config.rotation,
installed: false,
})
}
#[allow(
deprecated,
reason = "drmModeSetCursor is deprecated in favor of a cursor plane, \
which is what every other path here uses. This is the \
fallback for controllers that have no such plane."
)]
fn create_legacy(
device: &'a Device,
config: &RendererConfig,
plane: SelectedPlane,
) -> Result<Self, CursorError> {
use drm::control::Device as _;
if config.rotation != Rotation::None {
return Err(CursorError::Unsupported);
}
let crtc = crtc_handle(config.crtc_id).ok_or(CursorError::Unsupported)?;
if let Err(error) = device.set_cursor::<LegacyCursor>(crtc, None) {
return Err(match error.kind() {
std::io::ErrorKind::PermissionDenied => {
CursorError::Io("not DRM master".to_owned())
}
_ => CursorError::Unsupported,
});
}
let size = preferred_size(config.preferred_size, PlanePath::Legacy, None);
let buffers = allocate_pair(device, size, plane.fourcc)?;
Ok(Self {
device,
crtc_id: config.crtc_id,
plane,
rotation_mask: 0,
props: PropertyStore::new(),
buffers,
which: PingPong::single(),
cursor: None,
placement: None,
drawn_frame: None,
position: (0, 0),
visible: true,
rotation: Rotation::None,
installed: false,
})
}
#[must_use]
pub const fn plane(&self) -> SelectedPlane {
self.plane
}
#[must_use]
pub const fn reserved_plane_id(&self) -> u32 {
match self.plane.path {
PlanePath::Legacy => self.plane.legacy_plane_id,
PlanePath::AtomicCursor | PlanePath::AtomicOverlay => self.plane.plane_id,
}
}
#[must_use]
pub const fn size(&self) -> (u32, u32) {
(self.buffers[0].width(), self.buffers[0].height())
}
const fn rotates_in_hardware(&self) -> bool {
self.rotation_mask & self.rotation.drm_mask() != 0
}
const fn draw_rotation(&self) -> Rotation {
if self.rotates_in_hardware() {
Rotation::None
} else {
self.rotation
}
}
pub fn set_rotation(&mut self, rotation: Rotation) -> Result<(), CursorError> {
if rotation != Rotation::None && self.plane.path == PlanePath::Legacy {
return Err(CursorError::Unsupported);
}
self.rotation = rotation;
match self.drawn_frame {
Some(index) => self.draw(index),
None => Ok(()),
}
}
pub fn set_cursor(&mut self, cursor: Cursor) -> Result<(), CursorError> {
self.cursor = Some(cursor);
self.drawn_frame = None;
self.draw(0)
}
pub const fn move_to(&mut self, x: i32, y: i32) {
self.position = (x, y);
}
pub const fn set_visible(&mut self, visible: bool) {
self.visible = visible;
}
pub fn tick(&mut self, elapsed: Duration) -> Result<bool, CursorError> {
let Some(cursor) = self.cursor.as_ref() else {
return Ok(false);
};
if !cursor.is_animated() {
return Ok(false);
}
let wanted = cursor.frame_index_at(elapsed);
if self.drawn_frame == Some(wanted) {
return Ok(false);
}
self.draw(wanted)?;
Ok(true)
}
pub fn stage_into(&self, request: &mut AtomicRequest) -> Result<usize, CursorError> {
let buffer = &self.buffers[self.which.commit_target()];
let fb_id = if self.visible {
buffer.fb_id().unwrap_or(0)
} else {
0
};
let state = PlaneState {
plane_id: self.plane.plane_id,
crtc_id: if self.visible { self.crtc_id } else { 0 },
fb_id,
width: buffer.width(),
height: buffer.height(),
pointer_x: self.position.0,
pointer_y: self.position.1,
hotspot: self
.placement
.map(|placed| (placed.hotspot_x, placed.hotspot_y)),
rotation: if self.rotates_in_hardware() {
self.rotation
} else {
Rotation::None
},
rotation_mask: self.rotation_mask,
};
stage(request, &self.props, &state)
}
pub const fn published(&mut self) {
self.which.published();
}
pub fn commit(&mut self) -> Result<(), CursorError> {
if self.plane.path == PlanePath::Legacy {
return self.commit_legacy();
}
let mut request = AtomicRequest::new();
self.stage_into(&mut request)?;
let flags = if self.installed {
AtomicCommitFlags::empty()
} else {
AtomicCommitFlags::ALLOW_MODESET
};
request
.commit(self.device, flags)
.map_err(|error| CursorError::Io(error.to_string()))?;
self.installed = true;
self.published();
Ok(())
}
#[allow(
deprecated,
reason = "see create_legacy: this path exists for hardware with no \
cursor plane to use instead"
)]
fn commit_legacy(&mut self) -> Result<(), CursorError> {
use drm::control::Device as _;
let crtc = crtc_handle(self.crtc_id).ok_or(CursorError::Unsupported)?;
let hotspot = self
.placement
.map_or((0, 0), |placed| (placed.hotspot_x, placed.hotspot_y));
if !self.visible {
self.device
.set_cursor::<LegacyCursor>(crtc, None)
.map_err(|error| CursorError::Io(error.to_string()))?;
self.installed = false;
return Ok(());
}
if !self.installed {
let buffer = LegacyCursor {
handle: self.buffers[0]
.gem_handle()
.ok_or_else(|| CursorError::Io("cursor buffer has no handle".to_owned()))?,
width: self.buffers[0].width(),
height: self.buffers[0].height(),
pitch: self.buffers[0].stride(),
};
self.device
.set_cursor(crtc, Some(&buffer))
.map_err(|error| CursorError::Io(error.to_string()))?;
self.installed = true;
}
let (x, y) = crate::stage::plane_origin(self.position, hotspot);
self.device
.move_cursor(crtc, (x, y))
.map_err(|error| CursorError::Io(error.to_string()))?;
Ok(())
}
#[must_use]
pub fn pause(mut self) -> Paused {
let size = self.buffers[0].width();
for buffer in &mut self.buffers {
buffer.forget();
}
Paused {
crtc_id: self.crtc_id,
plane: self.plane,
rotation_mask: self.rotation_mask,
rotation: self.rotation,
cursor: self.cursor,
drawn_frame: self.drawn_frame,
position: self.position,
visible: self.visible,
size,
}
}
fn draw(&mut self, index: usize) -> Result<(), CursorError> {
let Some(cursor) = self.cursor.as_ref() else {
return Ok(());
};
let frame = cursor
.frames()
.get(index)
.unwrap_or_else(|| cursor.first())
.clone();
let rotation = self.draw_rotation();
let fourcc = self.plane.fourcc;
let target = self.which.draw_target();
let buffer = &mut self.buffers[target];
let (width, height) = (buffer.width(), buffer.height());
let stride_px = (buffer.stride() / 4) as usize;
let mut scratch = vec![0u32; height as usize * stride_px];
let placement = sprite::compose(&frame, rotation, &mut scratch, width, height, stride_px);
{
let mut mapping = buffer.map(MapAccess::Write);
for y in 0..height {
let row = mapping
.row_mut(y)
.ok_or_else(|| CursorError::Io("cursor buffer row out of range".to_owned()))?;
let from = y as usize * stride_px;
for (x, pixel) in scratch[from..from + width as usize].iter().enumerate() {
let at = x * 4;
row[at..at + 4].copy_from_slice(&pack(*pixel, fourcc).to_ne_bytes());
}
}
}
self.placement = Some(placement);
self.drawn_frame = Some(index);
self.which.mark_drawn();
Ok(())
}
}
fn cursor_cap(device: &Device) -> Option<u32> {
let cap = |which| {
drm::Device::get_driver_capability(device, which)
.ok()
.and_then(|value| u32::try_from(value).ok())
.filter(|value| *value != 0)
};
let width = cap(drm::DriverCapability::CursorWidth)?;
let height = cap(drm::DriverCapability::CursorHeight)?;
Some(width.min(height))
}
pub(crate) const fn pack(pixel: u32, fourcc: u32) -> u32 {
let (a, r, g, b) = (
pixel >> 24,
(pixel >> 16) & 0xff,
(pixel >> 8) & 0xff,
pixel & 0xff,
);
match fourcc {
drmkit_fmt::fourcc::RGBA8888 => (r << 24) | (g << 16) | (b << 8) | a,
drmkit_fmt::fourcc::ABGR8888 => (a << 24) | (b << 16) | (g << 8) | r,
drmkit_fmt::fourcc::BGRA8888 => (b << 24) | (g << 16) | (r << 8) | a,
_ => pixel,
}
}
struct LegacyCursor {
handle: u32,
width: u32,
height: u32,
pitch: u32,
}
impl drm::buffer::Buffer for LegacyCursor {
fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
fn format(&self) -> drm::buffer::DrmFourcc {
drm::buffer::DrmFourcc::Argb8888
}
fn pitch(&self) -> u32 {
self.pitch
}
fn handle(&self) -> drm::buffer::Handle {
drm::buffer::Handle::from(
core::num::NonZeroU32::new(self.handle)
.unwrap_or(core::num::NonZeroU32::new(1).expect("one is not zero")),
)
}
}
fn crtc_handle(crtc_id: u32) -> Option<drm::control::crtc::Handle> {
core::num::NonZeroU32::new(crtc_id).map(drm::control::crtc::Handle::from)
}
fn allocate_pair(device: &Device, size: u32, fourcc: u32) -> Result<[Buffer; 2], CursorError> {
let config = Config {
width: size,
height: size,
fourcc,
bpp: 32,
add_fb: true,
};
let first = Buffer::create(device, &config).map_err(|e| CursorError::Io(e.to_string()))?;
let second = Buffer::create(device, &config).map_err(|e| CursorError::Io(e.to_string()))?;
Ok([first, second])
}
fn test_plane(
device: &Device,
props: &PropertyStore,
plane: &SelectedPlane,
crtc_id: u32,
fb_id: u32,
size: u32,
) -> bool {
let mut request = AtomicRequest::new();
let state = PlaneState {
plane_id: plane.plane_id,
crtc_id,
fb_id,
width: size,
height: size,
pointer_x: 0,
pointer_y: 0,
hotspot: None,
rotation: Rotation::None,
rotation_mask: 0,
};
if stage(&mut request, props, &state).is_err() {
return false;
}
request.test(device, AtomicCommitFlags::empty()).is_ok()
}
fn plane_bound_elsewhere(device: &Device, plane_id: u32, crtc_id: u32) -> bool {
use drm::control::Device as _;
let Some(handle) = core::num::NonZeroU32::new(plane_id) else {
return false;
};
device
.get_plane(drm::control::plane::Handle::from(handle))
.ok()
.and_then(|info| info.crtc())
.is_some_and(|bound| u32::from(bound) != crtc_id)
}
#[derive(Debug)]
pub struct Paused {
crtc_id: u32,
plane: SelectedPlane,
rotation_mask: u64,
rotation: Rotation,
cursor: Option<Cursor>,
drawn_frame: Option<usize>,
position: (i32, i32),
visible: bool,
size: u32,
}
impl Paused {
pub fn resume(self, device: &Device) -> Result<Renderer<'_>, CursorError> {
let mut props = PropertyStore::new();
if self.plane.path != PlanePath::Legacy {
props
.cache_properties(device, self.plane.plane_id, ObjectType::Plane)
.map_err(|error| CursorError::Io(error.to_string()))?;
}
let mut renderer = Renderer {
device,
crtc_id: self.crtc_id,
plane: self.plane,
rotation_mask: self.rotation_mask,
props,
buffers: allocate_pair(device, self.size, self.plane.fourcc)?,
which: if self.plane.path == PlanePath::Legacy {
PingPong::single()
} else {
PingPong::new()
},
cursor: self.cursor,
placement: None,
drawn_frame: None,
position: self.position,
visible: self.visible,
rotation: self.rotation,
installed: false,
};
if renderer.cursor.is_some() {
renderer.draw(self.drawn_frame.unwrap_or(0))?;
}
Ok(renderer)
}
#[must_use]
pub const fn has_cursor(&self) -> bool {
self.cursor.is_some()
}
}