From 9fe331178a4c28dde6ff35be4a5891b6d0004e99 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Fri, 3 Jul 2026 08:29:44 -0700 Subject: [PATCH] chore: move to hardware cursor --- .../src/compositor/cursor.rs | 45 +++- .../src/compositor/mod.rs | 8 - .../src/compositor/runtime.rs | 9 + .../src/compositor/udev.rs | 10 +- .../src/compositor/udev/device.rs | 252 ++++++++++++++++-- 5 files changed, 286 insertions(+), 38 deletions(-) diff --git a/crates/hearthspace-compositor/src/compositor/cursor.rs b/crates/hearthspace-compositor/src/compositor/cursor.rs index 6a7923b..917736d 100644 --- a/crates/hearthspace-compositor/src/compositor/cursor.rs +++ b/crates/hearthspace-compositor/src/compositor/cursor.rs @@ -22,23 +22,20 @@ pub(super) struct SoftwareCursor { pub(super) hotspot: (i32, i32), } +pub(in crate::compositor) struct CursorImage { + pub(in crate::compositor) width: u32, + pub(in crate::compositor) height: u32, + pub(in crate::compositor) hotspot: (i32, i32), + pub(in crate::compositor) pixels_rgba: Vec, +} + const DEFAULT_CURSOR: &[u8] = include_bytes!("../../../../assets/cursors/default"); const DESIRED_CURSOR_SIZE: u32 = 24; pub(super) fn standard_software_cursor() -> SoftwareCursor { - let images = - parse_xcursor(DEFAULT_CURSOR).expect("packaged default cursor must be valid Xcursor data"); - let image = images - .iter() - .min_by_key(|image| image.size.abs_diff(DESIRED_CURSOR_SIZE)) - .expect("packaged default cursor must contain at least one image"); - + let image = standard_cursor_image(); let width = i32::try_from(image.width).expect("cursor width must fit in i32"); let height = i32::try_from(image.height).expect("cursor height must fit in i32"); - let hotspot = ( - i32::try_from(image.xhot).expect("cursor x hotspot must fit in i32"), - i32::try_from(image.yhot).expect("cursor y hotspot must fit in i32"), - ); let buffer = MemoryRenderBuffer::from_slice( &image.pixels_rgba, @@ -49,5 +46,29 @@ pub(super) fn standard_software_cursor() -> SoftwareCursor { None, ); - SoftwareCursor { buffer, hotspot } + SoftwareCursor { + buffer, + hotspot: image.hotspot, + } +} + +pub(in crate::compositor) fn standard_cursor_image() -> CursorImage { + let images = + parse_xcursor(DEFAULT_CURSOR).expect("packaged default cursor must be valid Xcursor data"); + let image = images + .iter() + .min_by_key(|image| image.size.abs_diff(DESIRED_CURSOR_SIZE)) + .expect("packaged default cursor must contain at least one image"); + + let hotspot = ( + i32::try_from(image.xhot).expect("cursor x hotspot must fit in i32"), + i32::try_from(image.yhot).expect("cursor y hotspot must fit in i32"), + ); + + CursorImage { + width: image.width, + height: image.height, + hotspot, + pixels_rgba: image.pixels_rgba.clone(), + } } diff --git a/crates/hearthspace-compositor/src/compositor/mod.rs b/crates/hearthspace-compositor/src/compositor/mod.rs index cd939f7..f776bd9 100644 --- a/crates/hearthspace-compositor/src/compositor/mod.rs +++ b/crates/hearthspace-compositor/src/compositor/mod.rs @@ -202,14 +202,6 @@ struct App { software_cursor: SoftwareCursor, } -impl App { - #[cfg(feature = "udev")] - pub(in crate::compositor) fn enable_software_cursor(&mut self) { - self.software_cursor_visible = true; - self.request_redraw(); - } -} - pub(in crate::compositor) struct AppInit { pub(in crate::compositor) display: Display, pub(in crate::compositor) event_loop: EventLoop<'static, CalloopData>, diff --git a/crates/hearthspace-compositor/src/compositor/runtime.rs b/crates/hearthspace-compositor/src/compositor/runtime.rs index 13617c5..fe1c518 100644 --- a/crates/hearthspace-compositor/src/compositor/runtime.rs +++ b/crates/hearthspace-compositor/src/compositor/runtime.rs @@ -109,6 +109,7 @@ pub(in crate::compositor) fn run_event_loop( data: &mut CalloopData, ) -> Result<(), Box> { info!("entering compositor event loop"); + data.update_native_hardware_cursor(); if let Err(error) = data.render() { warn!(%error, "initial compositor render failed"); } @@ -151,6 +152,7 @@ pub(in crate::compositor) fn run_event_loop( data.state.handle_idle_transitions(); data.state.advance_viewport_animation(); data.apply_cursor_icon(); + data.update_native_hardware_cursor(); if data.state.needs_redraw { if let Err(error) = data.render() { @@ -183,6 +185,13 @@ impl CalloopData { } } + fn update_native_hardware_cursor(&mut self) { + #[cfg(feature = "udev")] + if let Backend::Udev(backend) = &mut self.backend { + backend.update_hardware_cursor(&self.state); + } + } + /// Push the compositor's desired cursor to the host winit window, but only /// when it differs from the cursor currently shown. fn apply_cursor_icon(&mut self) { diff --git a/crates/hearthspace-compositor/src/compositor/udev.rs b/crates/hearthspace-compositor/src/compositor/udev.rs index bfc3b09..9b64e1d 100644 --- a/crates/hearthspace-compositor/src/compositor/udev.rs +++ b/crates/hearthspace-compositor/src/compositor/udev.rs @@ -152,7 +152,6 @@ pub fn run_udev(options: RunOptions) -> Result<(), Box> { .map(UdevDevice::output_descriptors) .unwrap_or_default(); app.sync_connector_outputs(&dh, output_descriptors); - app.enable_software_cursor(); app.configure_shell_bars(); app.reconcile_pointer_after_output_geometry_change(); app.request_redraw(); @@ -369,6 +368,15 @@ fn insert_libinput_source( } impl UdevBackendState { + pub(in crate::compositor) fn update_hardware_cursor(&mut self, state: &super::App) { + if self.drm_commits_paused { + return; + } + if let Some(device) = self.primary_device.as_mut() { + device.update_hardware_cursor(state); + } + } + pub(in crate::compositor) fn render_frame( &mut self, state: &mut super::App, diff --git a/crates/hearthspace-compositor/src/compositor/udev/device.rs b/crates/hearthspace-compositor/src/compositor/udev/device.rs index fd31d52..9713395 100644 --- a/crates/hearthspace-compositor/src/compositor/udev/device.rs +++ b/crates/hearthspace-compositor/src/compositor/udev/device.rs @@ -1,10 +1,10 @@ -use std::{io, path::PathBuf}; +use std::{cmp, io, path::PathBuf}; use smithay::{ backend::{ allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}, allocator::{Format, Fourcc}, - drm::{DrmDevice, DrmDeviceFd, DrmSurface, GbmBufferedSurface}, + drm::{DrmDevice, DrmDeviceFd, GbmBufferedSurface}, egl::{EGLContext, EGLDisplay}, renderer::{ImportDma, damage::OutputDamageTracker, gles::GlesRenderer}, session::{Session, libseat::LibSeatSession}, @@ -12,13 +12,22 @@ use smithay::{ }, output::{PhysicalProperties, Subpixel}, reexports::{ - drm::control::{Device as ControlDevice, Mode, connector, crtc, dumbbuffer}, + drm::{ + Device as BasicDrmDevice, DriverCapability, + buffer::Buffer as DrmBuffer, + control::{ + AtomicCommitFlags, Device as ControlDevice, Mode, PlaneType, atomic::AtomicModeReq, + connector, crtc, dumbbuffer, property, + }, + }, rustix::fs::{OFlags, stat}, }, - utils::{DeviceFd, Physical, Size, Transform}, + utils::{DeviceFd, Logical, Physical, Point, Rectangle, Size, Transform}, }; use tracing::{debug, error, info, warn}; +use super::super::cursor::{CursorImage, standard_cursor_image}; + pub(super) struct UdevDevice { pub(super) path: PathBuf, pub(super) render_node: RenderNode, @@ -27,6 +36,10 @@ pub(super) struct UdevDevice { pub(super) output_targets: Vec, pub(super) output_target: Option, pub(super) output_surfaces: Vec, + hardware_cursor: Option, + hardware_cursor_initialized: bool, + hardware_cursor_crtc: Option, + hardware_cursor_position: Option<(i32, i32)>, } pub(super) struct RenderNode { @@ -47,6 +60,11 @@ pub(super) struct KmsOutputSurface { pub(super) frame_dirty: bool, } +struct HardwareCursor { + buffer: dumbbuffer::DumbBuffer, + hotspot: (i32, i32), +} + #[derive(Debug, Clone, PartialEq)] pub(super) struct KmsOutputTarget { pub(super) connector: connector::Handle, @@ -206,6 +224,9 @@ impl UdevDevice { ) -> Result<(), Box> { self.output_surfaces.clear(); self.output_target = targets.first().cloned(); + self.hardware_cursor_initialized = false; + self.hardware_cursor_crtc = None; + self.hardware_cursor_position = None; if targets.is_empty() { warn!(path = %self.path.display(), "no KMS output target selected; native scanout disabled"); @@ -227,6 +248,53 @@ impl UdevDevice { Ok(()) } + pub(super) fn update_hardware_cursor(&mut self, state: &super::super::App) { + let Some(cursor) = self.hardware_cursor.as_ref() else { + return; + }; + + if !self.hardware_cursor_initialized { + clear_all_drm_cursor_planes(&self.scanout_node); + for output in &self.output_surfaces { + clear_legacy_cursor(&self.scanout_node, output.target.crtc); + } + self.hardware_cursor_initialized = true; + } + + let pointer = state.pointer_location; + let target = self.output_surfaces.iter().find_map(|output| { + let view = state.output_render_view(output.target.connector_name())?; + let rect = Rectangle::new(view.location, view.size); + point_in_logical_rect(pointer, rect).then(|| { + let x = (pointer.x - f64::from(view.location.x)).round() as i32 - cursor.hotspot.0; + let y = (pointer.y - f64::from(view.location.y)).round() as i32 - cursor.hotspot.1; + (output.target.crtc, (x, y)) + }) + }); + + let Some((crtc, position)) = target else { + if let Some(active_crtc) = self.hardware_cursor_crtc.take() { + clear_legacy_cursor(&self.scanout_node, active_crtc); + } + self.hardware_cursor_position = None; + return; + }; + + if self.hardware_cursor_crtc != Some(crtc) { + if let Some(active_crtc) = self.hardware_cursor_crtc { + clear_legacy_cursor(&self.scanout_node, active_crtc); + } + set_legacy_cursor(&self.scanout_node, crtc, cursor); + self.hardware_cursor_crtc = Some(crtc); + self.hardware_cursor_position = None; + } + + if self.hardware_cursor_position != Some(position) { + move_legacy_cursor(&self.scanout_node, crtc, position); + self.hardware_cursor_position = Some(position); + } + } + pub(super) fn has_output_surfaces(&self) -> bool { !self.output_surfaces.is_empty() } @@ -260,7 +328,6 @@ fn create_output_surface( target.mode, std::slice::from_ref(&target.connector), )?; - clear_hardware_cursor(scanout_node, &surface, &target); info!(connector = ?target.connector, crtc = ?target.crtc, mode = ?target.mode, "created DRM surface"); let gbm_device = GbmDevice::new(render_node.drm_fd.clone())?; @@ -290,30 +357,176 @@ fn create_output_surface( }) } -fn clear_hardware_cursor( - scanout_node: &ScanoutNode, - surface: &DrmSurface, - target: &KmsOutputTarget, -) { +fn set_legacy_cursor(scanout_node: &ScanoutNode, crtc: crtc::Handle, cursor: &HardwareCursor) { + #[allow(deprecated)] + match scanout_node.drm_fd.set_cursor(crtc, Some(&cursor.buffer)) { + Ok(()) => debug!(?crtc, "set legacy DRM cursor"), + Err(error) => debug!(?crtc, %error, "legacy DRM cursor set failed"), + } +} + +fn move_legacy_cursor(scanout_node: &ScanoutNode, crtc: crtc::Handle, position: (i32, i32)) { + #[allow(deprecated)] + match scanout_node.drm_fd.move_cursor(crtc, position) { + Ok(()) => debug!(?crtc, ?position, "moved legacy DRM cursor"), + Err(error) => debug!(?crtc, ?position, %error, "legacy DRM cursor move failed"), + } +} + +fn clear_legacy_cursor(scanout_node: &ScanoutNode, crtc: crtc::Handle) { #[allow(deprecated)] match scanout_node .drm_fd - .set_cursor(target.crtc, Option::<&dumbbuffer::DumbBuffer>::None) + .set_cursor(crtc, Option::<&dumbbuffer::DumbBuffer>::None) + { + Ok(()) => debug!(?crtc, "cleared legacy DRM cursor"), + Err(error) => debug!(?crtc, %error, "legacy DRM cursor clear failed"), + } +} + +fn clear_all_drm_cursor_planes(scanout_node: &ScanoutNode) { + let Ok(planes) = scanout_node.drm_device.plane_handles() else { + debug!("failed to query DRM planes for cursor clear"); + return; + }; + + let mut request = AtomicModeReq::new(); + let mut cursor_planes = Vec::new(); + for plane in planes { + let Ok(properties) = scanout_node.drm_device.get_properties(plane) else { + debug!( + ?plane, + "failed to query DRM plane properties for cursor clear" + ); + continue; + }; + let Ok(property_info) = properties.as_hashmap(&scanout_node.drm_device) else { + debug!( + ?plane, + "failed to map DRM plane properties for cursor clear" + ); + continue; + }; + let Some(type_property) = property_info.get("type") else { + continue; + }; + let Some((_, type_value)) = properties + .iter() + .find(|(property, _)| **property == type_property.handle()) + else { + continue; + }; + if *type_value != PlaneType::Cursor as u64 { + continue; + } + let Some(crtc_id) = property_info.get("CRTC_ID") else { + continue; + }; + let Some(fb_id) = property_info.get("FB_ID") else { + continue; + }; + request.add_property(plane, crtc_id.handle(), property::Value::CRTC(None)); + request.add_property(plane, fb_id.handle(), property::Value::Framebuffer(None)); + cursor_planes.push(plane); + } + + if cursor_planes.is_empty() { + return; + } + + match scanout_node + .drm_device + .atomic_commit(AtomicCommitFlags::empty(), request) { - Ok(()) => debug!(crtc = ?target.crtc, "cleared legacy DRM cursor plane"), - Err(error) => debug!(crtc = ?target.crtc, %error, "legacy DRM cursor clear failed"), + Ok(()) => debug!(?cursor_planes, "cleared all DRM cursor planes"), + Err(error) => debug!(?cursor_planes, %error, "all DRM cursor plane clear failed"), } +} - for plane in surface.planes().cursor.iter().map(|plane| plane.handle) { - match surface.clear_plane(plane) { - Ok(()) => debug!(crtc = ?target.crtc, ?plane, "queued atomic DRM cursor plane clear"), +fn create_hardware_cursor(scanout_node: &ScanoutNode) -> Option { + let image = standard_cursor_image(); + let width = cursor_capability(scanout_node, DriverCapability::CursorWidth) + .unwrap_or(image.width) + .max(image.width); + let height = cursor_capability(scanout_node, DriverCapability::CursorHeight) + .unwrap_or(image.height) + .max(image.height); + let mut buffer = + match scanout_node + .drm_device + .create_dumb_buffer((width, height), Fourcc::Argb8888, 32) + { + Ok(buffer) => buffer, Err(error) => { - debug!(crtc = ?target.crtc, ?plane, %error, "atomic DRM cursor plane clear failed") + warn!(%error, width, height, "failed to create DRM hardware cursor buffer"); + return None; } + }; + + let pitch = usize::try_from(buffer.pitch()).ok()?; + let mut mapping = match scanout_node.drm_device.map_dumb_buffer(&mut buffer) { + Ok(mapping) => mapping, + Err(error) => { + warn!(%error, "failed to map DRM hardware cursor buffer"); + return None; + } + }; + mapping.fill(0); + copy_cursor_rgba_to_argb8888(&image, mapping.as_mut(), pitch, width, height); + drop(mapping); + + let hotspot = ( + cmp::min(image.hotspot.0.max(0), width.saturating_sub(1) as i32), + cmp::min(image.hotspot.1.max(0), height.saturating_sub(1) as i32), + ); + info!( + width, + height, + ?hotspot, + "created DRM hardware cursor buffer" + ); + Some(HardwareCursor { buffer, hotspot }) +} + +fn cursor_capability(scanout_node: &ScanoutNode, capability: DriverCapability) -> Option { + scanout_node + .drm_device + .get_driver_capability(capability) + .ok() + .and_then(|value| u32::try_from(value).ok()) + .filter(|value| *value > 0) +} + +fn copy_cursor_rgba_to_argb8888( + image: &CursorImage, + target: &mut [u8], + pitch: usize, + cursor_width: u32, + cursor_height: u32, +) { + let copy_width = cmp::min(image.width, cursor_width) as usize; + let copy_height = cmp::min(image.height, cursor_height) as usize; + let image_width = image.width as usize; + for y in 0..copy_height { + for x in 0..copy_width { + let source = (y * image_width + x) * 4; + let destination = y * pitch + x * 4; + let [red, green, blue, alpha] = image.pixels_rgba[source..source + 4] else { + continue; + }; + // DRM_FORMAT_ARGB8888 is stored as BGRA bytes on little-endian hosts. + target[destination..destination + 4].copy_from_slice(&[blue, green, red, alpha]); } } } +fn point_in_logical_rect(point: Point, rect: Rectangle) -> bool { + point.x >= f64::from(rect.loc.x) + && point.y >= f64::from(rect.loc.y) + && point.x < f64::from(rect.loc.x + rect.size.w) + && point.y < f64::from(rect.loc.y + rect.size.h) +} + pub(super) fn create_udev_device( session: &mut LibSeatSession, path: PathBuf, @@ -339,7 +552,12 @@ pub(super) fn create_udev_device( output_targets: Vec::new(), output_target: None, output_surfaces: Vec::new(), + hardware_cursor: None, + hardware_cursor_initialized: false, + hardware_cursor_crtc: None, + hardware_cursor_position: None, }; + device.hardware_cursor = create_hardware_cursor(&device.scanout_node); device.output_targets = device.connected_output_targets(); device.rebuild_output_surfaces(device.output_targets.clone())?; Ok((device, notifier)) -- 2.51.2