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:
1 parent
405c74af9f
commit
73f21e20ce
23 files changed
+2287
-457
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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"]
|
||||
);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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 = ®istry[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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user