general improvement and bug fixes, cursor scaling, DMA-BUF routing and nested backend recovery

ignore performance-baseline.md

more improvement and code quality fixes
This commit is contained in:
Samuel Onoja committed 2026-10-04 18:33:05 +01:00
1 parent 405c74af9f
commit 73f21e20ce
23 files changed
+2287 -457

No files matched your search

+29 -1
View File
@@ -107,6 +107,16 @@ impl DirectBackendState {
self.active && !self.topology.reconciling && !self.lid.pending()
}
pub(crate) fn process_dmabuf_imports(&mut self, imports: &mut crate::dmabuf_imports::ImportManager) {
if !self.active || self.topology.reconciling {
return;
}
for device in self.devices.values_mut().filter(|device| device.drm.is_active()) {
imports.process(device.dmabuf_global, &mut device.renderer, Some(device.render_node));
}
}
pub(crate) fn scene_output(&self, state: &Ferese, output: &Output) -> Output {
let source = self
.devices
@@ -950,6 +960,7 @@ fn open_device(state: &mut Ferese, path: &Path) -> Result<DrmNode, Box<dyn Error
let dmabuf_global = state
.dmabuf_state
.create_global_with_default_feedback::<Ferese>(&display_handle, &feedback);
state.dmabuf_imports.register(dmabuf_global);
state
.direct_backend
.as_mut()
@@ -1088,6 +1099,23 @@ fn cursor_affected_outputs(state: &Ferese) -> Vec<Output> {
.collect()
}
pub(crate) fn cursor_animation_visible(state: &Ferese) -> bool {
if let Some(backend) = state.direct_backend.as_ref() {
backend.can_render()
&& backend.devices.values().any(|device| {
device
.outputs
.values()
.any(|output| output.power.can_render() && redraw::cursor_on_output(state, &output.output))
})
} else {
state
.space
.outputs()
.any(|output| redraw::cursor_on_output(state, output))
}
}
#[track_caller]
pub(crate) fn render_surface(
state: &mut Ferese,
@@ -1272,7 +1300,6 @@ fn render_output(
let frame = state.sample_frame(&scene_output, horizon);
let rendered = {
(|| -> Result<bool, Box<dyn Error>> {
state.process_dmabuf_imports(&mut device.renderer, Some(device.render_node));
let elements = if output.mirror_source.is_some() {
mirror::elements(state, &mut device.renderer, &scene_output, &frame, &mut output)?
} else {
@@ -1562,6 +1589,7 @@ fn remove_device(state: &mut Ferese, node: DrmNode) {
};
state.loop_handle.remove(device.notifier);
state.dmabuf_imports.remove(device.dmabuf_global);
state
.dmabuf_state
.disable_global::<Ferese>(&state.display_handle, &device.dmabuf_global);
+1 -1
View File
@@ -67,7 +67,7 @@ pub(super) fn capture_mirror(
}
}
fn capture_request<T, E: Display>(
pub(crate) fn capture_request<T, E: Display>(
state: &mut Ferese,
output: &Output,
include_cursor: bool,
+6 -17
View File
@@ -123,22 +123,11 @@ pub(super) fn cursor_on_output(state: &crate::Ferese, output: &smithay::output::
.to_f64()
}
CursorImageStatus::Named(icon) => {
let Some(cursor) = state.named_cursors.get(icon) else {
CursorImageStatus::Named(_) => {
let Some(cursor) = state.named_cursor_frame() else {
return false;
};
let scale = output.current_scale().fractional_scale();
// Named buffers use buffer scale 1: Smithay scales their size
// with the output, while the hotspot is subtracted physically.
let size = cursor.size;
Rectangle::<f64, Logical>::new(
(
location.x - f64::from(cursor.hotspot.x) / scale,
location.y - f64::from(cursor.hotspot.y) / scale,
)
.into(),
(f64::from(size.w), f64::from(size.h)).into(),
)
cursor.logical_rect(location)
}
CursorImageStatus::Hidden => return false,
@@ -353,9 +342,9 @@ mod tests {
);
assert!(cursor_on_output(&state, &outputs[0]));
assert!(cursor_on_output(&state, &outputs[1]), "cursor image straddles the seam");
if let CursorImageStatus::Named(icon) = &state.cursor_status {
let cursor = state.named_cursors.get(icon).unwrap();
let x = 1920.0 - f64::from(cursor.size.w) * 0.75 + f64::from(cursor.hotspot.x) / 2.0;
if let Some(cursor) = state.named_cursor_frame() {
let x = 1920.0 - f64::from(cursor.size.w) / f64::from(cursor.buffer_scale) * 0.75
+ f64::from(cursor.hotspot.x) / f64::from(cursor.buffer_scale);
pointer.motion(
&mut state,
None,
+424 -82
View File
@@ -1,49 +1,328 @@
use std::fs;
use std::sync::OnceLock;
use std::fs::File;
use std::io::Read;
use std::sync::{Arc, Condvar, Mutex, mpsc};
use std::time::{Duration, Instant};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::memory::MemoryRenderBuffer;
use smithay::input::pointer::CursorIcon;
use smithay::utils::{Buffer, Point, Size, Transform};
use smithay::input::pointer::{CursorIcon, CursorImageStatus};
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::calloop::{LoopSignal, RegistrationToken};
use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Size, Transform};
use xcursor::CursorTheme;
use xcursor::parser::Image;
const DEFAULT_CURSOR_SIZE: u32 = 24;
const MAX_FILE_BYTES: usize = 8 * 1024 * 1024;
const MAX_FRAMES: usize = 256;
const MAX_PIXELS: usize = 2 * 1024 * 1024;
const MAX_CACHE_BYTES: usize = 32 * 1024 * 1024;
#[derive(Debug, Clone)]
pub(crate) struct NamedCursor {
pub(crate) struct CursorFrame {
pub buffer: MemoryRenderBuffer,
pub hotspot: Point<i32, Buffer>,
pub size: Size<i32, Buffer>,
pub buffer_scale: i32,
delay: Duration,
}
pub(crate) fn cursor_theme() -> CursorTheme {
let name = std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into());
impl CursorFrame {
pub(crate) fn logical_rect(&self, pointer: Point<f64, Logical>) -> Rectangle<f64, Logical> {
Rectangle::new(
pointer
- Point::from((
f64::from(self.hotspot.x) / f64::from(self.buffer_scale),
f64::from(self.hotspot.y) / f64::from(self.buffer_scale),
)),
(
f64::from(self.size.w) / f64::from(self.buffer_scale),
f64::from(self.size.h) / f64::from(self.buffer_scale),
)
.into(),
)
}
CursorTheme::load(&name)
}
pub(crate) fn load_named_cursor(theme: &CursorTheme, icon: CursorIcon) -> NamedCursor {
let requested_size = cursor_size();
let image = cursor_names(icon)
.iter()
.find_map(|name| load_image(theme, name, requested_size));
match image {
Some(image) => cursor_from_image(image),
None => fallback_cursor(),
pub(crate) fn physical_location(&self, pointer: Point<f64, Logical>, scale: f64) -> Point<f64, Physical> {
self.logical_rect(pointer).loc.to_physical(scale)
}
}
fn cursor_size() -> u32 {
static SIZE: OnceLock<u32> = OnceLock::new();
#[derive(Debug, Clone)]
pub(crate) struct NamedCursor {
frames: Arc<[CursorFrame]>,
bytes: usize,
requested_scale: i32,
}
*SIZE.get_or_init(|| {
std::env::var("XCURSOR_SIZE")
.ok()
.and_then(|value| value.parse().ok())
.filter(|size| *size > 0)
.unwrap_or(DEFAULT_CURSOR_SIZE)
impl NamedCursor {
fn sample(&self, elapsed: Duration) -> (usize, Option<Duration>) {
if self.frames.len() <= 1 {
return (0, None);
}
let period: Duration = self.frames.iter().map(|frame| frame.delay).sum();
let mut offset = elapsed.as_nanos() % period.as_nanos();
for (index, frame) in self.frames.iter().enumerate() {
if offset < frame.delay.as_nanos() {
return (index, Some(frame.delay - Duration::from_nanos(offset as u64)));
}
offset -= frame.delay.as_nanos();
}
unreachable!("cursor phase is within the bounded, nonzero period")
}
}
#[derive(Default)]
struct Requests {
pending: Option<(CursorIcon, i32)>,
in_flight: Option<(CursorIcon, i32)>,
closed: bool,
}
struct Loaded {
icon: CursorIcon,
scale: i32,
cursor: Option<NamedCursor>,
}
pub(crate) struct Loader {
shared: Arc<(Mutex<Requests>, Condvar)>,
results: mpsc::Receiver<Loaded>,
}
impl Loader {
pub(crate) fn new(signal: LoopSignal) -> std::io::Result<Self> {
Self::with_loader(signal, {
// Theme discovery and inherited-theme reads belong to the worker too.
let mut theme = None;
move |(icon, scale)| {
let theme = theme.get_or_insert_with(|| {
CursorTheme::load(&std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into()))
});
load_named_cursor(theme, icon, scale)
}
})
}
fn with_loader(
signal: LoopSignal,
mut load: impl FnMut((CursorIcon, i32)) -> Option<NamedCursor> + Send + 'static,
) -> std::io::Result<Self> {
let shared = Arc::new((
Mutex::new(Requests {
pending: Some((CursorIcon::Default, 1)),
..Requests::default()
}),
Condvar::new(),
));
let worker = shared.clone();
let (sender, results) = mpsc::sync_channel(1);
std::thread::Builder::new()
.name("ferese-cursors".into())
.spawn(move || {
loop {
let (requests, ready) = &*worker;
let mut requests = ready
.wait_while(requests.lock().unwrap(), |requests| {
!requests.closed && requests.pending.is_none()
})
.unwrap();
if requests.closed {
break;
}
let request = requests.pending.take().unwrap();
requests.in_flight = Some(request);
drop(requests);
let cursor = load(request);
let (icon, scale) = request;
if sender.send(Loaded { icon, scale, cursor }).is_err() {
break;
}
signal.wakeup();
worker.0.lock().unwrap().in_flight = None;
}
})?;
Ok(Self { shared, results })
}
pub(crate) fn request(&self, icon: CursorIcon, scale: i32) {
let request = (icon, scale);
let mut requests = self.shared.0.lock().unwrap();
if requests.in_flight == Some(request) || requests.pending == Some(request) {
return;
}
requests.pending = Some(request);
self.shared.1.notify_one();
}
}
impl Drop for Loader {
fn drop(&mut self) {
let mut requests = self.shared.0.lock().unwrap();
requests.closed = true;
requests.pending = None;
self.shared.1.notify_one();
}
}
pub(crate) struct Animation {
icon: Option<CursorIcon>,
epoch: Instant,
frame: usize,
timer: Option<RegistrationToken>,
pub(crate) reset: bool,
pub(crate) buffer_scale: i32,
}
impl Default for Animation {
fn default() -> Self {
Self {
icon: None,
epoch: Instant::now(),
frame: 0,
timer: None,
reset: false,
buffer_scale: 1,
}
}
}
impl crate::Ferese {
pub(crate) fn named_cursor_frame(&self) -> Option<&CursorFrame> {
let CursorImageStatus::Named(icon) = self.cursor_status else {
return None;
};
let cursor = self
.named_cursors
.get(&icon)
.or_else(|| self.named_cursors.get(&CursorIcon::Default))?;
let frame = if self.cursor_animation.icon == Some(icon) {
self.cursor_animation.frame
} else {
0
};
cursor.frames.get(frame).or_else(|| cursor.frames.first())
}
pub(crate) fn update_named_cursor(&mut self, now: Instant) {
self.cursor_animation.buffer_scale = if let Some(backend) = self.direct_backend.as_ref() {
backend
.physical_outputs()
.map(|output| output.current_scale().fractional_scale().ceil() as i32)
.max()
} else {
self.space
.outputs()
.map(|output| output.current_scale().fractional_scale().ceil() as i32)
.max()
}
.unwrap_or(1)
.clamp(1, 8);
while let Ok(Loaded { icon, scale, cursor }) = self.cursor_loader.results.try_recv() {
let mut cursor = cursor.unwrap_or_else(fallback_cursor);
cursor.requested_scale = scale;
let active = match self.cursor_status {
CursorImageStatus::Named(icon) => Some(icon),
_ => None,
};
let affects_current = active == Some(icon)
|| (icon == CursorIcon::Default
&& active.is_some_and(|active| !self.named_cursors.contains_key(&active)));
let mut bytes: usize = self.named_cursors.values().map(|cursor| cursor.bytes).sum();
while bytes + cursor.bytes > MAX_CACHE_BYTES {
let Some(evict) = self
.named_cursors
.keys()
.copied()
.find(|key| *key != CursorIcon::Default && *key != icon && Some(*key) != active)
else {
break;
};
bytes -= self.named_cursors.remove(&evict).unwrap().bytes;
}
self.named_cursors.insert(icon, cursor);
if affects_current {
self.cursor_animation.reset = true;
self.cursor_redraw_pending |= crate::backends::direct::cursor_animation_visible(self);
}
}
let icon = match self.cursor_status {
CursorImageStatus::Named(icon) => Some(icon),
_ => None,
};
if let Some(icon) = icon
&& self
.named_cursors
.get(&icon)
.is_none_or(|cursor| cursor.requested_scale != self.cursor_animation.buffer_scale)
{
self.cursor_loader.request(icon, self.cursor_animation.buffer_scale);
}
let animated = icon
.and_then(|icon| {
self.named_cursors
.get(&icon)
.or_else(|| self.named_cursors.get(&CursorIcon::Default))
})
.is_some_and(|cursor| cursor.frames.len() > 1);
let visible = animated && crate::backends::direct::cursor_animation_visible(self);
if !visible || self.cursor_animation.icon != icon || self.cursor_animation.reset {
if let Some(token) = self.cursor_animation.timer.take() {
self.loop_handle.remove(token);
}
self.cursor_animation.icon = icon.filter(|_| visible);
self.cursor_animation.epoch = now;
self.cursor_animation.frame = 0;
self.cursor_animation.reset = false;
}
if !visible {
return;
}
let icon = icon.unwrap();
let cursor = self
.named_cursors
.get(&icon)
.or_else(|| self.named_cursors.get(&CursorIcon::Default))
.unwrap();
let (frame, remaining) = cursor.sample(now.saturating_duration_since(self.cursor_animation.epoch));
if self.cursor_animation.frame != frame {
self.cursor_animation.frame = frame;
self.cursor_redraw_pending = true;
}
if let Some(remaining) = remaining
&& self.cursor_animation.timer.is_none()
{
match self
.loop_handle
.insert_source(Timer::from_deadline(now + remaining), |_, _, state| {
state.cursor_animation.timer = None;
state.cursor_redraw_pending = true;
TimeoutAction::Drop
}) {
Ok(token) => self.cursor_animation.timer = Some(token),
Err(error) => tracing::warn!(%error, "could not schedule animated cursor"),
}
}
}
}
fn load_named_cursor(theme: &CursorTheme, icon: CursorIcon, scale: i32) -> Option<NamedCursor> {
let requested = std::env::var("XCURSOR_SIZE")
.ok()
.and_then(|size| size.parse::<u32>().ok())
.filter(|size| *size > 0 && *size <= 1024)
.unwrap_or(DEFAULT_CURSOR_SIZE);
cursor_names(icon).iter().find_map(|name| {
let path = theme.load_icon(name)?;
let file = File::open(path).ok()?;
let metadata = file.metadata().ok()?;
if !metadata.is_file() || metadata.len() > MAX_FILE_BYTES as u64 {
return None;
}
let mut bytes = Vec::new();
file.take((MAX_FILE_BYTES + 1) as u64).read_to_end(&mut bytes).ok()?;
parse_cursor(&bytes, requested, scale)
})
}
@@ -56,37 +335,113 @@ fn cursor_names(icon: CursorIcon) -> [&'static str; 3] {
}
}
fn load_image(theme: &CursorTheme, name: &str, requested_size: u32) -> Option<Image> {
let path = theme.load_icon(name)?;
let bytes = fs::read(path).ok()?;
xcursor::parser::parse_xcursor(&bytes)?.into_iter().min_by_key(|image| {
(
image.size.abs_diff(requested_size),
image.width.abs_diff(requested_size),
)
// Inspect every image before allocating buffers, then decode only the selected
// nominal size. TOC aliases count toward the budget just like separate frames.
fn parse_cursor(bytes: &[u8], requested: u32, scale: i32) -> Option<NamedCursor> {
fn word(bytes: &[u8], offset: usize) -> Option<u32> {
Some(u32::from_le_bytes(
bytes.get(offset..offset.checked_add(4)?)?.try_into().ok()?,
))
}
if bytes.len() > MAX_FILE_BYTES || bytes.get(..4)? != b"Xcur" {
return None;
}
let header = word(bytes, 4)? as usize;
let count = word(bytes, 12)? as usize;
if header < 16 || count > MAX_FRAMES || header.checked_add(count.checked_mul(12)?)? > bytes.len() {
return None;
}
let mut images = Vec::new();
let mut total_pixels = 0usize;
for i in 0..count {
let toc = header + i * 12;
if word(bytes, toc)? != 0xfffd0002 {
continue;
}
let nominal = word(bytes, toc + 4)?;
let pos = word(bytes, toc + 8)? as usize;
if word(bytes, pos)? != 36
|| word(bytes, pos.checked_add(4)?)? != 0xfffd0002
|| word(bytes, pos.checked_add(8)?)? != nominal
|| word(bytes, pos.checked_add(12)?)? != 1
{
return None;
}
let width = word(bytes, pos + 16)?;
let height = word(bytes, pos + 20)?;
let xhot = word(bytes, pos + 24)?;
let yhot = word(bytes, pos + 28)?;
if nominal == 0 || width == 0 || height == 0 || width > 1024 || height > 1024 || xhot > width || yhot > height {
return None;
}
let pixels = (width as usize).checked_mul(height as usize)?;
total_pixels = total_pixels.checked_add(pixels)?;
if total_pixels > MAX_PIXELS {
return None;
}
let start = pos.checked_add(36)?;
let end = start.checked_add(pixels.checked_mul(4)?)?;
let rgba = bytes.get(start..end)?;
images.push((nominal, width, height, xhot, yhot, word(bytes, pos + 32)?, rgba));
}
let base = images
.iter()
.map(|image| image.0)
.min_by_key(|size| (size.abs_diff(requested), *size))?;
let scale = scale.clamp(1, 8);
let desired = base.checked_mul(scale as u32)?;
let nominal = images
.iter()
.map(|image| image.0)
.min_by_key(|size| (size.abs_diff(desired), *size))?;
let mut frames = Vec::new();
let mut asset_bytes = 0usize;
for (_, width, height, xhot, yhot, delay, rgba) in images.into_iter().filter(|image| image.0 == nominal) {
let ratio = f64::from(desired) / f64::from(nominal);
let w = (f64::from(width) * ratio).round().max(1.0) as u32;
let h = (f64::from(height) * ratio).round().max(1.0) as u32;
if w > 1024 || h > 1024 {
return None;
}
let size: Size<i32, Buffer> = (w as i32, h as i32).into();
asset_bytes = asset_bytes.checked_add((w as usize).checked_mul(h as usize)?.checked_mul(4)?)?;
if asset_bytes > MAX_PIXELS * 4 {
return None;
}
let resized;
let pixels = if w != width || h != height {
let original = image::RgbaImage::from_raw(width, height, rgba.to_vec())?;
resized = image::imageops::resize(&original, w, h, image::imageops::FilterType::Triangle);
resized.as_raw().as_slice()
} else {
rgba
};
frames.push(CursorFrame {
buffer: MemoryRenderBuffer::from_slice(pixels, Fourcc::Abgr8888, size, scale, Transform::Normal, None),
hotspot: (
(f64::from(xhot) * ratio).round() as i32,
(f64::from(yhot) * ratio).round() as i32,
)
.into(),
size,
buffer_scale: scale,
// Zero-delay themes need a finite cadence rather than a busy loop.
delay: Duration::from_millis(if delay == 0 { 16 } else { u64::from(delay) }),
});
}
Some(NamedCursor {
frames: frames.into(),
bytes: asset_bytes,
requested_scale: scale,
})
}
fn cursor_from_image(image: Image) -> NamedCursor {
let size = Size::<i32, Buffer>::from((image.width as i32, image.height as i32));
let buffer = MemoryRenderBuffer::from_slice(&image.pixels_rgba, Fourcc::Abgr8888, size, 1, Transform::Normal, None);
NamedCursor {
buffer,
hotspot: Point::from((image.xhot as i32, image.yhot as i32)),
size,
}
}
fn fallback_cursor() -> NamedCursor {
pub(crate) fn fallback_cursor() -> NamedCursor {
const WIDTH: usize = 12;
const HEIGHT: usize = 18;
let mut pixels = vec![0_u8; WIDTH * HEIGHT * 4];
for y in 0..HEIGHT {
let body_width = (y / 2 + 1).min(8);
for x in 0..body_width {
let edge = x == 0 || x + 1 == body_width || y == 0;
let offset = (y * WIDTH + x) * 4;
@@ -94,39 +449,26 @@ fn fallback_cursor() -> NamedCursor {
pixels[offset..offset + 4].copy_from_slice(&[color, color, color, 255]);
}
}
NamedCursor {
buffer: MemoryRenderBuffer::from_slice(
&pixels,
Fourcc::Abgr8888,
Size::<i32, Buffer>::from((WIDTH as i32, HEIGHT as i32)),
1,
Transform::Normal,
None,
),
hotspot: Point::default(),
size: (WIDTH as i32, HEIGHT as i32).into(),
frames: vec![CursorFrame {
buffer: MemoryRenderBuffer::from_slice(
&pixels,
Fourcc::Abgr8888,
(WIDTH as i32, HEIGHT as i32),
1,
Transform::Normal,
None,
),
hotspot: Point::default(),
size: (WIDTH as i32, HEIGHT as i32).into(),
buffer_scale: 1,
delay: Duration::from_millis(16),
}]
.into(),
bytes: pixels.len(),
requested_scale: 1,
}
}
#[cfg(test)]
mod tests {
use smithay::input::pointer::CursorIcon;
use super::cursor_names;
#[test]
fn supplies_legacy_fallback_names_for_common_cursors() {
assert_eq!(cursor_names(CursorIcon::Default), ["default", "left_ptr", "arrow"]);
assert_eq!(cursor_names(CursorIcon::Pointer), ["pointer", "hand2", "left_ptr"]);
assert_eq!(cursor_names(CursorIcon::Text), ["text", "xterm", "left_ptr"]);
}
#[test]
fn falls_back_to_default_for_other_named_cursors() {
assert_eq!(
cursor_names(CursorIcon::Crosshair),
["crosshair", "default", "left_ptr"]
);
}
}
mod tests;
+217
View File
@@ -0,0 +1,217 @@
use super::*;
use smithay::input::SeatHandler;
use smithay::input::pointer::MotionEvent;
use smithay::output::{Mode, Output, PhysicalProperties, Subpixel};
use smithay::reexports::calloop::EventLoop;
use smithay::utils::SERIAL_COUNTER;
fn file(frames: &[(u32, u32, u32, u32)]) -> Vec<u8> {
let mut bytes = b"Xcur".to_vec();
bytes.extend([16u32, 1, frames.len() as u32].into_iter().flat_map(u32::to_le_bytes));
let mut pos = 16 + frames.len() * 12;
for &(size, width, _, _) in frames {
bytes.extend([0xfffd0002, size, pos as u32].into_iter().flat_map(u32::to_le_bytes));
pos += 36 + width as usize * width as usize * 4;
}
for &(size, width, hotspot, delay) in frames {
bytes.extend(
[36, 0xfffd0002, size, 1, width, width, hotspot, hotspot, delay]
.into_iter()
.flat_map(u32::to_le_bytes),
);
bytes.extend([255, 255, 255, 255].repeat(width as usize * width as usize));
}
bytes
}
#[test]
fn hotspot_and_footprint_use_the_images_scale() {
let bytes = file(&[(24, 24, 12, 50), (48, 48, 24, 50)]);
for scale in [1.0f64, 1.25, 2.0] {
let asset = parse_cursor(&bytes, 24, scale.ceil() as i32).unwrap();
let frame = &asset.frames[0];
let pointer: Point<f64, Logical> = (100.0, 120.0).into();
let location = frame.physical_location(pointer, scale);
let hotspot = f64::from(frame.hotspot.x) * scale / f64::from(frame.buffer_scale);
assert_eq!(location.x + hotspot, pointer.x * scale);
assert_eq!(location.y + hotspot, pointer.y * scale);
assert_eq!(frame.logical_rect(pointer).size, (24.0, 24.0).into());
if scale > 1.0 {
assert_eq!(frame.size.w, 48);
}
}
}
#[test]
fn unavailable_hidpi_size_preserves_logical_size_and_hotspot() {
let asset = parse_cursor(&file(&[(32, 32, 16, 50)]), 24, 2).unwrap();
let frame = &asset.frames[0];
assert_eq!(frame.buffer_scale, 2);
assert_eq!(frame.size.w, 64);
assert_eq!(frame.hotspot.x, 32);
assert_eq!(frame.logical_rect((100.0, 100.0).into()).size, (32.0, 32.0).into());
}
#[test]
fn selects_a_nominal_sequence_and_preserves_frame_delays() {
let asset = parse_cursor(&file(&[(48, 48, 24, 70), (24, 24, 12, 30), (24, 24, 12, 80)]), 24, 1).unwrap();
assert_eq!(asset.frames.len(), 2);
for (time, index, remaining) in [
(0, 0, 30),
(29, 0, 1),
(30, 1, 80),
(109, 1, 1),
(110, 0, 30),
(400, 1, 40),
] {
assert_eq!(
asset.sample(Duration::from_millis(time)),
(index, Some(Duration::from_millis(remaining)))
);
}
let still = parse_cursor(&file(&[(24, 24, 12, 0)]), 24, 1).unwrap();
assert_eq!(still.sample(Duration::from_secs(300)), (0, None));
let zero = parse_cursor(&file(&[(24, 24, 12, 0), (24, 24, 12, 0)]), 24, 1).unwrap();
assert_eq!(zero.sample(Duration::from_millis(16)).0, 1);
}
#[test]
fn rejects_oversized_and_malformed_assets_before_buffer_allocation() {
assert!(parse_cursor(&vec![0; MAX_FILE_BYTES + 1], 24, 1).is_none());
let mut bytes = file(&[(24, 24, 12, 30)]);
bytes[12..16].copy_from_slice(&((MAX_FRAMES + 1) as u32).to_le_bytes());
assert!(parse_cursor(&bytes, 24, 1).is_none());
let mut bytes = file(&[(24, 24, 12, 30)]);
bytes[16 + 8..16 + 12].copy_from_slice(&u32::MAX.to_le_bytes());
assert!(parse_cursor(&bytes, 24, 1).is_none());
let mut bytes = file(&[(24, 24, 12, 30)]);
bytes[28 + 16..28 + 20].copy_from_slice(&2048u32.to_le_bytes());
assert!(parse_cursor(&bytes, 24, 1).is_none());
// Repeated TOC references must not bypass the aggregate pixel limit.
let mut bytes = file(&[(1024, 1024, 0, 30)]);
let frame = bytes.split_off(28);
bytes.truncate(16);
bytes[12..16].copy_from_slice(&3u32.to_le_bytes());
for _ in 0..3 {
bytes.extend([0xfffd0002, 1024, 52].into_iter().flat_map(u32::to_le_bytes));
}
bytes.extend(frame);
assert!(parse_cursor(&bytes, 24, 1).is_none());
assert!(parse_cursor(&file(&[(24, 24, 12, 30)])[..40], 24, 1).is_none());
}
#[test]
fn delayed_loading_does_not_block_cursor_callbacks_or_pointer_dispatch() {
if !crate::startup_tests::private_runtime(
"cursor::tests::delayed_loading_does_not_block_cursor_callbacks_or_pointer_dispatch",
) {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
let (started, calls) = mpsc::channel();
let (release, held) = mpsc::channel();
state.cursor_loader = Loader::with_loader(events.get_signal(), move |request| {
started.send(request).unwrap();
if request.0 == CursorIcon::Default {
held.recv_timeout(Duration::from_secs(2)).unwrap();
}
None
})
.unwrap();
assert_eq!(
calls.recv_timeout(Duration::from_secs(2)).unwrap().0,
CursorIcon::Default
);
let seat = state.seat.clone();
state.cursor_image(&seat, CursorImageStatus::Named(CursorIcon::Pointer));
state.cursor_image(&seat, CursorImageStatus::Named(CursorIcon::Text));
let pointer = state.seat.get_pointer().unwrap();
pointer.motion(
&mut state,
None,
&MotionEvent {
location: (10.0, 20.0).into(),
serial: SERIAL_COUNTER.next_serial(),
time: 1,
},
);
state.cursor_image(&seat, CursorImageStatus::Named(CursorIcon::Text));
crate::after_dispatch_with_redraw(&mut state, |_, _| {});
assert_eq!(pointer.current_location(), (10.0, 20.0).into());
assert!(
state.named_cursor_frame().is_some(),
"cached fallback must remain visible"
);
assert!(calls.try_recv().is_err(), "decoder is still deliberately blocked");
release.send(()).unwrap();
assert_eq!(calls.recv_timeout(Duration::from_secs(2)).unwrap().0, CursorIcon::Text);
}
#[test]
fn cursor_animation_advances_and_cancels_on_hide_and_shape_change() {
if !crate::startup_tests::private_runtime(
"cursor::tests::cursor_animation_advances_and_cancels_on_hide_and_shape_change",
) {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
// No background result can replace the synthetic fixture.
state.cursor_loader = Loader::with_loader(events.get_signal(), |_| None).unwrap();
let output = Output::new(
"cursor-test".into(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
output.change_current_state(
Some(Mode {
size: (800, 600).into(),
refresh: 60_000,
}),
None,
None,
None,
);
state.space.map_output(&output, (0, 0));
let asset = parse_cursor(&file(&[(24, 24, 12, 30), (24, 24, 12, 80)]), 24, 1).unwrap();
state.named_cursors.insert(CursorIcon::Wait, asset);
state.cursor_status = CursorImageStatus::Named(CursorIcon::Wait);
let now = Instant::now();
state.update_named_cursor(now);
assert_eq!(state.cursor_animation.frame, 0);
assert!(state.cursor_animation.timer.is_some());
// Invoke the same deadline callback by dispatching the real timer.
while Instant::now() < now + Duration::from_millis(35) {
events.dispatch(Duration::from_millis(5), &mut state).unwrap();
}
state.update_named_cursor(now + Duration::from_millis(30));
assert_eq!(state.cursor_animation.frame, 1);
assert!(state.cursor_animation.timer.is_some());
let seat = state.seat.clone();
state.cursor_image(&seat, CursorImageStatus::Hidden);
state.update_named_cursor(now + Duration::from_millis(31));
assert!(state.cursor_animation.timer.is_none());
state.cursor_image(&seat, CursorImageStatus::Named(CursorIcon::Wait));
state.update_named_cursor(now + Duration::from_millis(32));
assert!(state.cursor_animation.timer.is_some());
state.named_cursors.insert(CursorIcon::Text, fallback_cursor());
state.cursor_image(&seat, CursorImageStatus::Named(CursorIcon::Text));
state.update_named_cursor(now + Duration::from_millis(33));
assert!(state.cursor_animation.timer.is_none());
}
#[test]
fn supplies_legacy_fallback_names_for_common_cursors() {
assert_eq!(cursor_names(CursorIcon::Default), ["default", "left_ptr", "arrow"]);
assert_eq!(cursor_names(CursorIcon::Pointer), ["pointer", "hand2", "left_ptr"]);
assert_eq!(cursor_names(CursorIcon::Text), ["text", "xterm", "left_ptr"]);
assert_eq!(
cursor_names(CursorIcon::Crosshair),
["crosshair", "default", "left_ptr"]
);
}
+293
View File
@@ -0,0 +1,293 @@
use std::collections::{HashSet, VecDeque};
use std::time::{Duration, Instant};
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::drm::DrmNode;
use smithay::backend::renderer::ImportDma;
use smithay::reexports::calloop::RegistrationToken;
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::wayland_server::backend::ClientId;
use smithay::wayland::dmabuf::{DmabufGlobal, ImportNotifier};
use crate::Ferese;
const MAX_PENDING: usize = 64;
const MAX_PER_CLIENT: usize = 16;
const MAX_WAIT: Duration = Duration::from_secs(5);
struct Pending<K, C, T> {
owner: K,
client: C,
deadline: Instant,
value: T,
}
struct Queue<K, C, T> {
pending: VecDeque<Pending<K, C, T>>,
}
impl<K, C, T> Default for Queue<K, C, T> {
fn default() -> Self {
Self {
pending: VecDeque::new(),
}
}
}
impl<K: Eq, C: Eq, T> Queue<K, C, T> {
fn enqueue(&mut self, owner: K, client: C, value: T, now: Instant) -> Result<(), T> {
if self.pending.len() >= MAX_PENDING
|| self.pending.iter().filter(|entry| entry.client == client).count() >= MAX_PER_CLIENT
{
return Err(value);
}
self.pending.push_back(Pending {
owner,
client,
value,
deadline: now + MAX_WAIT,
});
Ok(())
}
fn process(&mut self, owner: &K, mut complete: impl FnMut(T)) {
self.remove_if(|entry| &entry.owner == owner, |entry| complete(entry.value));
}
fn remove_if(
&mut self,
mut remove: impl FnMut(&Pending<K, C, T>) -> bool,
mut complete: impl FnMut(Pending<K, C, T>),
) {
for _ in 0..self.pending.len() {
let entry = self.pending.pop_front().unwrap();
if remove(&entry) {
complete(entry);
} else {
self.pending.push_back(entry);
}
}
}
fn deadline(&self) -> Option<Instant> {
self.pending.front().map(|entry| entry.deadline)
}
}
struct Request {
buffer: Dmabuf,
notifier: ImportNotifier,
}
#[derive(Default)]
pub(crate) struct ImportManager {
globals: HashSet<DmabufGlobal>,
queue: Queue<DmabufGlobal, ClientId, Request>,
timer: Option<RegistrationToken>,
}
impl Drop for ImportManager {
fn drop(&mut self) {
self.queue.remove_if(|_| true, |entry| entry.value.notifier.failed());
}
}
impl ImportManager {
pub(crate) fn register(&mut self, global: DmabufGlobal) {
self.globals.insert(global);
}
pub(crate) fn remove(&mut self, global: DmabufGlobal) {
self.globals.remove(&global);
self.queue.process(&global, |request| request.notifier.failed());
}
pub(crate) fn enqueue(&mut self, global: DmabufGlobal, buffer: Dmabuf, notifier: ImportNotifier, now: Instant) {
let Some(client) = notifier.client() else {
notifier.failed();
return;
};
if !self.globals.contains(&global) {
notifier.failed();
return;
}
if let Err(request) = self
.queue
.enqueue(global, client.id(), Request { buffer, notifier }, now)
{
request.notifier.failed();
}
}
pub(crate) fn process<R: ImportDma>(&mut self, global: DmabufGlobal, renderer: &mut R, node: Option<DrmNode>) {
self.process_with(global, node, |buffer| renderer.import_dmabuf(buffer, None).is_ok());
}
pub(crate) fn process_with(
&mut self,
global: DmabufGlobal,
node: Option<DrmNode>,
mut import: impl FnMut(&Dmabuf) -> bool,
) {
self.queue.process(&global, |request| {
if request.notifier.client().is_none() {
request.notifier.failed();
return;
}
if !import(&request.buffer) {
request.notifier.failed();
return;
}
if let Some(node) = node {
request.buffer.set_node(node);
}
if let Err(error) = request.notifier.successful::<Ferese>() {
tracing::debug!(?error, "dma-buf client disappeared before import completed");
}
});
}
pub(crate) fn expire(&mut self, now: Instant) {
self.queue.remove_if(
|entry| entry.deadline <= now || entry.value.notifier.client().is_none(),
|entry| entry.value.notifier.failed(),
);
}
}
impl Ferese {
pub(crate) fn process_pending_dmabuf_imports(&mut self) {
self.dmabuf_imports.expire(Instant::now());
if self.dmabuf_imports.queue.deadline().is_some() {
if let Some(backend) = self.direct_backend.as_mut() {
backend.process_dmabuf_imports(&mut self.dmabuf_imports);
} else if let Some(backend) = self.nested_backend.as_ref() {
let mut backend = backend.borrow_mut();
// Nested Ferese publishes exactly one DMA-BUF global.
if let Some(global) = self.dmabuf_imports.globals.iter().next().copied() {
self.dmabuf_imports.process(global, backend.renderer(), None);
}
}
}
if let Some(deadline) = self.dmabuf_imports.queue.deadline() {
if self.dmabuf_imports.timer.is_none() {
match self
.loop_handle
.insert_source(Timer::from_deadline(deadline), |_, _, state| {
state.dmabuf_imports.expire(Instant::now());
if let Some(deadline) = state.dmabuf_imports.queue.deadline() {
TimeoutAction::ToInstant(deadline)
} else {
state.dmabuf_imports.timer = None;
TimeoutAction::Drop
}
}) {
Ok(token) => self.dmabuf_imports.timer = Some(token),
Err(error) => {
tracing::warn!(%error, "could not schedule DMA-BUF import deadline");
self.dmabuf_imports
.queue
.remove_if(|_| true, |entry| entry.value.notifier.failed());
}
}
}
} else if let Some(token) = self.dmabuf_imports.timer.take() {
self.loop_handle.remove(token);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::fd::AsRawFd;
#[test]
fn renderer_order_cannot_consume_another_globals_requests() {
for order in [[1, 2], [2, 1]] {
let now = Instant::now();
let mut queue = Queue::default();
queue.enqueue(2, 1, "B-only", now).unwrap();
let mut results = Vec::new();
for owner in order {
queue.process(&owner, |buffer| {
assert_eq!(owner, 2);
assert_eq!(buffer, "B-only");
results.push(true);
});
}
assert_eq!(results, [true]);
assert!(queue.pending.is_empty());
}
}
#[test]
fn unavailable_imports_are_bounded_and_recover_before_the_deadline() {
let now = Instant::now();
let mut queue = Queue::default();
for index in 0..MAX_PENDING {
queue.enqueue(1, index / MAX_PER_CLIENT, index, now).unwrap();
}
assert_eq!(queue.enqueue(1, 99, 100, now), Err(100));
let mut completed = Vec::new();
queue.remove_if(
|entry| entry.deadline <= now + MAX_WAIT / 2,
|_| panic!("expired early"),
);
queue.process(&1, |value| completed.push(value));
assert_eq!(completed, (0..MAX_PENDING).collect::<Vec<_>>());
assert!(queue.deadline().is_none());
}
#[test]
fn per_client_admission_leaves_room_for_other_clients() {
let now = Instant::now();
let mut queue = Queue::default();
for index in 0..MAX_PER_CLIENT {
queue.enqueue(1, 1, index, now).unwrap();
}
assert_eq!(queue.enqueue(2, 1, 100, now), Err(100));
queue.enqueue(2, 2, 101, now).unwrap();
}
#[test]
fn expiry_and_device_removal_release_resources_once_without_touching_other_owners() {
if !crate::startup_tests::private_runtime(
"dmabuf_imports::tests::expiry_and_device_removal_release_resources_once_without_touching_other_owners",
) {
return;
}
let now = Instant::now();
let mut queue = Queue::default();
let removed = tempfile::tempfile().unwrap();
let expired = tempfile::tempfile().unwrap();
let fd_removed = removed.as_raw_fd();
let fd_expired = expired.as_raw_fd();
queue.enqueue(1, 1, removed, now).unwrap();
queue.enqueue(2, 1, expired, now + Duration::from_secs(1)).unwrap();
let mut failures = 0;
queue.process(&1, |_| failures += 1);
// SAFETY: F_GETFD observes descriptor validity without modifying it.
assert_eq!(unsafe { libc::fcntl(fd_removed, libc::F_GETFD) }, -1);
assert_ne!(unsafe { libc::fcntl(fd_expired, libc::F_GETFD) }, -1);
queue.remove_if(|entry| entry.deadline <= now + MAX_WAIT, |_| panic!("expired early"));
queue.remove_if(
|entry| entry.deadline <= now + MAX_WAIT + Duration::from_secs(1),
|_| failures += 1,
);
assert_eq!(unsafe { libc::fcntl(fd_expired, libc::F_GETFD) }, -1);
assert_eq!(failures, 2);
assert!(queue.deadline().is_none());
}
}
+3 -3
View File
@@ -112,9 +112,9 @@ impl DmabufHandler for Ferese {
&mut self.dmabuf_state
}
fn dmabuf_imported(&mut self, _global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) {
self.queue_dmabuf_import(dmabuf, notifier);
crate::backends::direct::render_all(self);
fn dmabuf_imported(&mut self, global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) {
self.dmabuf_imports
.enqueue(*global, dmabuf, notifier, std::time::Instant::now());
}
}
+10 -4
View File
@@ -46,11 +46,17 @@ impl SeatHandler for Ferese {
}
fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
if let CursorImageStatus::Named(icon) = &image {
self.named_cursors
.entry(*icon)
.or_insert_with(|| crate::cursor::load_named_cursor(&self.cursor_theme, *icon));
if let CursorImageStatus::Named(icon) = &image
&& !self.named_cursors.contains_key(icon)
{
self.cursor_loader.request(*icon, self.cursor_animation.buffer_scale);
}
self.cursor_animation.reset |= match (&self.cursor_status, &image) {
(CursorImageStatus::Named(previous), CursorImageStatus::Named(next)) => previous != next,
(CursorImageStatus::Hidden, CursorImageStatus::Hidden) => false,
(CursorImageStatus::Surface(previous), CursorImageStatus::Surface(next)) => previous != next,
_ => true,
};
self.cursor_status = image;
self.update_capture_cursor_privacy();
// Pointer focus transitions invoke this with the pointer lock held.
+3
View File
@@ -4,6 +4,7 @@ mod cursor;
mod daemon;
mod dimming;
mod display_presentation;
mod dmabuf_imports;
mod effects;
mod floating;
mod frame_scheduler;
@@ -161,6 +162,8 @@ fn after_dispatch(state: &mut Ferese) {
}
fn after_dispatch_with_redraw(state: &mut Ferese, mut redraw: impl FnMut(&mut Ferese, RedrawRequest<'_>)) {
state.process_pending_dmabuf_imports();
state.update_named_cursor(std::time::Instant::now());
state.poll_theme();
// The decoder wakes the loop after publishing its result, including
// when no output has a pending frame.
+4 -5
View File
@@ -487,15 +487,14 @@ pub(super) fn cursor_elements(
})
.collect()
}
CursorImageStatus::Named(icon) => {
let Some(cursor) = state.named_cursors.get(icon) else {
CursorImageStatus::Named(_) => {
let Some(cursor) = state.named_cursor_frame() else {
return Vec::new();
};
let hotspot = Point::<i32, Physical>::from((cursor.hotspot.x, cursor.hotspot.y));
let physical_location = pointer_location.to_physical_precise_round(scale) - hotspot;
let physical_location = cursor.physical_location(pointer_location, scale);
let Ok(element) = MemoryRenderBufferRenderElement::from_buffer(
renderer,
physical_location.to_f64(),
physical_location,
&cursor.buffer,
None,
None,
+1 -1
View File
@@ -27,7 +27,7 @@ pub(crate) fn private_runtime(test: &str) -> bool {
let log_path = directory.path().join("test.log");
let log = std::fs::File::create(&log_path).unwrap();
let mut child = Command::new(std::env::current_exe().unwrap())
.args(["--exact", test, "--nocapture"])
.args(["--exact", test, "--include-ignored", "--nocapture"])
.env(CHILD, test)
.env("XDG_RUNTIME_DIR", directory.path())
.env("XDG_CONFIG_HOME", directory.path())
+15 -37
View File
@@ -33,10 +33,8 @@ use ferese_layout::{
Axis, ColumnWidth, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, ViewportFocusStrategy, WindowId,
};
use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1;
use smithay::backend::allocator::dmabuf::Dmabuf;
use smithay::backend::drm::{DrmEventTime, DrmNode};
use smithay::backend::input::Keycode;
use smithay::backend::renderer::ImportDma;
use smithay::desktop::space::SpaceElement;
use smithay::desktop::utils::send_frames_surface_tree;
use smithay::desktop::{LayerSurface, PopupKind, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output};
@@ -55,7 +53,7 @@ use smithay::utils::{Logical, Point, Rectangle, Size};
use smithay::wayland::alpha_modifier::AlphaModifierState;
use smithay::wayland::compositor::{CompositorClientState, CompositorState, with_states};
use smithay::wayland::cursor_shape::CursorShapeManagerState;
use smithay::wayland::dmabuf::{DmabufState, ImportNotifier};
use smithay::wayland::dmabuf::DmabufState;
use smithay::wayland::fractional_scale::FractionalScaleManagerState;
use smithay::wayland::idle_inhibit::IdleInhibitManagerState;
use smithay::wayland::idle_notify::IdleNotifierState;
@@ -84,7 +82,7 @@ use window_registry::WindowRegistry;
use crate::backends::direct::DirectBackendState;
use crate::config::{Binding, BindingSet, DaemonConfig, InputSettings, OutputProfile, ThemeSettings};
use crate::cursor::{NamedCursor, cursor_theme, load_named_cursor};
use crate::cursor::NamedCursor;
use crate::dimming::DimAnimation;
use crate::gestures::{Swipe, SwipeDirection};
use crate::handlers::screencopy::PendingScreencopy;
@@ -468,7 +466,8 @@ pub struct Ferese {
pub(crate) output_redraw_pending: Vec<Output>,
pub(crate) locked_pointer_hint: Option<LockedPointerHint>,
pub(crate) last_pointer_time: u32,
pub(crate) cursor_theme: xcursor::CursorTheme,
pub(crate) cursor_loader: crate::cursor::Loader,
pub(crate) cursor_animation: crate::cursor::Animation,
pub(crate) named_cursors: HashMap<CursorIcon, NamedCursor>,
pub intercepted_keys: HashSet<Keycode>,
pub(crate) swipe: Swipe,
@@ -479,7 +478,7 @@ pub struct Ferese {
pub active_shortcuts_inhibitor: Option<KeyboardShortcutsInhibitor>,
pub direct_backend: Option<DirectBackendState>,
_ipc_socket: Option<IpcSocketGuard>,
pending_dmabuf_imports: Vec<(Dmabuf, ImportNotifier)>,
pub(crate) dmabuf_imports: crate::dmabuf_imports::ImportManager,
pub(crate) activation_inputs: crate::handlers::activation::InputHistory,
pub(crate) pending_screencopies: Vec<PendingScreencopy>,
// Screenshot requests outlive the readback that filled them: the
@@ -630,8 +629,8 @@ impl Ferese {
let socket_name = Self::init_wayland_listener(display, event_loop)?;
let start_time = Instant::now();
let cursor_theme = cursor_theme();
let default_cursor = load_named_cursor(&cursor_theme, CursorIcon::Default);
let cursor_loader = crate::cursor::Loader::new(event_loop.get_signal())?;
let default_cursor = crate::cursor::fallback_cursor();
let named_cursors = HashMap::from([(CursorIcon::Default, default_cursor)]);
let mut state = Self {
@@ -712,7 +711,8 @@ impl Ferese {
output_redraw_pending: Vec::new(),
locked_pointer_hint: None,
last_pointer_time: 0,
cursor_theme,
cursor_loader,
cursor_animation: crate::cursor::Animation::default(),
named_cursors,
intercepted_keys: HashSet::new(),
swipe: Swipe::default(),
@@ -723,7 +723,7 @@ impl Ferese {
active_shortcuts_inhibitor: None,
direct_backend: None,
_ipc_socket: None,
pending_dmabuf_imports: Vec::new(),
dmabuf_imports: crate::dmabuf_imports::ImportManager::default(),
activation_inputs: Default::default(),
pending_screencopies: Vec::new(),
screenshot: Coordinator::new(),
@@ -809,10 +809,6 @@ impl Ferese {
if input_changed && let Some(keyboard) = self.seat.get_keyboard() {
if keyboard_changed {
self.input_capture.close_all();
if self.input_capture.restore_focus {
self.restore_input_capture_focus();
}
let options = (!config.input_settings.xkb_options.is_empty())
.then(|| config.input_settings.xkb_options.join(","));
@@ -827,6 +823,11 @@ impl Ferese {
},
)
.map_err(|e| format!("keymap reload failed: {e}"))?;
self.input_capture.close_all();
if self.input_capture.restore_focus {
self.restore_input_capture_focus();
}
}
keyboard.change_repeat_info(config.input_settings.repeat_rate, config.input_settings.repeat_delay_ms);
}
@@ -931,29 +932,6 @@ impl Ferese {
Ok(())
}
pub(crate) fn queue_dmabuf_import(&mut self, dmabuf: Dmabuf, notifier: ImportNotifier) {
self.pending_dmabuf_imports.push((dmabuf, notifier));
}
pub(crate) fn process_dmabuf_imports<R>(&mut self, renderer: &mut R, node: Option<DrmNode>)
where
R: ImportDma,
{
for (dmabuf, notifier) in self.pending_dmabuf_imports.drain(..) {
if renderer.import_dmabuf(&dmabuf, None).is_ok() {
if let Some(node) = node {
dmabuf.set_node(node);
}
if let Err(error) = notifier.successful::<Self>() {
tracing::debug!(?error, "dma-buf client disappeared before import completed");
}
} else {
notifier.failed();
}
}
}
fn init_wayland_listener(
display: Display<Self>,
event_loop: &mut EventLoop<'static, Self>,
@@ -31,6 +31,190 @@ fn fixture() -> (smithay::reexports::calloop::EventLoop<'static, Ferese>, Ferese
(events, state, output)
}
#[test]
fn late_keymap_failure_preserves_capture_sessions_and_runtime_settings() {
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::late_keymap_failure_preserves_capture_sessions_and_runtime_settings",
) {
return;
}
let (mut events, mut state, _) = fixture();
let (window, _wire) = window(&mut state, &mut events, 0xff112233);
let focus = state.focused_window;
let session = state.input_capture.register(42, 1).unwrap();
state.input_capture.enable(session).unwrap();
let previous_layout = state.input_settings.xkb_layout.clone();
let previous_repeat = state.input_settings.repeat_rate;
let mut runtime = crate::config::Config::default().runtime_config().unwrap();
// Preparation succeeded; force the real late application failure.
runtime.input_settings.xkb_layout = "ferese-nonexistent-layout-for-regression".into();
runtime.input_settings.repeat_rate = previous_repeat + 1;
let result = state.apply_runtime_config(runtime, &serde_json::json!({"input": "changed"}));
assert!(result.unwrap_err().contains("keymap reload failed"));
assert!(state.input_capture.has_sessions());
state.input_capture.enable(session).unwrap();
assert!(!state.input_capture.restore_focus);
assert_eq!(state.focused_window, focus);
assert_eq!(state.input_settings.xkb_layout, previous_layout);
assert_eq!(state.input_settings.repeat_rate, previous_repeat);
assert!(state.windows.ids().contains_key(&window));
}
#[test]
fn dmabuf_validation_uses_the_originating_global_without_a_desktop_frame() {
use smithay::backend::allocator::{Buffer, Format, Fourcc, Modifier};
use std::io::Read;
use std::os::fd::AsRawFd;
use std::os::unix::net::UnixStream;
use std::sync::Arc;
if !crate::startup_tests::private_runtime(
"state::window_lifecycle_tests::dmabuf_validation_uses_the_originating_global_without_a_desktop_frame",
) {
return;
}
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
let formats = [Format {
code: Fourcc::Argb8888,
modifier: Modifier::Linear,
}];
let a = state
.dmabuf_state
.create_global::<Ferese>(&state.display_handle, formats);
state.dmabuf_imports.register(a);
let (server, mut wire) = UnixStream::pair().unwrap();
let client = state
.display_handle
.insert_client(server, Arc::new(ClientState::default()))
.unwrap();
request(&mut wire, 1, 1, &[2], None);
dispatch(&mut events, &mut state);
let read = |wire: &mut UnixStream| {
wire.set_nonblocking(true).unwrap();
let mut bytes = Vec::new();
let _ = wire.read_to_end(&mut bytes);
wire.set_nonblocking(false).unwrap();
bytes
};
let mut registry = read(&mut wire);
// Register B after the initial registry roundtrip so notification order
// identifies A and B without depending on backend map iteration order.
let b = state
.dmabuf_state
.create_global::<Ferese>(&state.display_handle, formats);
state.dmabuf_imports.register(b);
state.display_handle.flush_clients().unwrap();
registry.extend(read(&mut wire));
let mut offset = 0;
let mut names = Vec::new();
while offset < registry.len() {
let header = u32::from_ne_bytes(registry[offset + 4..offset + 8].try_into().unwrap());
let length = (header >> 16) as usize;
let message = &registry[offset..offset + length];
if message
.windows(b"zwp_linux_dmabuf_v1\0".len())
.any(|text| text == b"zwp_linux_dmabuf_v1\0")
{
names.push(u32::from_ne_bytes(message[8..12].try_into().unwrap()));
}
offset += length;
}
assert_eq!(names.len(), 2);
let interface = b"zwp_linux_dmabuf_v1\0";
for (name, id) in names.into_iter().zip([3, 4]) {
let mut args = vec![name, interface.len() as u32];
let mut padded = interface.to_vec();
padded.resize(padded.len().next_multiple_of(4), 0);
args.extend(
padded
.chunks_exact(4)
.map(|word| u32::from_ne_bytes(word.try_into().unwrap())),
);
args.extend([3, id]);
request(&mut wire, 2, 0, &args, None);
}
dispatch(&mut events, &mut state);
read(&mut wire);
let file = tempfile::tempfile().unwrap();
file.set_len(64).unwrap();
// Both asynchronous and immediate imports must survive the wrong GPU
// being visited first. The mock capability accepts only B's buffers.
for (params, buffer) in [(5, None), (6, Some(7))] {
request(&mut wire, 4, 1, &[params], None);
request(&mut wire, params, 1, &[0, 0, 16, 0, 0], Some(file.as_raw_fd()));
let mut args = vec![4, 4, Fourcc::Argb8888 as u32, 0];
if let Some(buffer) = buffer {
args.insert(0, buffer);
}
request(&mut wire, params, if buffer.is_some() { 3 } else { 2 }, &args, None);
dispatch(&mut events, &mut state);
assert!(state.space.outputs().next().is_none());
state.process_pending_dmabuf_imports();
assert!(
read(&mut wire).is_empty(),
"unavailable renderer must defer notification"
);
state
.dmabuf_imports
.process_with(a, None, |_| panic!("B's request reached A's renderer"));
let mut calls = 0;
state.dmabuf_imports.process_with(b, None, |dmabuf| {
calls += 1;
assert_eq!(dmabuf.size(), (4, 4).into());
true
});
assert_eq!(calls, 1);
state.display_handle.flush_clients().unwrap();
let response = read(&mut wire);
if buffer.is_none() {
assert_eq!(u32::from_ne_bytes(response[..4].try_into().unwrap()), params);
assert_eq!(u32::from_ne_bytes(response[4..8].try_into().unwrap()) & 0xffff, 0);
} else {
assert!(response.is_empty(), "create_immed success must not emit a failed event");
}
assert!(client.get_credentials(&state.display_handle).is_ok());
}
// Deferred requests receive one terminal failure on timeout or removal.
for params in [8, 9] {
request(&mut wire, 4, 1, &[params], None);
request(&mut wire, params, 1, &[0, 0, 16, 0, 0], Some(file.as_raw_fd()));
request(&mut wire, params, 2, &[4, 4, Fourcc::Argb8888 as u32, 0], None);
dispatch(&mut events, &mut state);
state.process_pending_dmabuf_imports();
if params == 8 {
state.dmabuf_imports.expire(Instant::now() + Duration::from_secs(5));
} else {
state.dmabuf_imports.remove(b);
}
state.display_handle.flush_clients().unwrap();
let response = read(&mut wire);
assert_eq!(response.len(), 8);
assert_eq!(u32::from_ne_bytes(response[..4].try_into().unwrap()), params);
assert_eq!(u32::from_ne_bytes(response[4..8].try_into().unwrap()) & 0xffff, 1);
state
.dmabuf_imports
.process_with(b, None, |_| panic!("failed request survived"));
assert!(client.get_credentials(&state.display_handle).is_ok());
}
state.dmabuf_imports.register(b);
request(&mut wire, 4, 1, &[10], None);
request(&mut wire, 10, 1, &[0, 0, 16, 0, 0], Some(file.as_raw_fd()));
request(&mut wire, 10, 2, &[4, 4, Fourcc::Argb8888 as u32, 0], None);
dispatch(&mut events, &mut state);
drop(wire);
dispatch(&mut events, &mut state);
state.process_pending_dmabuf_imports();
state
.dmabuf_imports
.process_with(b, None, |_| panic!("disconnected client's request survived"));
}
#[test]
fn hidden_null_commit_removes_membership_and_remaps_without_workspace_switch() {
if !crate::startup_tests::private_runtime(
+316 -58
View File
@@ -12,9 +12,7 @@ use smithay::desktop::layer_map_for_output;
use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
use smithay::reexports::calloop::EventLoop;
use smithay::reexports::calloop::timer::{TimeoutAction, Timer};
use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind;
use smithay::utils::{Clock, Monotonic, Physical, Rectangle, Transform};
use smithay::wayland::presentation::Refresh;
use smithay::utils::{Physical, Rectangle, Size, Transform};
use crate::Ferese;
use crate::metrics::{FrameEffectMetrics, RenderMetrics};
@@ -32,14 +30,18 @@ type DamageRenderResult = Result<
pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<(), Box<dyn Error>> {
let (mut backend, event_source) = winit::init::<GlesRenderer>()?;
configure_nested_protocols(state);
let initial_size = backend.window_size();
let drawable = Rc::new(Cell::new(usable_size(initial_size)));
let dmabuf_formats = backend.renderer().dmabuf_formats();
let display_handle = state.display_handle.clone();
state
let dmabuf_global = state
.dmabuf_state
.create_global::<Ferese>(&display_handle, dmabuf_formats);
state.dmabuf_imports.register(dmabuf_global);
let initial_scale = normalized_scale(backend.scale_factor());
let mode = Mode {
size: backend.window_size(),
size: if drawable.get() { initial_size } else { (1, 1).into() },
refresh: backend
.window()
.current_monitor()
@@ -67,8 +69,6 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
state.register_output(&output, "nested-primary".to_owned());
let mut damage_tracker = OutputDamageTracker::from_output(&output);
let clock = Clock::<Monotonic>::new();
let mut sequence = 0_u64;
let mut missed_deadlines = 0_u64;
let mut render_metrics = RenderMetrics::from_environment(output.name());
@@ -82,11 +82,16 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
state.nested_backend = Some(backend.clone());
let redraw_backend = backend.clone();
let redraw_refresh = refresh.clone();
let redraw_drawable = drawable.clone();
event_loop
.handle()
.insert_source(Timer::from_duration(refresh.get()), move |_, _, _| {
redraw_backend.borrow().window().request_redraw();
TimeoutAction::ToDuration(redraw_refresh.get())
if redraw_drawable.get() {
redraw_backend.borrow().window().request_redraw();
TimeoutAction::ToDuration(redraw_refresh.get())
} else {
TimeoutAction::ToDuration(Duration::from_millis(250))
}
})?;
let mut capture_texture = None;
@@ -97,21 +102,16 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
match event {
WinitEvent::Resized { size, scale_factor } => {
tracing::debug!(target: "ferese::nested_input", ?size, scale_factor, "host resized nested output");
let scale = normalized_scale(scale_factor);
let rate = backend
.window()
.current_monitor()
.and_then(|monitor| monitor.refresh_rate_millihertz())
.unwrap_or(60_000) as i32;
refresh.set(Duration::from_nanos(1_000_000_000_000 / rate.max(1) as u64));
if !resize_nested_output(state, &output, &drawable, size, scale_factor, rate) {
return;
}
let mut runtime = state.current_desktop_outputs();
let resized = runtime.iter_mut().find(|entry| entry.output == output).unwrap();
resized.mode = Mode { size, refresh: rate };
resized.scale = Scale::Fractional(scale);
state.begin_desktop_transition();
let changes = state.publish_desktop(runtime).expect("valid nested output geometry");
state.finish_desktop_transition(changes);
refresh.set(Duration::from_nanos(1_000_000_000_000 / rate.max(1) as u64));
if let Err(error) = state.display_handle.flush_clients() {
tracing::debug!(%error, "failed to flush output-resize configure");
}
@@ -125,13 +125,17 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
"host input in nested output");
}
WinitEvent::Redraw => {
if !drawable.get() || !usable_size(backend.window_size()) {
drawable.set(false);
return;
}
state.advance_animations(Instant::now());
let age = backend.buffer_age().unwrap_or(0);
let render_started = Instant::now();
let rendered = (|| -> DamageRenderResult {
{
let (renderer, mut framebuffer) = backend.bind()?;
state.process_dmabuf_imports(renderer, None);
let elements = animated_window_elements(state, renderer, &output);
let effects = frame_effect_metrics(&elements, output.current_scale().fractional_scale());
let result = damage_tracker.render_output(
@@ -143,31 +147,41 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
)?;
// Keep the existing readback-only path when the
// displayed scene has no privacy exclusions.
let mut capture_changed_binding = !state.has_capture_exclusions()
&& state.process_screencopies(renderer, &framebuffer, &output, true);
if state.has_pending_screencopy(&output, false)
|| state.has_pending_screencopy(&output, true)
{
let scene = state.sample_frame(&output, Duration::ZERO);
crate::backends::direct::capture::capture_output(
state,
renderer,
&mut capture_texture,
&output,
&scene,
);
capture_changed_binding = true;
}
if capture_changed_binding {
// Restore the display framebuffer binding after offscreen readback.
let _ = renderer
.render(
&mut framebuffer,
output.current_mode().expect("output has a mode").size,
output.current_transform(),
)?
.finish()?;
}
capture_with_display_restore(
renderer,
&mut framebuffer,
|renderer, framebuffer| {
let mut changed = !state.has_capture_exclusions()
&& state.process_screencopies(renderer, framebuffer, &output, true);
if state.has_pending_screencopy(&output, false)
|| state.has_pending_screencopy(&output, true)
{
let scene = state.sample_frame(&output, Duration::ZERO);
crate::backends::direct::capture::capture_output(
state,
renderer,
&mut capture_texture,
&output,
&scene,
);
changed = true;
}
changed
},
|renderer, framebuffer| {
// Capture failures are handled locally. Failure to restore
// the display target means the renderer itself is unusable.
let _ = renderer
.render(
framebuffer,
output.current_mode().unwrap().size,
output.current_transform(),
)?
.finish()?;
Ok::<_, smithay::backend::renderer::gles::GlesError>(())
},
)?;
Ok((result.damage.cloned(), effects, result.states))
}
@@ -212,19 +226,6 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
}
render_metrics.record_frame(elapsed, &damage, missed_deadlines, effects);
let mut presentation = crate::presentation::take_output_feedback(
state,
&output,
&rendered_states,
PresentationKind::Vsync,
);
sequence = sequence.wrapping_add(1);
presentation.presented(
clock.now(),
Refresh::fixed(refresh.get()),
sequence,
PresentationKind::Vsync,
);
send_nested_frame_callbacks(state, &output);
state.space.refresh();
state.popups.cleanup();
@@ -240,6 +241,74 @@ pub fn init(event_loop: &mut EventLoop<Ferese>, state: &mut Ferese) -> Result<()
Ok(())
}
fn configure_nested_protocols(state: &mut Ferese) {
// Winit's swap has no host presentation timestamp or retrace counter.
// Do not advertise timing we cannot report accurately.
state
.display_handle
.disable_global::<Ferese>(state.presentation_state.global());
}
fn usable_size(size: Size<i32, Physical>) -> bool {
size.w > 0 && size.h > 0
}
fn resize_nested_output(
state: &mut Ferese,
output: &Output,
drawable: &Cell<bool>,
size: Size<i32, Physical>,
scale_factor: f64,
rate: i32,
) -> bool {
if !usable_size(size) {
drawable.set(false);
return false;
}
let mut runtime = state.current_desktop_outputs();
let Some(resized) = runtime.iter_mut().find(|entry| entry.output == *output) else {
drawable.set(false);
return false;
};
resized.mode = Mode {
size,
refresh: rate.max(1),
};
resized.scale = Scale::Fractional(normalized_scale(scale_factor));
// Paused host time must not advance springs when publication relayouts.
if !drawable.get() {
state.reset_animation_clock();
}
state.begin_desktop_transition();
let changes = match state.publish_desktop(runtime) {
Ok(changes) => changes,
Err(error) => {
state.desktop_transition = None;
drawable.set(false);
tracing::warn!(%error, "ignoring invalid nested output geometry");
return false;
}
};
state.finish_desktop_transition(changes);
drawable.set(true);
true
}
fn capture_with_display_restore<R, F, E>(
renderer: &mut R,
framebuffer: &mut F,
capture: impl FnOnce(&mut R, &mut F) -> bool,
restore: impl FnOnce(&mut R, &mut F) -> Result<(), E>,
) -> Result<(), E> {
if capture(renderer, framebuffer) {
restore(renderer, framebuffer)?;
}
Ok(())
}
fn send_nested_frame_callbacks(state: &mut Ferese, output: &Output) {
let eligible = state.callback_outputs();
if state.session_lock.active() {
@@ -268,7 +337,196 @@ fn normalized_scale(scale: f64) -> f64 {
#[cfg(test)]
mod tests {
use super::normalized_scale;
use super::*;
#[test]
fn zero_host_resize_preserves_geometry_and_recovers() {
if !crate::startup_tests::private_runtime("winit::tests::zero_host_resize_preserves_geometry_and_recovers") {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
let output = Output::new(
"nested".into(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
output.change_current_state(
Some(Mode {
size: (800, 600).into(),
refresh: 60_000,
}),
None,
None,
None,
);
state.space.map_output(&output, (0, 0));
state.register_output(&output, "nested-primary".into());
let drawable = Cell::new(true);
assert!(resize_nested_output(
&mut state,
&output,
&drawable,
(900, 600).into(),
1.25,
60_000
));
let before = output.current_mode();
for size in [(0, 600), (900, 0), (0, 0), (-1, 600)] {
assert!(!resize_nested_output(
&mut state,
&output,
&drawable,
size.into(),
2.0,
144_000
));
assert_eq!(output.current_mode(), before);
assert_eq!(output.current_scale().fractional_scale(), 1.25);
assert!(!drawable.get());
assert!(state.desktop_transition.is_none());
}
assert!(resize_nested_output(
&mut state,
&output,
&drawable,
(1200, 800).into(),
1.5,
120_000
));
assert_eq!(output.current_mode().unwrap().size, (1200, 800).into());
assert!(drawable.get());
assert!(state.desktop_transition.is_none());
}
#[test]
fn nested_registry_does_not_advertise_unobserved_presentation_timing() {
use std::io::{Read, Write};
use std::os::unix::net::UnixStream;
use std::sync::Arc;
if !crate::startup_tests::private_runtime(
"winit::tests::nested_registry_does_not_advertise_unobserved_presentation_timing",
) {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
configure_nested_protocols(&mut state);
let (server, mut wire) = UnixStream::pair().unwrap();
let _client = state
.display_handle
.insert_client(server, Arc::new(crate::state::ClientState::default()))
.unwrap();
wire.write_all(
&[1u32, 12 << 16 | 1, 2]
.into_iter()
.flat_map(u32::to_ne_bytes)
.collect::<Vec<_>>(),
)
.unwrap();
events.dispatch(Duration::from_millis(1), &mut state).unwrap();
state.display_handle.flush_clients().unwrap();
wire.set_nonblocking(true).unwrap();
let mut globals = Vec::new();
let _ = wire.read_to_end(&mut globals);
assert!(
globals
.windows(b"wl_compositor\0".len())
.any(|bytes| bytes == b"wl_compositor\0")
);
assert!(
!globals
.windows(b"wp_presentation\0".len())
.any(|bytes| bytes == b"wp_presentation\0")
);
}
#[test]
#[ignore = "requires an EGL device and private Wayland sockets"]
fn capture_failure_restores_display_target_and_subsequent_frames_render() {
use smithay::backend::allocator::Fourcc;
use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay};
use smithay::backend::renderer::gles::{GlesError, GlesTexture};
use smithay::backend::renderer::{Bind, Color32F, ExportMem, Offscreen};
use smithay::reexports::calloop::channel;
use smithay::utils::Buffer;
if !crate::startup_tests::private_runtime(
"winit::tests::capture_failure_restores_display_target_and_subsequent_frames_render",
) {
return;
}
let mut events = EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
let display = unsafe { EGLDisplay::new(EGLDevice::enumerate().unwrap().last().unwrap()).unwrap() };
let context = EGLContext::new(&display).unwrap();
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
let output = Output::new(
"nested".into(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
let size: Size<i32, Physical> = (32, 24).into();
let mut displayed: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (32, 24).into()).unwrap();
let mut offscreen: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (32, 24).into()).unwrap();
let damage = [Rectangle::from_size(size)];
let mut framebuffer = renderer.bind(&mut displayed).unwrap();
for changed_binding in [false, true] {
let (sender, receiver) = channel::channel();
state
.pending_screencopies
.push(crate::handlers::screencopy::PendingScreencopy::owned(
1,
0,
sender,
output.clone(),
Rectangle::<i32, Buffer>::from_size((32, 24).into()),
));
capture_with_display_restore(
&mut renderer,
&mut framebuffer,
|renderer, _| {
if changed_binding {
let _target = renderer.bind(&mut offscreen).unwrap();
}
assert!(
crate::backends::direct::capture::capture_request(&mut state, &output, false, || {
Err::<(), _>("injected offscreen allocation/render failure")
})
.is_none()
);
true
},
|renderer, framebuffer| {
let _ = renderer.render(framebuffer, size, Transform::Normal)?.finish()?;
Ok::<_, GlesError>(())
},
)
.unwrap();
assert!(receiver.try_recv().unwrap().result.is_err());
assert!(receiver.try_recv().is_err());
assert!(state.pending_screencopies.is_empty());
let mut frame = renderer.render(&mut framebuffer, size, Transform::Normal).unwrap();
frame.clear(Color32F::new(0.0, 0.0, 1.0, 1.0), &damage).unwrap();
frame.finish().unwrap().wait().unwrap();
let pixels = renderer
.copy_framebuffer(&framebuffer, Rectangle::from_size((32, 24).into()), Fourcc::Abgr8888)
.unwrap();
assert_eq!(&renderer.map_texture(&pixels).unwrap()[..4], &[0, 0, 255, 255]);
}
}
#[test]
fn accepts_positive_finite_scale() {
assert_eq!(normalized_scale(1.25), 1.25);