diff --git a/TODO.md b/TODO.md index b7dc92fa2..5f9136a69 100644 --- a/TODO.md +++ b/TODO.md @@ -6,8 +6,8 @@ - Work on `ConnectorSavedState` - Keyboard focus in Idea Xwayland is weird when creating a new Java file -Snowcap -- Add `send_message` for layers +- Snowcap crashes when a window opens and immediately closes because the foreign toplevel handle is no longer valid +- Cursor position when scaled is wrong Testing - Test layout mode changing and how it interacts with client fullscreen/maximized requests diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 01fcd26c6..fc6e5e40c 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -63,7 +63,7 @@ use smithay::{ protocol::{wl_shm, wl_surface::WlSurface}, }, }, - utils::{DeviceFd, Rectangle, Transform}, + utils::{DeviceFd, Point, Rectangle, Transform}, wayland::{ dmabuf::{self, DmabufFeedback, DmabufFeedbackBuilder, DmabufGlobal}, presentation::Refresh, @@ -121,7 +121,7 @@ pub struct Udev { pub session: LibSeatSession, udev_dispatcher: Dispatcher<'static, UdevBackend, State>, display_handle: DisplayHandle, - pub(super) primary_gpu: DrmNode, + pub primary_gpu: DrmNode, pub(super) gpu_manager: GpuManager>, devices: HashMap, /// The global corresponding to the primary gpu @@ -1384,6 +1384,11 @@ impl Udev { return; } + let Some(output_geo) = pinnacle.space.output_geometry(output) else { + make_idle(&mut surface.render_state, &pinnacle.loop_handle); + return; + }; + assert_matches!( surface.render_state, RenderState::Scheduled | RenderState::WaitingForEstimatedVblankAndScheduled(_) @@ -1419,12 +1424,19 @@ impl Udev { || (pinnacle.lock_state.is_locked() && output.with_state(|state| state.lock_surface.is_none())); + let scale = output.current_scale().fractional_scale(); + + let cursor_hotspot = pinnacle + .cursor_state + .cursor_hotspot(pinnacle.clock.now(), scale) + .unwrap_or_default(); + let (pointer_render_elements, cursor_ids) = pointer_render_elements( - output, + (pointer_location - output_geo.loc.to_f64()).to_physical_precise_round(scale) + - Point::new(cursor_hotspot.x, cursor_hotspot.y), + scale, &mut renderer, &mut pinnacle.cursor_state, - &pinnacle.space, - pointer_location, pinnacle.dnd_icon.as_ref(), &pinnacle.clock, ); diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 9d069871f..a5655ff3f 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -28,7 +28,7 @@ use smithay::{ window::{Icon, WindowAttributes}, }, }, - utils::{Rectangle, Transform}, + utils::{Point, Rectangle, Transform}, wayland::{dmabuf, presentation::Refresh}, }; use tracing::{debug, error, info, trace, warn}; @@ -243,12 +243,20 @@ impl Winit { .map(|ptr| ptr.current_location()) .unwrap_or((0.0, 0.0).into()); + let output_loc = pinnacle.space.output_geometry(&self.output).unwrap().loc; + let scale = self.output.current_scale().fractional_scale(); + + let cursor_hotspot = pinnacle + .cursor_state + .cursor_hotspot(pinnacle.clock.now(), scale) + .unwrap_or_default(); + let (pointer_render_elements, _cursor_ids) = pointer_render_elements( - &self.output, + (pointer_location - output_loc.to_f64()).to_physical_precise_round(scale) + - Point::new(cursor_hotspot.x, cursor_hotspot.y), + scale, self.backend.renderer(), &mut pinnacle.cursor_state, - &pinnacle.space, - pointer_location, pinnacle.dnd_icon.as_ref(), &pinnacle.clock, ); diff --git a/src/cursor.rs b/src/cursor.rs index 6523e4665..62296d360 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -5,7 +5,10 @@ use std::{collections::HashMap, rc::Rc}; use anyhow::Context; use smithay::backend::allocator::Fourcc; -use smithay::utils::IsAlive; +use smithay::desktop::utils::bbox_from_surface_tree; +use smithay::input::pointer::CursorImageSurfaceData; +use smithay::utils::{Buffer, IsAlive, Monotonic, Point, Rectangle, Time}; +use smithay::wayland::compositor; use smithay::{ backend::renderer::element::memory::MemoryRenderBuffer, input::pointer::{CursorIcon, CursorImageStatus}, @@ -143,6 +146,62 @@ impl CursorState { } } + pub fn cursor_geometry( + &mut self, + time: Time, + scale: f64, + ) -> Option> { + match self.pointer_element() { + PointerElement::Hidden => None, + PointerElement::Named { cursor, size } => { + let image = cursor.image(time.into(), size * scale.ceil() as u32); + let geo = Rectangle::from_size((image.width as i32, image.height as i32).into()); + Some(geo) + } + PointerElement::Surface { surface } => { + let geo = bbox_from_surface_tree(&surface, (0, 0)); + let buffer_geo = Rectangle::new( + (geo.loc.x, geo.loc.y).into(), + geo.size + .to_f64() + .to_buffer(scale, Transform::Normal) + .to_i32_round(), + ); + Some(buffer_geo) + } + } + } + + pub fn cursor_hotspot( + &mut self, + time: Time, + scale: f64, + ) -> Option> { + match self.pointer_element() { + PointerElement::Hidden => None, + PointerElement::Named { cursor, size } => { + let image = cursor.image(time.into(), size * scale.ceil() as u32); + let hotspot = (image.xhot as i32, image.yhot as i32); + Some(Point::from(hotspot).downscale(scale.ceil() as i32)) + } + PointerElement::Surface { surface } => { + let hotspot: Point = compositor::with_states(&surface, |states| { + states + .data_map + .get::() + .unwrap() + .lock() + .unwrap() + .hotspot + }) + .to_f64() + .upscale(scale) + .to_i32_round(); + Some((hotspot.x, hotspot.y).into()) + } + } + } + pub fn is_current_cursor_animated(&mut self) -> bool { let _span = tracy_client::span!("CursorState::is_current_cursor_animated"); diff --git a/src/decoration.rs b/src/decoration.rs index 32538572e..6dfdab2fe 100644 --- a/src/decoration.rs +++ b/src/decoration.rs @@ -130,6 +130,11 @@ impl DecorationSurface { self.cached_state().z_index } + pub fn offset(&self) -> Point { + let bounds = self.bounds(); + Point::new(bounds.left as i32, bounds.top as i32) + } + pub fn bbox(&self) -> Rectangle { bbox_from_surface_tree(self.0.surface.wl_surface(), (0, 0)) } diff --git a/src/focus/pointer.rs b/src/focus/pointer.rs index 6a3ef52fa..79ca3eb81 100644 --- a/src/focus/pointer.rs +++ b/src/focus/pointer.rs @@ -49,7 +49,6 @@ impl PointerFocusTarget { }); } - #[cfg(feature = "snowcap")] if !found { win.with_state(|state| { for deco in state.decoration_surfaces.iter() { diff --git a/src/handlers.rs b/src/handlers.rs index f779a9f34..2ba1b0a9e 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -6,14 +6,19 @@ pub mod ext_workspace; mod foreign_toplevel; pub mod foreign_toplevel_list; pub mod idle; +mod image_capture_source; +pub mod image_copy_capture; pub mod session_lock; -#[cfg(feature = "snowcap")] pub mod snowcap_decoration; pub mod xdg_activation; mod xdg_shell; pub mod xwayland; -use std::{collections::HashMap, os::fd::OwnedFd, sync::Arc}; +use std::{ + collections::HashMap, + os::fd::OwnedFd, + sync::{Arc, atomic::Ordering}, +}; use anyhow::Context; use smithay::{ @@ -215,9 +220,7 @@ impl CompositorHandler for State { } // Window surface commit - if let Some(window) = self.pinnacle.window_for_surface(surface).cloned() - && window.is_wayland() - { + if let Some(window) = self.pinnacle.window_for_surface(surface).cloned() { let Some(is_mapped) = with_renderer_surface_state(surface, |state| state.buffer().is_some()) else { @@ -226,28 +229,30 @@ impl CompositorHandler for State { window.on_commit(); - // Toplevel has become unmapped, - // see https://wayland.app/protocols/xdg-shell#xdg_toplevel - if !is_mapped { - self.pinnacle.remove_window(&window, true); + if window.is_wayland() { + // Toplevel has become unmapped, + // see https://wayland.app/protocols/xdg-shell#xdg_toplevel + if !is_mapped { + self.pinnacle.remove_window(&window, true); - let output = window.output(&self.pinnacle); + let output = window.output(&self.pinnacle); - if let Some(output) = output { - self.pinnacle.request_layout(&output); + if let Some(output) = output { + self.pinnacle.request_layout(&output); + } } - } - // Update reactive popups - for (popup, _) in PopupManager::popups_for_surface(surface) { - if let PopupKind::Xdg(popup) = popup - && popup.with_pending_state(|state| state.positioner.reactive) - { - if let Err(err) = self.pinnacle.position_popup(&popup) { - debug!("Failed to position reactive popup: {err}"); - } - if let Err(err) = popup.send_configure() { - warn!("Failed to configure reactive popup: {err}"); + // Update reactive popups + for (popup, _) in PopupManager::popups_for_surface(surface) { + if let PopupKind::Xdg(popup) = popup + && popup.with_pending_state(|state| state.positioner.reactive) + { + if let Err(err) = self.pinnacle.position_popup(&popup) { + debug!("Failed to position reactive popup: {err}"); + } + if let Err(err) = popup.send_configure() { + warn!("Failed to configure reactive popup: {err}"); + } } } } @@ -366,40 +371,31 @@ impl CompositorHandler for State { .cloned() { vec![output] // surface is a lock surface - } else { - #[cfg(feature = "snowcap")] - if let Some((win, deco)) = self.pinnacle.windows.iter().find_map(|win| { - let deco = win - .with_state(|state| { - state - .decoration_surfaces - .iter() - .find(|deco| deco.wl_surface() == surface || deco.wl_surface() == &root) - .cloned() - }) - .map(|deco| (win.clone(), deco)); - deco - }) { - use std::sync::atomic::Ordering; - - if deco.with_state(|state| state.bounds_changed.fetch_and(false, Ordering::Relaxed)) + } else if let Some((win, deco)) = self.pinnacle.windows.iter().find_map(|win| { + let deco = win + .with_state(|state| { + state + .decoration_surfaces + .iter() + .find(|deco| deco.wl_surface() == surface || deco.wl_surface() == &root) + .cloned() + }) + .map(|deco| (win.clone(), deco)); + deco + }) { + if deco.with_state(|state| state.bounds_changed.fetch_and(false, Ordering::Relaxed)) { + if win.with_state(|state| state.layout_mode.is_tiled()) + && let Some(output) = win.output(&self.pinnacle) { - if win.with_state(|state| state.layout_mode.is_tiled()) - && let Some(output) = win.output(&self.pinnacle) - { - self.pinnacle.request_layout(&output); - } else { - self.pinnacle.update_window_geometry(&win, false); - } + self.pinnacle.request_layout(&output); + } else { + self.pinnacle.update_window_geometry(&win, false); } - - // FIXME: granular - self.pinnacle.space.outputs().cloned().collect() - } else { - return; } - #[cfg(not(feature = "snowcap"))] + // FIXME: granular + self.pinnacle.space.outputs().cloned().collect() + } else { return; }; @@ -1032,90 +1028,67 @@ impl Pinnacle { unreachable!("popup has a root surface and therefore a parent"); }; - let popup_geo = { - if parent == root { - // Slide toplevel popup x's instead of flipping; this mimics Awesome - positioner - .constraint_adjustment - .remove(ConstraintAdjustment::FlipX); - } - - #[cfg(feature = "snowcap")] - { - // The anchor rectangle is relative to the window's wl surface. - // If there is a decoration, we need to offset the anchor rect - // by the decoration offset. - - let offset = if let Some(win) = self.window_for_surface(&root) { - win.with_state(|state| { - let bounds = state.max_decoration_bounds(); - Point::new(bounds.left as i32, bounds.top as i32) - }) - } else { - Default::default() - }; + let deco_offset = self + .window_for_surface(&root) + .map(|win| win.total_decoration_offset()) + .unwrap_or_default(); - positioner.anchor_rect.loc += offset; - } - - let (root_global_loc, output) = if let Some(win) = self.window_for_surface(&root) { - let win_geo = self - .space - .element_geometry(win) - .context("window was not mapped")?; - - ( - win_geo.loc, - self.focused_output().context("no focused output")?.clone(), - ) - } else { - self.space - .outputs() - .find_map(|op| { - let layer_map = layer_map_for_output(op); - let layer = - layer_map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?; - let output_loc = self.space.output_geometry(op)?.loc; - Some(( - layer_map.layer_geometry(layer)?.loc + output_loc, - op.clone(), - )) - }) - .context("no valid parent")? - }; + if parent == root { + // Slide toplevel popup x's instead of flipping; this mimics Awesome + positioner + .constraint_adjustment + .remove(ConstraintAdjustment::FlipX); + } - let parent_global_loc = if root == parent { - root_global_loc - } else { - root_global_loc + get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone())) - }; + // The anchor rectangle is relative to the window's wl surface. + // If there is a decoration, we need to offset the anchor rect + // by the decoration offset. + positioner.anchor_rect.loc += deco_offset; - let mut output_geo = self + let (root_global_loc, output) = if let Some(win) = self.window_for_surface(&root) { + let win_geo = self .space - .output_geometry(&output) - .context("output was not mapped")?; + .element_geometry(win) + .context("window was not mapped")?; - // Make local to parent - output_geo.loc -= parent_global_loc; - positioner.get_unconstrained_geometry(output_geo) + ( + win_geo.loc, + self.focused_output().context("no focused output")?.clone(), + ) + } else { + self.space + .outputs() + .find_map(|op| { + let layer_map = layer_map_for_output(op); + let layer = layer_map.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?; + let output_loc = self.space.output_geometry(op)?.loc; + Some(( + layer_map.layer_geometry(layer)?.loc + output_loc, + op.clone(), + )) + }) + .context("no valid parent")? }; - #[cfg(feature = "snowcap")] - let popup_geo = { - // Make the popup location relative to the wl surface - // by "undoing" the offset above. + let parent_global_loc = if root == parent { + root_global_loc + } else { + root_global_loc + get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone())) + }; - let offset = if let Some(win) = self.window_for_surface(&root) { - win.with_state(|state| { - let bounds = state.max_decoration_bounds(); - Point::new(bounds.left as i32, bounds.top as i32) - }) - } else { - Default::default() - }; + let mut output_geo = self + .space + .output_geometry(&output) + .context("output was not mapped")?; - Rectangle::new(popup_geo.loc - offset, popup_geo.size) - }; + // Make local to parent + output_geo.loc -= parent_global_loc; + + let mut popup_geo = positioner.get_unconstrained_geometry(output_geo); + + // Make the popup location relative to the wl surface + // by "undoing" the offset above. + popup_geo.loc -= deco_offset; popup.with_pending_state(|state| { state.geometry = popup_geo; diff --git a/src/handlers/image_capture_source.rs b/src/handlers/image_capture_source.rs new file mode 100644 index 000000000..6b95ce449 --- /dev/null +++ b/src/handlers/image_capture_source.rs @@ -0,0 +1,3 @@ +use crate::{protocol::image_capture_source::delegate_image_capture_source, state::State}; + +delegate_image_capture_source!(State); diff --git a/src/handlers/image_copy_capture.rs b/src/handlers/image_copy_capture.rs new file mode 100644 index 000000000..cbd857ff3 --- /dev/null +++ b/src/handlers/image_copy_capture.rs @@ -0,0 +1,688 @@ +use std::mem; + +use smithay::{ + backend::{ + egl::EGLDevice, + renderer::{ + Bind, Color32F, ExportMem, Offscreen, buffer_dimensions, + damage::OutputDamageTracker, + element::RenderElement, + gles::{GlesRenderbuffer, GlesRenderer}, + }, + }, + output::Output, + reexports::wayland_server::protocol::wl_shm, + utils::{Buffer, Physical, Point, Rectangle, Size, Transform}, + wayland::{ + compositor, + dmabuf::get_dmabuf, + fractional_scale::with_fractional_scale, + seat::WaylandFocus, + shm::{shm_format_to_fourcc, with_buffer_contents}, + }, +}; +use tracing::error; + +use crate::{ + protocol::{ + image_capture_source::Source, + image_copy_capture::{ + ImageCopyCaptureHandler, ImageCopyCaptureState, delegate_image_copy_capture, + frame::Frame, + session::{Cursor, CursorSession, Session}, + }, + }, + render::{ + OutputRenderElement, output_render_elements, + pointer::pointer_render_elements, + util::{DynElement, damage::BufferDamageElement}, + }, + state::{Pinnacle, State, WithState}, +}; + +impl ImageCopyCaptureHandler for State { + fn image_copy_capture_state(&mut self) -> &mut ImageCopyCaptureState { + &mut self.pinnacle.image_copy_capture_state + } + + fn new_session(&mut self, session: Session) { + let Some((buffer_size, scale)) = self.pinnacle.buffer_size_and_scale_for_session(&session) + else { + session.stopped(); + return; + }; + + session.resized(buffer_size); + let trackers = SessionDamageTrackers::new(buffer_size, scale); + + match session.source() { + Source::Output(wl_output) => { + let Some(output) = Output::from_resource(&wl_output) else { + session.stopped(); + return; + }; + + output.with_state_mut(|state| state.capture_sessions.insert(session, trackers)); + } + Source::ForeignToplevel(ext_foreign_toplevel_handle_v1) => { + let Some(window) = self + .pinnacle + .window_for_foreign_toplevel_handle(&ext_foreign_toplevel_handle_v1) + else { + session.stopped(); + return; + }; + + window.with_state_mut(|state| state.capture_sessions.insert(session, trackers)); + } + } + } + + fn new_cursor_session(&mut self, cursor_session: CursorSession) { + match cursor_session.source() { + Source::Output(wl_output) => { + let Some(output) = Output::from_resource(wl_output) else { + return; + }; + + output.with_state_mut(|state| state.cursor_sessions.push(cursor_session)); + } + Source::ForeignToplevel(ext_foreign_toplevel_handle_v1) => { + let Some(window) = self + .pinnacle + .window_for_foreign_toplevel_handle(ext_foreign_toplevel_handle_v1) + else { + return; + }; + + window.with_state_mut(|state| state.cursor_sessions.push(cursor_session)); + } + } + } + + fn session_destroyed(&mut self, session: Session) { + for output in self.pinnacle.outputs.iter() { + output.with_state_mut(|state| state.capture_sessions.remove(&session)); + } + + for win in self.pinnacle.windows.iter() { + win.with_state_mut(|state| state.capture_sessions.remove(&session)); + } + } + + fn cursor_session_destroyed(&mut self, cursor_session: CursorSession) { + for output in self.pinnacle.outputs.iter() { + output.with_state_mut(|state| { + state + .cursor_sessions + .retain(|session| *session != cursor_session) + }); + } + + for win in self.pinnacle.windows.iter() { + win.with_state_mut(|state| { + state + .cursor_sessions + .retain(|session| *session != cursor_session) + }); + } + } +} +delegate_image_copy_capture!(State); + +impl State { + /// Sets buffer constraints for all [`Session`]s globally. + pub fn set_copy_capture_buffer_constraints(&mut self) { + let shm_formats = [wl_shm::Format::Argb8888]; + + let dmabuf_device = self + .backend + .with_renderer(|renderer| { + EGLDevice::device_for_display(renderer.egl_context().display()) + .ok() + .and_then(|device| device.try_get_render_node().ok().flatten()) + }) + .flatten(); + + let dmabuf_formats = self + .backend + .with_renderer(|renderer| renderer.egl_context().dmabuf_render_formats().clone()) + .unwrap_or_default(); + + self.pinnacle + .image_copy_capture_state + .set_buffer_constraints(shm_formats, dmabuf_device, dmabuf_formats); + } + + /// Processes all active [`Session`]s by updating cursor positions and + /// rendering pending frames. + pub fn process_capture_sessions(&mut self) { + let _span = tracy_client::span!(); + + self.update_cursor_capture_positions(); + + for win in self.pinnacle.windows.clone() { + let mut sessions = win.with_state_mut(|state| mem::take(&mut state.capture_sessions)); + for (session, trackers) in sessions.iter_mut() { + let Some((size, scale)) = self.pinnacle.buffer_size_and_scale_for_session(session) + else { + session.stopped(); + continue; + }; + + if (size, scale) != (trackers.size(), trackers.scale()) { + if size != trackers.size() { + session.resized(size); + } + *trackers = SessionDamageTrackers::new(size, scale); + } + + let Some(frame) = session.get_pending_frame(size) else { + continue; + }; + + if let Some(cursor_session) = session.cursor_session() { + let hotspot = self + .pinnacle + .cursor_state + .cursor_hotspot(self.pinnacle.clock.now(), scale) + .unwrap_or_default(); + cursor_session.set_hotspot(hotspot); + } + + let elements = match session.cursor() { + Cursor::Hidden => self + .backend + .with_renderer(|renderer| { + let elements = win.render_elements( + renderer, + (0, 0).into(), + scale.into(), + 1.0, + false, + ); + + elements + .popup_elements + .into_iter() + .chain(elements.surface_elements) + .map(OutputRenderElement::from) + .collect::>() + }) + .unwrap(), + Cursor::Composited => self + .backend + .with_renderer(|renderer| { + let win_loc = self.pinnacle.space.element_location(&win); + let pointer_elements = if let Some(win_loc) = win_loc { + let hotspot = self + .pinnacle + .cursor_state + .cursor_hotspot(self.pinnacle.clock.now(), scale) + .unwrap_or_default(); + + let pointer_loc = + self.pinnacle.seat.get_pointer().unwrap().current_location() + - win_loc.to_f64() + - win.total_decoration_offset().to_f64(); + + let (pointer_elements, _) = pointer_render_elements( + pointer_loc.to_physical_precise_round(scale) + - Point::new(hotspot.x, hotspot.y), + scale, + renderer, + &mut self.pinnacle.cursor_state, + self.pinnacle.dnd_icon.as_ref(), + &self.pinnacle.clock, + ); + pointer_elements + } else { + Vec::new() + }; + let elements = win.render_elements( + renderer, + (0, 0).into(), + scale.into(), + 1.0, + false, + ); + let elements = pointer_elements + .into_iter() + .map(OutputRenderElement::from) + .chain( + elements + .popup_elements + .into_iter() + .chain(elements.surface_elements) + .map(OutputRenderElement::from), + ) + .collect::>(); + elements + }) + .unwrap(), + Cursor::Standalone { pointer: _ } => self + .backend + .with_renderer(|renderer| { + let (pointer_elements, _) = pointer_render_elements( + (0, 0).into(), + scale, + renderer, + &mut self.pinnacle.cursor_state, + self.pinnacle.dnd_icon.as_ref(), + &self.pinnacle.clock, + ); + pointer_elements + .into_iter() + .map(OutputRenderElement::from) + .collect() + }) + .unwrap(), + }; + + self.handle_frame(frame, &elements, trackers); + } + win.with_state_mut(|state| state.capture_sessions = sessions); + } + + for output in self.pinnacle.outputs.clone() { + let mut sessions = + output.with_state_mut(|state| mem::take(&mut state.capture_sessions)); + for (session, trackers) in sessions.iter_mut() { + let Some((size, scale)) = self.pinnacle.buffer_size_and_scale_for_session(session) + else { + session.stopped(); + continue; + }; + + if (size, scale) != (trackers.size(), trackers.scale()) { + if size != trackers.size() { + session.resized(size); + } + *trackers = SessionDamageTrackers::new(size, scale); + } + + let Some(frame) = session.get_pending_frame(size) else { + continue; + }; + + if let Some(cursor_session) = session.cursor_session() { + let hotspot = self + .pinnacle + .cursor_state + .cursor_hotspot(self.pinnacle.clock.now(), scale) + .unwrap_or_default(); + cursor_session.set_hotspot(hotspot); + } + + let elements = match session.cursor() { + Cursor::Hidden => self + .backend + .with_renderer(|renderer| { + output_render_elements( + &output, + renderer, + &self.pinnacle.space, + &self.pinnacle.z_index_stack, + ) + }) + .unwrap(), + Cursor::Composited => { + let Some(output_geo) = self.pinnacle.space.output_geometry(&output) else { + continue; + }; + + let hotspot = self + .pinnacle + .cursor_state + .cursor_hotspot(self.pinnacle.clock.now(), scale) + .unwrap_or_default(); + + let pointer_loc = + self.pinnacle.seat.get_pointer().unwrap().current_location() + - output_geo.loc.to_f64(); + let scale = output.current_scale().fractional_scale(); + + self.backend + .with_renderer(|renderer| { + let (pointer_elements, _) = pointer_render_elements( + pointer_loc.to_physical_precise_round(scale) + - Point::new(hotspot.x, hotspot.y), + scale, + renderer, + &mut self.pinnacle.cursor_state, + self.pinnacle.dnd_icon.as_ref(), + &self.pinnacle.clock, + ); + let elements = output_render_elements( + &output, + renderer, + &self.pinnacle.space, + &self.pinnacle.z_index_stack, + ); + pointer_elements + .into_iter() + .map(OutputRenderElement::from) + .chain(elements) + .collect::>() + }) + .unwrap() + } + Cursor::Standalone { pointer: _ } => { + let scale = output.current_scale().fractional_scale(); + self.backend + .with_renderer(|renderer| { + let (pointer_elements, _) = pointer_render_elements( + (0, 0).into(), + scale, + renderer, + &mut self.pinnacle.cursor_state, + self.pinnacle.dnd_icon.as_ref(), + &self.pinnacle.clock, + ); + pointer_elements + .into_iter() + .map(OutputRenderElement::from) + .collect() + }) + .unwrap() + } + }; + + self.handle_frame(frame, &elements, trackers); + } + + output.with_state_mut(|state| state.capture_sessions = sessions); + } + } + + /// Sends copy-capture clients updated cursor positions relative to their source. + fn update_cursor_capture_positions(&mut self) { + let _span = tracy_client::span!(); + + let cursor_loc = self.pinnacle.seat.get_pointer().unwrap().current_location(); + + for output in self.pinnacle.outputs.iter() { + let sessions = output.with_state(|state| state.cursor_sessions.clone()); + + if sessions.is_empty() { + continue; + } + + let cursor_loc: Point = (cursor_loc + - output.current_location().to_f64()) + .to_physical_precise_round(output.current_scale().fractional_scale()); + let cursor_loc: Point = (cursor_loc.x, cursor_loc.y).into(); + + let mut cursor_geo = self + .pinnacle + .cursor_state + .cursor_geometry( + self.pinnacle.clock.now(), + output.current_scale().fractional_scale(), + ) + .unwrap_or_default(); + + cursor_geo.loc += cursor_loc; + + let mode_size = output + .current_mode() + .map(|mode| mode.size) + .unwrap_or_default(); + let mode_rect: Rectangle = + Rectangle::from_size((mode_size.w, mode_size.h).into()); + + let position = if cursor_geo.overlaps(mode_rect) { + Some(cursor_loc) + } else { + None + }; + + for session in sessions { + session.set_position(position); + } + } + + for window in self.pinnacle.windows.iter() { + let sessions = window.with_state(|state| state.cursor_sessions.clone()); + + if sessions.is_empty() { + continue; + } + + let Some(window_loc) = self.pinnacle.space.element_location(window) else { + continue; + }; + + let Some(surface) = window.wl_surface() else { + continue; + }; + + let fractional_scale = compositor::with_states(&surface, |data| { + with_fractional_scale(data, |scale| scale.preferred_scale()) + }) + .unwrap_or(1.0); + + let cursor_loc = + cursor_loc - window_loc.to_f64() - window.total_decoration_offset().to_f64(); + + let cursor_loc: Point = + cursor_loc.to_physical_precise_round(fractional_scale); + let cursor_loc: Point = (cursor_loc.x, cursor_loc.y).into(); + + let mut cursor_geo = self + .pinnacle + .cursor_state + .cursor_geometry(self.pinnacle.clock.now(), fractional_scale) + .unwrap_or_default(); + + cursor_geo.loc += cursor_loc; + + let buffer_size: Size = window + .geometry_without_decorations() + .size + .to_f64() + .to_physical_precise_round(fractional_scale); + let buffer_geo: Rectangle = + Rectangle::from_size((buffer_size.w, buffer_size.h).into()); + + let position = if cursor_geo.overlaps(buffer_geo) { + Some(cursor_loc) + } else { + None + }; + + for session in sessions { + session.set_position(position); + } + } + } + + /// Renders elements to a [`Frame`] if they caused damage, then notifies the client. + fn handle_frame( + &mut self, + frame: Frame, + elements: &[impl RenderElement], + trackers: &mut SessionDamageTrackers, + ) { + let (damage, _) = trackers.damage.damage_output(1, elements).unwrap(); + let damage = damage.map(|damage| damage.as_slice()).unwrap_or_default(); + if damage.is_empty() { + frame.submit(Transform::Normal, []); + return; + } + + let buffer = frame.buffer(); + let buffer_size = buffer_dimensions(&buffer).expect("this buffer is handled"); + + let client_damage = frame + .buffer_damage() + .into_iter() + .map(BufferDamageElement::new) + .collect::>(); + + self.backend.with_renderer(|renderer| { + let mut dmabuf; + let mut renderbuffer: GlesRenderbuffer; + let mut shm_format = None; + + let mut framebuffer = if let Ok(dma) = get_dmabuf(&buffer).cloned() { + dmabuf = dma; + renderer.bind(&mut dmabuf).unwrap() + } else if let Ok(format) = with_buffer_contents(&buffer, |_, _, data| data.format) { + shm_format = Some(format); + renderbuffer = renderer + .create_buffer(shm_format_to_fourcc(format).unwrap(), buffer_size) + .unwrap(); + renderer.bind(&mut renderbuffer).unwrap() + } else { + panic!("captured frame that doesn't have a shm or dma buffer"); + }; + + let elements = client_damage + .iter() + .map(DynElement::new) + .chain(elements.iter().map(DynElement::new)) + .collect::>(); + + let rendered_damage = trackers + .render + .render_output( + renderer, + &mut framebuffer, + 1, + &elements, + Color32F::TRANSPARENT, + ) + .unwrap(); + + if let Some(shm_format) = shm_format { + let mapping = renderer + .copy_framebuffer( + &framebuffer, + Rectangle::from_size(buffer_size), + shm_format_to_fourcc(shm_format).unwrap(), + ) + .unwrap(); + + let bytes = renderer.map_texture(&mapping).unwrap(); + + for rect in rendered_damage.damage.unwrap() { + if let Err(err) = crate::render::util::blit( + bytes, + buffer_size, + Rectangle::new( + (rect.loc.x, rect.loc.y).into(), + (rect.size.w, rect.size.h).into(), + ), + &buffer, + ) { + error!("failed to copy capture: {err}"); + return; + } + } + } + + frame.submit( + Transform::Normal, + damage.iter().map(|rect| { + Rectangle::new( + (rect.loc.x, rect.loc.y).into(), + (rect.size.w, rect.size.h).into(), + ) + }), + ); + }); + } +} + +impl Pinnacle { + /// Returns the target buffer size and scale for a [`Session`]. + /// + /// Returns `None` if the source doesn't exist. + fn buffer_size_and_scale_for_session( + &mut self, + session: &Session, + ) -> Option<(Size, f64)> { + match session.source() { + Source::Output(wl_output) => { + let output = Output::from_resource(&wl_output)?; + let scale = output.current_scale().fractional_scale(); + + if matches!(session.cursor(), Cursor::Standalone { .. }) { + let geo = self + .cursor_state + .cursor_geometry(self.clock.now(), scale) + .unwrap_or(Rectangle::from_size((1, 1).into())); + Some((geo.size, scale)) + } else { + let size = output.current_mode()?.size; + Some(((size.w, size.h).into(), scale)) + } + } + Source::ForeignToplevel(ext_foreign_toplevel_handle_v1) => { + let window = + self.window_for_foreign_toplevel_handle(&ext_foreign_toplevel_handle_v1)?; + + let surface = window.wl_surface()?; + + let fractional_scale = compositor::with_states(&surface, |data| { + with_fractional_scale(data, |scale| scale.preferred_scale()) + })?; + + if matches!(session.cursor(), Cursor::Standalone { .. }) { + let geo = self + .cursor_state + .cursor_geometry(self.clock.now(), fractional_scale) + .unwrap_or(Rectangle::from_size((1, 1).into())); + Some((geo.size, fractional_scale)) + } else { + let size = window + .geometry_without_decorations() + .size + .to_f64() + .to_buffer(fractional_scale, Transform::Normal) + .to_i32_round(); + + Some((size, fractional_scale)) + } + } + } + } +} + +/// Damage trackers for copy-capture sessions. +#[derive(Debug)] +pub struct SessionDamageTrackers { + /// The "something has changed on-screen" damage tracker. + /// + /// This tracks actual screen damage to see if a new frame + /// should be rendered. + damage: OutputDamageTracker, + /// The rendering damage tracker. + /// + /// This is used to render to session buffers, and the returned damage + /// is used to optimize blitting into said buffers. + render: OutputDamageTracker, +} + +impl SessionDamageTrackers { + /// Creates a new set of damage trackers for handling copy-capture sessions. + fn new(size: Size, scale: f64) -> Self { + Self { + damage: OutputDamageTracker::new((size.w, size.h), scale, Transform::Normal), + render: OutputDamageTracker::new((size.w, size.h), scale, Transform::Normal), + } + } + + /// Returns the current buffer size of these trackers. + fn size(&self) -> Size { + let (size, _, _) = self.render.mode().try_into().unwrap(); + (size.w, size.h).into() + } + + /// Returns the current scale of these trackers. + fn scale(&self) -> f64 { + let (_, scale, _) = self.render.mode().try_into().unwrap(); + scale.x + } +} diff --git a/src/handlers/xwayland.rs b/src/handlers/xwayland.rs index 9d0dc3190..a8233b05b 100644 --- a/src/handlers/xwayland.rs +++ b/src/handlers/xwayland.rs @@ -227,7 +227,6 @@ impl XwmHandler for State { "XwmHandler::configure_notify" ); - #[cfg(feature = "snowcap")] if let Some(win) = self.pinnacle.window_for_x11_surface(&surface) { win.with_state(|state| { for deco in state.decoration_surfaces.iter() { diff --git a/src/hook.rs b/src/hook.rs index fe90cf34b..2a1c3ff21 100644 --- a/src/hook.rs +++ b/src/hook.rs @@ -1,10 +1,10 @@ use smithay::{ - backend::renderer::utils::SurfaceView, + backend::renderer::{buffer_dimensions, utils::SurfaceView}, reexports::{ calloop::Interest, wayland_server::{Resource, protocol::wl_surface::WlSurface}, }, - utils::{HookId, Logical, Point, Rectangle}, + utils::{HookId, Logical, Point, Rectangle, Serial}, wayland::{ compositor::{ self, BufferAssignment, CompositorHandler, SubsurfaceCachedState, SurfaceAttributes, @@ -12,13 +12,16 @@ use smithay::{ }, dmabuf, shell::xdg::{ToplevelSurface, XdgToplevelSurfaceData}, + viewporter::ViewportCachedState, }, }; use tracing::{error, field::Empty, trace, trace_span}; -use crate::state::{Pinnacle, State, WithState}; +use crate::{ + state::{Pinnacle, State, WithState}, + util::transaction::TransactionBuilder, +}; -#[cfg(feature = "snowcap")] pub fn add_decoration_pre_commit_hook(deco: &crate::decoration::DecorationSurface) -> HookId { let wl_surface = deco.wl_surface(); let deco = deco.downgrade(); @@ -156,28 +159,24 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId (got_unmapped, dmabuf, role.configure_serial) }); - #[cfg(feature = "snowcap")] let mut deco_serials = Vec::new(); - #[cfg(feature = "snowcap")] - { - let size = compositor::with_states(surface, |states| { - let mut guard = states - .cached_state - .get::(); - guard.pending().geometry.map(|geo| geo.size) - }) - .unwrap_or_else(|| pending_bbox(surface).size); - - window.with_state(|state| { - for deco in state.decoration_surfaces.iter() { - deco.decoration_surface().with_pending_state(|state| { - state.toplevel_size = Some(size); - }); - deco_serials.push(deco.decoration_surface().send_pending_configure()); - } - }); - } + let size = compositor::with_states(surface, |states| { + let mut guard = states + .cached_state + .get::(); + guard.pending().geometry.map(|geo| geo.size) + }) + .unwrap_or_else(|| pending_bbox(surface).size); + + window.with_state(|state| { + for deco in state.decoration_surfaces.iter() { + deco.decoration_surface().with_pending_state(|state| { + state.toplevel_size = Some(size); + }); + deco_serials.push(deco.decoration_surface().send_pending_configure()); + } + }); let mut transaction_for_dmabuf = None; if let Some(serial) = commit_serial { @@ -185,14 +184,9 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId span.record("serial", format!("{serial:?}")); } - #[cfg(feature = "snowcap")] let mut already_txned_deco = false; - #[cfg(feature = "snowcap")] if window.with_state(|state| state.pending_transactions.is_empty()) { - use crate::util::transaction::TransactionBuilder; - use smithay::utils::Serial; - let txn_builder = TransactionBuilder::new(); let txn = txn_builder.get_transaction(&state.pinnacle.loop_handle); window.with_state_mut(|state| { @@ -215,7 +209,6 @@ pub fn add_mapped_toplevel_pre_commit_hook(toplevel: &ToplevelSurface) -> HookId trace!("taking pending transaction"); if let Some(transaction) = window.take_pending_transaction(serial) { - #[cfg(feature = "snowcap")] if !already_txned_deco { window.with_state(|state| { for (deco, serial) in state.decoration_surfaces.iter().zip(deco_serials) @@ -357,16 +350,11 @@ impl Pinnacle { } } -#[cfg(feature = "snowcap")] fn pending_surface_view(states: &SurfaceData) -> Option { let mut guard = states.cached_state.get::(); let attrs = guard.pending(); match attrs.buffer.as_ref() { Some(BufferAssignment::NewBuffer(buffer)) => { - use smithay::{ - backend::renderer::buffer_dimensions, wayland::viewporter::ViewportCachedState, - }; - let dimens = buffer_dimensions(buffer)?; let surface_size = dimens.to_logical(attrs.buffer_scale, attrs.buffer_transform.into()); let dst = states @@ -400,7 +388,6 @@ fn pending_surface_view(states: &SurfaceData) -> Option { } } -#[cfg(feature = "snowcap")] fn pending_bbox(surface: &WlSurface) -> Rectangle { let _span = tracy_client::span!("crate::hook::pending_bbox"); diff --git a/src/layout.rs b/src/layout.rs index 4f8812c36..4766b745d 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -414,20 +414,15 @@ impl State { for (window, loc) in locs { if let Some(surface) = window.x11_surface() { + let mut configure_loc = loc; + // FIXME: Don't do this here // `loc` includes bounds but we need to configure the x11 surface // with its actual location - #[cfg(feature = "snowcap")] - let loc = if window.should_not_have_ssd() { - loc - } else { - let mut loc = loc; - let max_bounds = window.with_state(|state| state.max_decoration_bounds()); - loc.x += max_bounds.left as i32; - loc.y += max_bounds.top as i32; - loc - }; - let _ = surface.configure(Rectangle::new(loc, surface.geometry().size)); + configure_loc += window.total_decoration_offset(); + + let _ = + surface.configure(Rectangle::new(configure_loc, surface.geometry().size)); } // if the window moved out of an output, we want to get it first. diff --git a/src/lib.rs b/src/lib.rs index 8b9a246d6..4f3609154 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,6 @@ pub mod backend; pub mod cli; pub mod config; pub mod cursor; -#[cfg(feature = "snowcap")] pub mod decoration; pub mod focus; pub mod grab; diff --git a/src/output.rs b/src/output.rs index 2692dc8fc..f71c345da 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -use std::cell::RefCell; +use std::{cell::RefCell, collections::HashMap}; use indexmap::IndexSet; use smithay::{ @@ -17,7 +17,11 @@ use crate::{ api::signal::Signal, backend::BackendData, config::ConnectorSavedState, - protocol::screencopy::Screencopy, + handlers::image_copy_capture::SessionDamageTrackers, + protocol::{ + image_copy_capture::session::{CursorSession, Session}, + screencopy::Screencopy, + }, state::{Pinnacle, State, WithState}, tag::Tag, util::centered_loc, @@ -76,6 +80,9 @@ pub struct OutputState { pub debug_damage_tracker: OutputDamageTracker, pub is_vrr_on: bool, pub is_vrr_on_demand: bool, + + pub capture_sessions: HashMap, + pub cursor_sessions: Vec, } impl Default for OutputState { @@ -95,6 +102,8 @@ impl Default for OutputState { ), is_vrr_on: false, is_vrr_on_demand: false, + capture_sessions: Default::default(), + cursor_sessions: Default::default(), } } } @@ -367,6 +376,12 @@ impl Pinnacle { self.signal_state.output_focused.signal(new_focused_output); } + output.with_state(|state| { + for session in state.capture_sessions.keys() { + session.stopped(); + } + }); + self.gamma_control_manager_state.output_removed(output); self.output_power_management_state.output_removed(output); diff --git a/src/protocol.rs b/src/protocol.rs index 2ffc60bc4..d4187ecd3 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -2,8 +2,9 @@ pub mod drm; pub mod ext_workspace; pub mod foreign_toplevel; pub mod gamma_control; +pub mod image_capture_source; +pub mod image_copy_capture; pub mod output_management; pub mod output_power_management; pub mod screencopy; -#[cfg(feature = "snowcap")] pub mod snowcap_decoration; diff --git a/src/protocol/image_capture_source.rs b/src/protocol/image_capture_source.rs new file mode 100644 index 000000000..1e2e3f62b --- /dev/null +++ b/src/protocol/image_capture_source.rs @@ -0,0 +1,193 @@ +use smithay::reexports::{ + wayland_protocols::ext::{ + foreign_toplevel_list::v1::server::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, + image_capture_source::v1::server::{ + ext_foreign_toplevel_image_capture_source_manager_v1::{ + self, ExtForeignToplevelImageCaptureSourceManagerV1, + }, + ext_image_capture_source_v1::ExtImageCaptureSourceV1, + ext_output_image_capture_source_manager_v1::{ + self, ExtOutputImageCaptureSourceManagerV1, + }, + }, + }, + wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + protocol::wl_output::WlOutput, + }, +}; + +const VERSION: u32 = 1; + +#[derive(Debug)] +pub struct ImageCaptureSourceState; + +impl ImageCaptureSourceState { + pub fn new(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch, + D: GlobalDispatch< + ExtForeignToplevelImageCaptureSourceManagerV1, + ImageCaptureSourceGlobalData, + >, + D: 'static, + F: Fn(&Client) -> bool + Send + Sync + Clone + 'static, + { + let global_data = ImageCaptureSourceGlobalData { + filter: Box::new(filter.clone()), + }; + display.create_global::(VERSION, global_data); + let global_data = ImageCaptureSourceGlobalData { + filter: Box::new(filter.clone()), + }; + display.create_global::( + VERSION, + global_data, + ); + + Self + } +} + +pub struct ImageCaptureSourceGlobalData { + filter: Box bool + Send + Sync>, +} + +#[derive(Debug, Clone)] +pub enum Source { + Output(WlOutput), + ForeignToplevel(ExtForeignToplevelHandleV1), +} + +impl GlobalDispatch + for ImageCaptureSourceState +where + D: Dispatch, +{ + fn bind( + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &ImageCaptureSourceGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, ()); + } + + fn can_view(client: Client, global_data: &ImageCaptureSourceGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl + GlobalDispatch + for ImageCaptureSourceState +where + D: Dispatch, +{ + fn bind( + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &ImageCaptureSourceGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, ()); + } + + fn can_view(client: Client, global_data: &ImageCaptureSourceGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for ImageCaptureSourceState +where + D: Dispatch, +{ + fn request( + _state: &mut D, + _client: &Client, + _resource: &ExtOutputImageCaptureSourceManagerV1, + request: ::Request, + _data: &(), + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_output_image_capture_source_manager_v1::Request::CreateSource { + source, + output, + } => { + data_init.init(source, Source::Output(output)); + } + ext_output_image_capture_source_manager_v1::Request::Destroy => (), + _ => (), + } + } +} + +impl Dispatch for ImageCaptureSourceState +where + D: Dispatch, +{ + fn request( + _state: &mut D, + _client: &Client, + _resource: &ExtForeignToplevelImageCaptureSourceManagerV1, + request: ::Request, + _data: &(), + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_foreign_toplevel_image_capture_source_manager_v1::Request::CreateSource { + source, + toplevel_handle, + } => { + data_init.init(source, Source::ForeignToplevel(toplevel_handle)); + } + ext_foreign_toplevel_image_capture_source_manager_v1::Request::Destroy => (), + _ => (), + } + } +} + +impl Dispatch for ImageCaptureSourceState { + fn request( + _state: &mut D, + _client: &Client, + _resource: &ExtImageCaptureSourceV1, + _request: ::Request, + _data: &Source, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + } +} + +macro_rules! delegate_image_capture_source { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::image_capture_source::v1::server::ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1: $crate::protocol::image_capture_source::ImageCaptureSourceGlobalData + ] => $crate::protocol::image_capture_source::ImageCaptureSourceState); + + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::image_capture_source::v1::server::ext_foreign_toplevel_image_capture_source_manager_v1::ExtForeignToplevelImageCaptureSourceManagerV1: $crate::protocol::image_capture_source::ImageCaptureSourceGlobalData + ] => $crate::protocol::image_capture_source::ImageCaptureSourceState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::image_capture_source::v1::server::ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1: () + ] => $crate::protocol::image_capture_source::ImageCaptureSourceState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::image_capture_source::v1::server::ext_foreign_toplevel_image_capture_source_manager_v1::ExtForeignToplevelImageCaptureSourceManagerV1: () + ] => $crate::protocol::image_capture_source::ImageCaptureSourceState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::image_capture_source::v1::server::ext_image_capture_source_v1::ExtImageCaptureSourceV1: $crate::protocol::image_capture_source::Source + ] => $crate::protocol::image_capture_source::ImageCaptureSourceState); + }; +} +pub(crate) use delegate_image_capture_source; diff --git a/src/protocol/image_copy_capture.rs b/src/protocol/image_copy_capture.rs new file mode 100644 index 000000000..c51662ec8 --- /dev/null +++ b/src/protocol/image_copy_capture.rs @@ -0,0 +1,230 @@ +use std::{collections::HashMap, sync::Mutex}; + +use smithay::{ + backend::{ + allocator::{Format as DrmFormat, Fourcc as DrmFourcc, Modifier as DrmModifier}, + drm::DrmNode, + }, + reexports::{ + wayland_protocols::ext::image_copy_capture::v1::server::{ + ext_image_copy_capture_cursor_session_v1::ExtImageCopyCaptureCursorSessionV1, + ext_image_copy_capture_manager_v1::{self, ExtImageCopyCaptureManagerV1}, + ext_image_copy_capture_session_v1::ExtImageCopyCaptureSessionV1, + }, + wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + protocol::wl_shm, + }, + }, +}; + +use crate::protocol::image_copy_capture::session::{ + Cursor, CursorSession, CursorSessionData, Session, SessionData, +}; + +pub mod frame; +pub mod session; + +const VERSION: u32 = 1; + +#[derive(Debug)] +pub struct ImageCopyCaptureState { + sessions: Vec, + shm_formats: Vec, + dmabuf_formats: HashMap>, + dmabuf_device: Option, +} + +pub struct ImageCopyCaptureGlobalData { + filter: Box bool + Send + Sync>, +} + +impl ImageCopyCaptureState { + pub fn new(display: &DisplayHandle, filter: F) -> Self + where + D: GlobalDispatch + 'static, + F: Fn(&Client) -> bool + Send + Sync + 'static, + { + let global_data = ImageCopyCaptureGlobalData { + filter: Box::new(filter), + }; + display.create_global::(VERSION, global_data); + + Self { + sessions: Vec::new(), + shm_formats: Vec::new(), + dmabuf_formats: HashMap::new(), + dmabuf_device: None, + } + } + + /// Sets format and device constraints for all current and new capture sessions. + pub fn set_buffer_constraints( + &mut self, + shm_formats: impl IntoIterator, + dmabuf_device: Option, + dmabuf_formats: impl IntoIterator, + ) { + self.shm_formats = shm_formats.into_iter().collect(); + + self.dmabuf_device = dmabuf_device; + + self.dmabuf_formats.clear(); + for format in dmabuf_formats.into_iter() { + self.dmabuf_formats + .entry(format.code) + .or_default() + .push(format.modifier); + } + + for session in self.sessions.iter() { + session.set_buffer_constraints( + self.shm_formats.clone(), + self.dmabuf_device, + self.dmabuf_formats.clone(), + ); + } + } +} + +pub trait ImageCopyCaptureHandler { + fn image_copy_capture_state(&mut self) -> &mut ImageCopyCaptureState; + fn new_session(&mut self, session: Session); + fn new_cursor_session(&mut self, cursor_session: CursorSession); + fn session_destroyed(&mut self, session: Session); + fn cursor_session_destroyed(&mut self, cursor_session: CursorSession); +} + +impl GlobalDispatch + for ImageCopyCaptureState +where + D: Dispatch, +{ + fn bind( + _state: &mut D, + _handle: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &ImageCopyCaptureGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + data_init.init(resource, ()); + } + + fn can_view(client: Client, global_data: &ImageCopyCaptureGlobalData) -> bool { + (global_data.filter)(&client) + } +} + +impl Dispatch for ImageCopyCaptureState +where + D: Dispatch> + + Dispatch + + ImageCopyCaptureHandler, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &ExtImageCopyCaptureManagerV1, + request: ::Request, + _data: &(), + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_image_copy_capture_manager_v1::Request::CreateSession { + session, + source, + options, + } => match options.into_result() { + Ok(options) => { + let cursor = if options + .contains(ext_image_copy_capture_manager_v1::Options::PaintCursors) + { + Cursor::Composited + } else { + Cursor::Hidden + }; + + let shm_formats = state.image_copy_capture_state().shm_formats.clone(); + let dmabuf_formats = state.image_copy_capture_state().dmabuf_formats.clone(); + let dmabuf_device = state.image_copy_capture_state().dmabuf_device; + + let session = data_init.init( + session, + Mutex::new(SessionData::new( + source, + cursor, + shm_formats, + dmabuf_formats, + dmabuf_device, + None, + )), + ); + let session = Session::new(session); + + state + .image_copy_capture_state() + .sessions + .push(session.clone()); + + state.new_session(session); + } + Err(err) => { + data_init.init( + session, + Mutex::new(SessionData::new( + source, + Cursor::Hidden, + Default::default(), + Default::default(), + Default::default(), + None, + )), + ); + resource.post_error( + ext_image_copy_capture_manager_v1::Error::InvalidOption, + err.to_string(), + ); + } + }, + ext_image_copy_capture_manager_v1::Request::CreatePointerCursorSession { + session, + source, + pointer, + } => { + let session = data_init.init(session, CursorSessionData::new(source, pointer)); + let session = CursorSession::new(session); + + state.new_cursor_session(session.clone()); + } + ext_image_copy_capture_manager_v1::Request::Destroy => (), + _ => (), + } + } +} + +macro_rules! delegate_image_copy_capture { + ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { + smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::image_copy_capture::v1::server::ext_image_copy_capture_manager_v1::ExtImageCopyCaptureManagerV1: $crate::protocol::image_copy_capture::ImageCopyCaptureGlobalData + ] => $crate::protocol::image_copy_capture::ImageCopyCaptureState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::image_copy_capture::v1::server::ext_image_copy_capture_manager_v1::ExtImageCopyCaptureManagerV1: () + ] => $crate::protocol::image_copy_capture::ImageCopyCaptureState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::image_copy_capture::v1::server::ext_image_copy_capture_session_v1::ExtImageCopyCaptureSessionV1: ::std::sync::Mutex<$crate::protocol::image_copy_capture::session::SessionData> + ] => $crate::protocol::image_copy_capture::ImageCopyCaptureState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::image_copy_capture::v1::server::ext_image_copy_capture_frame_v1::ExtImageCopyCaptureFrameV1: ::std::sync::Mutex<$crate::protocol::image_copy_capture::frame::FrameData> + ] => $crate::protocol::image_copy_capture::ImageCopyCaptureState); + + smithay::reexports::wayland_server::delegate_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ + smithay::reexports::wayland_protocols::ext::image_copy_capture::v1::server::ext_image_copy_capture_cursor_session_v1::ExtImageCopyCaptureCursorSessionV1: $crate::protocol::image_copy_capture::session::CursorSessionData + ] => $crate::protocol::image_copy_capture::ImageCopyCaptureState); + }; +} +pub(crate) use delegate_image_copy_capture; diff --git a/src/protocol/image_copy_capture/frame.rs b/src/protocol/image_copy_capture/frame.rs new file mode 100644 index 000000000..e7cd20a34 --- /dev/null +++ b/src/protocol/image_copy_capture/frame.rs @@ -0,0 +1,259 @@ +use std::{ + sync::{Mutex, MutexGuard}, + time::UNIX_EPOCH, +}; + +use smithay::{ + backend::renderer::{BufferType, buffer_type}, + reexports::{ + wayland_protocols::ext::image_copy_capture::v1::server::ext_image_copy_capture_frame_v1::{ + self, ExtImageCopyCaptureFrameV1, FailureReason, + }, + wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, Resource, protocol::wl_buffer::WlBuffer, + }, + }, + utils::{Buffer, Rectangle, Transform}, +}; +use wayland_backend::server::ClientId; + +use crate::protocol::image_copy_capture::{ImageCopyCaptureHandler, ImageCopyCaptureState}; + +/// A frame that a client has requested capture for. +/// +/// If this is dropped and [`Frame::submit`] has not been called, this will +/// send the `failed` event to the client. +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct Frame { + frame: ExtImageCopyCaptureFrameV1, +} + +impl Drop for Frame { + fn drop(&mut self) { + // There should've been no way for the frame to fail or become idle while this `Frame` exists. + assert!(self.data().frame_state > FrameState::Idle); + + if self.data().frame_state == FrameState::CaptureRequested { + // `submit` has been called with no damage + return; + } + + if self.data().frame_state != FrameState::Submitted { + self.data().frame_state = FrameState::Failed; + self.frame.failed(FailureReason::Unknown); + } + + // This is not strictly necessary, but sctk shm buffer handling + // will prevent access to any buffer while it's not released. I was working + // on a copy-capture test client which is why this is here. + self.buffer().release(); + } +} + +impl Frame { + /// Creates a new frame. + /// + /// This should only be created once the client has sent the capture request. + pub fn new(frame: ExtImageCopyCaptureFrameV1) -> Self { + { + let mut data = frame.data::>().unwrap().lock().unwrap(); + assert_eq!(data.frame_state, FrameState::CaptureRequested); + data.frame_state = FrameState::Capturing; + } + + Self { frame } + } + + /// Gets the buffer the client has attached to this frame. + pub fn buffer(&self) -> WlBuffer { + self.data() + .buffer + .clone() + .expect("frame should have a buffer here") + } + + /// Returns the buffer damage received by the client. + pub fn buffer_damage(&self) -> Vec> { + self.data().client_buffer_damage.clone() + } + + /// Submits this frame. + /// + /// If `damage` is empty, the frame returns to the state it had right after + /// the client sent the capture request. Otherwise, the client is notified of completion. + pub fn submit( + &self, + transform: Transform, + damage: impl IntoIterator>, + ) { + let mut damage = damage.into_iter().peekable(); + if damage.peek().is_none() { + self.data().frame_state = FrameState::CaptureRequested; + return; + } + + let time = UNIX_EPOCH.elapsed().unwrap(); + let tv_sec_hi = (time.as_secs() >> 32) as u32; + let tv_sec_lo = (time.as_secs() & 0xFFFFFFFF) as u32; + let tv_nsec = time.subsec_nanos(); + self.frame.presentation_time(tv_sec_hi, tv_sec_lo, tv_nsec); + self.frame.transform(transform.into()); + for damage in damage { + self.frame + .damage(damage.loc.x, damage.loc.y, damage.size.w, damage.size.h); + } + self.frame.ready(); + self.data().frame_state = FrameState::Submitted; + } + + fn data(&self) -> MutexGuard<'_, FrameData> { + self.frame + .data::>() + .unwrap() + .lock() + .unwrap() + } +} + +#[derive(Debug, Default)] +pub struct FrameData { + pub(super) frame_state: FrameState, + client_buffer_damage: Vec>, + pub(super) buffer: Option, +} + +/// The state of a frame. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum FrameState { + /// The frame has failed. + Failed, + /// The frame is idle. + #[default] + Idle, + /// The client has requested capture. + CaptureRequested, + /// The frame is in the process of capturing. + Capturing, + /// The frame has been captured and the client has been notified. + Submitted, +} + +impl Dispatch, D> for ImageCopyCaptureState +where + D: ImageCopyCaptureHandler, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &ExtImageCopyCaptureFrameV1, + request: ::Request, + data: &Mutex, + _dhandle: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_image_copy_capture_frame_v1::Request::Destroy => (), + ext_image_copy_capture_frame_v1::Request::AttachBuffer { buffer } => { + if data.lock().unwrap().frame_state >= FrameState::CaptureRequested { + resource.post_error( + ext_image_copy_capture_frame_v1::Error::AlreadyCaptured, + "cannot attach a buffer after capturing", + ); + return; + } + + data.lock().unwrap().buffer = Some(buffer); + } + ext_image_copy_capture_frame_v1::Request::DamageBuffer { + x, + y, + width, + height, + } => { + if data.lock().unwrap().frame_state >= FrameState::CaptureRequested { + resource.post_error( + ext_image_copy_capture_frame_v1::Error::AlreadyCaptured, + "cannot damage buffer after capturing", + ); + return; + } + + if x < 0 || y < 0 || width <= 0 || height <= 0 { + resource.post_error( + ext_image_copy_capture_frame_v1::Error::InvalidBufferDamage, + format!( + "x or y were < 0, or width or height were <= 0 \ + (x={x}, y={y}, width={width}, height={height})" + ), + ); + return; + } + + data.lock() + .unwrap() + .client_buffer_damage + .push(Rectangle::new((x, y).into(), (width, height).into())); + } + ext_image_copy_capture_frame_v1::Request::Capture => { + if data.lock().unwrap().frame_state >= FrameState::CaptureRequested { + resource.post_error( + ext_image_copy_capture_frame_v1::Error::AlreadyCaptured, + "this frame was already captured", + ); + return; + } + + match data.lock().unwrap().buffer.clone() { + Some(buffer) => { + if !matches!( + buffer_type(&buffer), + Some(BufferType::Shm | BufferType::Dma) + ) { + if data.lock().unwrap().frame_state != FrameState::Failed { + resource.failed(FailureReason::BufferConstraints); + data.lock().unwrap().frame_state = FrameState::Failed; + } + return; + } + } + None => { + resource.post_error( + ext_image_copy_capture_frame_v1::Error::NoBuffer, + "a buffer must be attached before capturing", + ); + return; + } + } + + if !state + .image_copy_capture_state() + .sessions + .iter() + .any(|session| Some(resource) == session.frame().as_ref()) + { + if data.lock().unwrap().frame_state != FrameState::Failed { + resource.failed(FailureReason::Stopped); + data.lock().unwrap().frame_state = FrameState::Failed; + } + return; + } + + data.lock().unwrap().frame_state = FrameState::CaptureRequested; + } + _ => (), + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + resource: &ExtImageCopyCaptureFrameV1, + _data: &Mutex, + ) { + for session in state.image_copy_capture_state().sessions.iter() { + if session.frame_destroyed(resource) { + break; + } + } + } +} diff --git a/src/protocol/image_copy_capture/session.rs b/src/protocol/image_copy_capture/session.rs new file mode 100644 index 000000000..012ca6b21 --- /dev/null +++ b/src/protocol/image_copy_capture/session.rs @@ -0,0 +1,462 @@ +use std::{ + collections::HashMap, + sync::{ + Mutex, MutexGuard, + atomic::{AtomicBool, AtomicI32, Ordering}, + }, +}; + +use smithay::{ + backend::{ + allocator::{Fourcc as DrmFourcc, Modifier as DrmModifier}, + drm::DrmNode, + renderer::buffer_dimensions, + }, + reexports::{ + wayland_protocols::ext::{ + image_capture_source::v1::server::ext_image_capture_source_v1::ExtImageCaptureSourceV1, + image_copy_capture::v1::server::{ + ext_image_copy_capture_cursor_session_v1::{ + self, ExtImageCopyCaptureCursorSessionV1, + }, + ext_image_copy_capture_frame_v1::{self, ExtImageCopyCaptureFrameV1}, + ext_image_copy_capture_session_v1::{self, ExtImageCopyCaptureSessionV1}, + }, + }, + wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, Resource, + protocol::{wl_pointer::WlPointer, wl_shm}, + }, + }, + utils::{Buffer, Point, Size}, +}; +use wayland_backend::server::ClientId; + +use crate::protocol::{ + image_capture_source::Source, + image_copy_capture::{ + ImageCopyCaptureHandler, ImageCopyCaptureState, + frame::{Frame, FrameData, FrameState}, + }, +}; + +/// An active capture session. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Session { + session: ExtImageCopyCaptureSessionV1, +} + +impl Session { + /// Returns the [`Source`] of this session. + pub fn source(&self) -> Source { + self.data().source.data::().unwrap().clone() + } + + /// Returns how the cursor should be handled during captures. + pub fn cursor(&self) -> Cursor { + self.data().cursor.clone() + } + + /// Notifies the client that this session has stopped. + pub fn stopped(&self) { + self.session.stopped(); + } + + /// Notifies the client that the session has resized. + /// + /// This will also send buffer constraints afterward. + pub fn resized(&self, new_size: Size) { + self.session + .buffer_size(new_size.w as u32, new_size.h as u32); + self.send_buffer_constraints(); + } + + pub(super) fn new(session: ExtImageCopyCaptureSessionV1) -> Self { + Self { session } + } + + pub(super) fn set_buffer_constraints( + &self, + shm_formats: Vec, + dmabuf_device: Option, + dmabuf_formats: HashMap>, + ) { + self.data().shm_formats = shm_formats.into_iter().collect(); + self.data().dmabuf_device = dmabuf_device; + self.data().dmabuf_formats = dmabuf_formats; + + self.send_buffer_constraints(); + } + + /// If the frame belongs to this session, removes it. + /// + /// Returns whether a frame was removed. + pub(super) fn frame_destroyed(&self, frame: &ExtImageCopyCaptureFrameV1) -> bool { + self.data().frame.take_if(|f| f == frame).is_some() + } + + /// Sends buffer constraints for this session to the client. + /// + /// Constraints are taken from the last call to + /// [`ImageCopyCaptureState::set_buffer_constraints`][super::ImageCopyCaptureState::set_buffer_constraints]. + /// + /// This will fail any frame pending for capture. + fn send_buffer_constraints(&self) { + for format in self.data().shm_formats.iter().copied() { + self.session.shm_format(format); + } + + for (&code, modifiers) in self.data().dmabuf_formats.iter() { + if code != DrmFourcc::Xrgb8888 && code != DrmFourcc::Argb8888 { + // TODO: Sending all formats causes pipewire over xdg-desktop-portal-wlr to stop + // working when resizing the buffer, figure that out + continue; + } + let modifiers = modifiers + .iter() + .flat_map(|&modifier| u64::from(modifier).to_ne_bytes()) + .collect(); + self.session.dmabuf_format(code as u32, modifiers); + } + + if let Some(device) = self.data().dmabuf_device { + self.session + .dmabuf_device(device.dev_id().to_ne_bytes().to_vec()); + } + + self.session.done(); + + if let Some(frame) = self.data().frame.clone() { + let mut frame_data = frame.data::>().unwrap().lock().unwrap(); + if let FrameState::CaptureRequested | FrameState::Capturing = frame_data.frame_state { + frame.failed(ext_image_copy_capture_frame_v1::FailureReason::BufferConstraints); + frame_data.frame_state = FrameState::Failed; + } + } + } + + /// Retrieves a requested frame for the given buffer size. + /// + /// This returns a [`Frame`] when the client has requested capture and + /// the attached buffer is the same size as the provided size. + /// + /// If the sizes are different, the frame fails. + pub fn get_pending_frame(&self, size: Size) -> Option { + if self.cursor_session().is_some_and(|cursor_session| { + !cursor_session.data().cursor_entered.load(Ordering::Relaxed) + }) { + // This is a cursor capture session and the cursor is not entered + // (i.e. is not over the source). + return None; + } + + self.data() + .frame + .clone() + .filter(|frame| { + let mut data = frame.data::>().unwrap().lock().unwrap(); + let capture_requested = data.frame_state == FrameState::CaptureRequested; + + capture_requested && { + let buffer = data.buffer.as_ref().unwrap(); + let buffer_size = buffer_dimensions(buffer).unwrap_or_default(); + if buffer_size != size { + data.frame_state = FrameState::Failed; + frame.failed( + ext_image_copy_capture_frame_v1::FailureReason::BufferConstraints, + ); + false + } else { + true + } + } + }) + .map(Frame::new) + } + + /// If this session is for a cursor session, returns the [`CursorSession`]. + pub fn cursor_session(&self) -> Option { + self.data().cursor_session.clone().map(CursorSession::new) + } + + pub(super) fn frame(&self) -> Option { + self.data().frame.clone() + } + + fn data(&self) -> MutexGuard<'_, SessionData> { + self.session + .data::>() + .unwrap() + .lock() + .unwrap() + } +} + +pub struct SessionData { + source: ExtImageCaptureSourceV1, + cursor: Cursor, + frame: Option, + shm_formats: Vec, + dmabuf_formats: HashMap>, + dmabuf_device: Option, + cursor_session: Option, +} + +impl SessionData { + pub(super) fn new( + source: ExtImageCaptureSourceV1, + cursor: Cursor, + shm_formats: Vec, + dmabuf_formats: HashMap>, + dmabuf_device: Option, + cursor_session: Option, + ) -> Self { + Self { + source, + cursor, + frame: None, + shm_formats, + dmabuf_formats, + dmabuf_device, + cursor_session, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +/// How cursors should be handled during copies. +pub enum Cursor { + /// The cursor should be hidden. + Hidden, + /// The cursor should be composited onto the frame. + Composited, + /// Only the cursor should be drawn. + Standalone { pointer: WlPointer }, +} + +/// An active cursor capture session. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CursorSession { + session: ExtImageCopyCaptureCursorSessionV1, +} + +impl CursorSession { + /// The source for this cursor session. + pub fn source(&self) -> &Source { + self.data().source.data::().unwrap() + } + + /// The pointer that this cursor session is capturing. + pub fn pointer(&self) -> &WlPointer { + &self.data().pointer + } + + /// Sets this cursor session's hotspot. + pub fn set_hotspot(&self, hotspot: Point) { + if self.data().hotspot() == hotspot { + return; + } + + self.data().set_hotspot(hotspot); + + if self.data().cursor_entered.load(Ordering::Relaxed) { + self.session.hotspot(hotspot.x, hotspot.y); + } + } + + /// Sets the position of the cursor hotspot relative to the source buffer. + /// + /// If `None`, this will send the leave event to the client. + pub fn set_position(&self, position: Option>) { + let cursor_entered = self.data().cursor_entered.load(Ordering::Relaxed); + if cursor_entered != position.is_some() { + self.data() + .cursor_entered + .store(position.is_some(), Ordering::Relaxed); + if position.is_some() { + self.session.enter(); + let hotspot = self.data().hotspot(); + self.session.hotspot(hotspot.x, hotspot.y); + } else { + self.session.leave(); + } + } + + if let Some(position) = position { + self.session.position(position.x, position.y); + } + } + + pub(super) fn new(session: ExtImageCopyCaptureCursorSessionV1) -> Self { + Self { session } + } + + fn data(&self) -> &CursorSessionData { + self.session.data::().unwrap() + } +} + +pub struct CursorSessionData { + source: ExtImageCaptureSourceV1, + pointer: WlPointer, + capture_session_retrieved: AtomicBool, + current_hotspot: (AtomicI32, AtomicI32), + cursor_entered: AtomicBool, +} + +impl CursorSessionData { + pub(super) fn new(source: ExtImageCaptureSourceV1, pointer: WlPointer) -> Self { + Self { + source, + pointer, + capture_session_retrieved: Default::default(), + current_hotspot: Default::default(), + cursor_entered: Default::default(), + } + } + + fn hotspot(&self) -> Point { + ( + self.current_hotspot.0.load(Ordering::Relaxed), + self.current_hotspot.1.load(Ordering::Relaxed), + ) + .into() + } + + fn set_hotspot(&self, hotspot: Point) { + self.current_hotspot.0.store(hotspot.x, Ordering::Relaxed); + self.current_hotspot.1.store(hotspot.y, Ordering::Relaxed); + } +} + +impl Dispatch, D> for ImageCopyCaptureState +where + D: Dispatch> + ImageCopyCaptureHandler, +{ + fn request( + _state: &mut D, + _client: &Client, + resource: &ExtImageCopyCaptureSessionV1, + request: ::Request, + data: &Mutex, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_image_copy_capture_session_v1::Request::CreateFrame { frame } => { + let frame = data_init.init(frame, Mutex::default()); + if data.lock().unwrap().frame.is_some() { + resource.post_error( + ext_image_copy_capture_session_v1::Error::DuplicateFrame, + "the previous frame must be destroyed before creating a new one", + ); + return; + } + + data.lock().unwrap().frame = Some(frame); + } + ext_image_copy_capture_session_v1::Request::Destroy => (), + _ => (), + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + resource: &ExtImageCopyCaptureSessionV1, + _data: &Mutex, + ) { + state + .image_copy_capture_state() + .sessions + .retain(|session| session.session != *resource); + + state.session_destroyed(Session { + session: resource.clone(), + }); + } +} + +impl Dispatch for ImageCopyCaptureState +where + D: Dispatch> + ImageCopyCaptureHandler, +{ + fn request( + state: &mut D, + _client: &Client, + resource: &ExtImageCopyCaptureCursorSessionV1, + request: ::Request, + data: &CursorSessionData, + _dhandle: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + match request { + ext_image_copy_capture_cursor_session_v1::Request::Destroy => (), + ext_image_copy_capture_cursor_session_v1::Request::GetCaptureSession { session } => { + if data.capture_session_retrieved.load(Ordering::Relaxed) { + resource.post_error( + ext_image_copy_capture_cursor_session_v1::Error::DuplicateSession, + "get_capture_session already sent", + ); + return; + } + + data.capture_session_retrieved + .store(true, Ordering::Relaxed); + + let source = data.source.clone(); + let cursor = Cursor::Standalone { + pointer: data.pointer.clone(), + }; + + let shm_formats = state.image_copy_capture_state().shm_formats.clone(); + let dmabuf_formats = state.image_copy_capture_state().dmabuf_formats.clone(); + let dmabuf_device = state.image_copy_capture_state().dmabuf_device; + + let session = data_init.init( + session, + Mutex::new(SessionData { + source, + cursor, + frame: Default::default(), + shm_formats, + dmabuf_formats, + dmabuf_device, + cursor_session: Some(resource.clone()), + }), + ); + let session = Session { session }; + + state + .image_copy_capture_state() + .sessions + .push(session.clone()); + + state.new_session(session); + } + _ => (), + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + resource: &ExtImageCopyCaptureCursorSessionV1, + _data: &CursorSessionData, + ) { + state.cursor_session_destroyed(CursorSession { + session: resource.clone(), + }); + + for session in state.image_copy_capture_state().sessions.iter() { + if session + .data() + .cursor_session + .take_if(|session| session == resource) + .is_some() + { + break; + } + } + } +} diff --git a/src/render.rs b/src/render.rs index ea6f38a05..882d8a877 100644 --- a/src/render.rs +++ b/src/render.rs @@ -5,6 +5,7 @@ pub mod render_elements; pub mod texture; pub mod util; +use itertools::Itertools; use smithay::{ backend::renderer::{ ImportAll, ImportMem, Renderer, RendererSuper, Texture, @@ -31,6 +32,7 @@ use util::{snapshot::SnapshotRenderElement, surface::WlSurfaceTextureRenderEleme use crate::{ backend::{Backend, udev::UdevRenderer}, + decoration::DecorationSurface, pinnacle_render_elements, state::{State, WithState}, window::{WindowElement, ZIndexElement}, @@ -145,44 +147,40 @@ impl WindowElement { location: Point, scale: Scale, alpha: f32, + include_decorations: bool, ) -> SplitRenderElements> { let _span = tracy_client::span!("WindowElement::render_elements"); - #[cfg(feature = "snowcap")] - let offset = self.with_state(|state| { - let bounds = state.max_decoration_bounds(); - Point::new(bounds.left as i32, bounds.top as i32) - }); + let total_deco_offset = if include_decorations { + self.total_decoration_offset() + } else { + Default::default() + }; - #[cfg(not(feature = "snowcap"))] - let offset = Point::default(); + let window_location = if include_decorations { + self.geometry().loc + } else { + self.geometry_without_decorations().loc + }; - let window_location = (location - self.geometry().loc).to_physical_precise_round(scale); + let window_location = (location - window_location).to_physical_precise_round(scale); // Popups render relative to the actual window, so offset by the decoration offset. - let surface_location = (location + offset).to_physical_precise_round(scale); - - let (deco_elems_under, deco_elems_over) = self.with_state(|state| { - #[cfg(feature = "snowcap")] - { - use itertools::Itertools; - - use crate::decoration::DecorationSurface; - - if self.should_not_have_ssd() { - (Vec::new(), Vec::new()) - } else { - let max_bounds = state.max_decoration_bounds(); + let surface_location = (location + total_deco_offset).to_physical_precise_round(scale); + let (deco_elems_under, deco_elems_over) = + if self.should_not_have_ssd() || !include_decorations { + (Vec::new(), Vec::new()) + } else { + self.with_state(|state| { let mut surfaces = state.decoration_surfaces.iter().collect::>(); surfaces.sort_by_key(|deco| deco.z_index()); let mut surfaces = surfaces.into_iter().rev().peekable(); let mut deco_to_elems = |deco: &DecorationSurface| { let deco_location = { - let mut deco_loc = location + deco.location(); - deco_loc.x += (max_bounds.left - deco.bounds().left) as i32; - deco_loc.y += (max_bounds.top - deco.bounds().top) as i32; + let deco_loc = + location + deco.location() + total_deco_offset - deco.offset(); deco_loc.to_physical_precise_round(scale) }; @@ -206,15 +204,8 @@ impl WindowElement { let deco_elems_under = surfaces.flat_map(deco_to_elems).collect::>(); (deco_elems_under, deco_elems_over) - } - } - - #[cfg(not(feature = "snowcap"))] - { - let _ = state; - (Vec::new(), Vec::new()) - } - }); + }) + }; match self.underlying_surface() { WindowSurface::Wayland(toplevel) => { @@ -274,57 +265,42 @@ impl WindowElement { let popup_location = location.to_physical_precise_round(scale); let window_location = (location - self.geometry().loc).to_physical_precise_round(scale); - let (deco_elems_under, deco_elems_over) = self.with_state(|state| { - #[cfg(feature = "snowcap")] - { - use itertools::Itertools; - - use crate::decoration::DecorationSurface; - - if self.should_not_have_ssd() { - (Vec::new(), Vec::new()) - } else { - let max_bounds = self.with_state(|state| state.max_decoration_bounds()); - - let mut surfaces = state.decoration_surfaces.iter().collect::>(); - surfaces.sort_by_key(|deco| deco.z_index()); - let mut surfaces = surfaces.into_iter().rev().peekable(); - - let mut deco_to_elems = |deco: &DecorationSurface| { - let deco_location = { - let mut deco_loc = location + deco.location(); - deco_loc.x += (max_bounds.left - deco.bounds().left) as i32; - deco_loc.y += (max_bounds.top - deco.bounds().top) as i32; - deco_loc.to_physical_precise_round(scale) - }; - - let surface_elements = texture_render_elements_from_surface_tree( - renderer.as_gles_renderer(), - deco.wl_surface(), - deco_location, - scale, - alpha, - ); - surface_elements + let (deco_elems_under, deco_elems_over) = if self.should_not_have_ssd() { + (Vec::new(), Vec::new()) + } else { + let total_deco_offset = self.total_decoration_offset(); + self.with_state(|state| { + let mut surfaces = state.decoration_surfaces.iter().collect::>(); + surfaces.sort_by_key(|deco| deco.z_index()); + let mut surfaces = surfaces.into_iter().rev().peekable(); + + let mut deco_to_elems = |deco: &DecorationSurface| { + let deco_location = { + let deco_loc = + location + deco.location() + total_deco_offset - deco.offset(); + deco_loc.to_physical_precise_round(scale) }; - let deco_elems_over = surfaces - .peeking_take_while(|deco| deco.z_index() >= 0) - .flat_map(&mut deco_to_elems) - .collect::>(); + let surface_elements = texture_render_elements_from_surface_tree( + renderer.as_gles_renderer(), + deco.wl_surface(), + deco_location, + scale, + alpha, + ); + surface_elements + }; - let deco_elems_under = surfaces.flat_map(deco_to_elems).collect::>(); + let deco_elems_over = surfaces + .peeking_take_while(|deco| deco.z_index() >= 0) + .flat_map(&mut deco_to_elems) + .collect::>(); - (deco_elems_under, deco_elems_over) - } - } + let deco_elems_under = surfaces.flat_map(deco_to_elems).collect::>(); - #[cfg(not(feature = "snowcap"))] - { - let _ = state; - (Vec::new(), Vec::new()) - } - }); + (deco_elems_under, deco_elems_over) + }) + }; match self.underlying_surface() { WindowSurface::Wayland(toplevel) => { @@ -527,7 +503,7 @@ fn window_render_elements( let SplitRenderElements { surface_elements, popup_elements, - } = win.render_elements(renderer, loc, scale, 1.0); + } = win.render_elements(renderer, loc, scale, 1.0, true); popups.extend(popup_elements.into_iter().map(OutputRenderElement::from)); diff --git a/src/render/pointer.rs b/src/render/pointer.rs index 3f913b11f..310531b91 100644 --- a/src/render/pointer.rs +++ b/src/render/pointer.rs @@ -11,19 +11,12 @@ use smithay::{ surface::{WaylandSurfaceRenderElement, render_elements_from_surface_tree}, }, }, - desktop::Space, - input::pointer::CursorImageSurfaceData, - output::Output, reexports::wayland_server::protocol::wl_surface::WlSurface, render_elements, - utils::{Clock, Logical, Monotonic, Point, Scale}, - wayland::compositor, + utils::{Clock, Monotonic, Physical, Point}, }; -use crate::{ - cursor::{CursorState, XCursor}, - window::WindowElement, -}; +use crate::cursor::{CursorState, XCursor}; use super::PRenderer; @@ -44,88 +37,64 @@ render_elements! { /// /// Additionally returns the ids of cursor elements for use in screencopy. pub fn pointer_render_elements( - output: &Output, + location: Point, + scale: f64, renderer: &mut R, cursor_state: &mut CursorState, - space: &Space, - pointer_location: Point, dnd_icon: Option<&WlSurface>, clock: &Clock, ) -> (Vec>, Vec) { - let mut pointer_render_elements = Vec::new(); - let mut cursor_ids = Vec::new(); - - let Some(output_geometry) = space.output_geometry(output) else { - return (pointer_render_elements, cursor_ids); - }; - - let scale = Scale::from(output.current_scale().fractional_scale()); - let integer_scale = output.current_scale().integer_scale(); + let integer_scale = scale.ceil() as i32; let pointer_elem = cursor_state.pointer_element(); - if output_geometry.to_f64().contains(pointer_location) { - let cursor_pos = pointer_location - output_geometry.loc.to_f64(); - - let mut elements = match &pointer_elem { - PointerElement::Hidden => vec![], - PointerElement::Named { cursor, size } => { - let image = cursor.image(clock.now().into(), *size * integer_scale as u32); - let hotspot = (image.xhot as i32, image.yhot as i32); - let buffer = cursor_state.buffer_for_image(image, integer_scale); - let elem = MemoryRenderBufferRenderElement::from_buffer( - renderer, - (cursor_pos - Point::from(hotspot).downscale(integer_scale).to_f64()) - .to_physical_precise_round(scale), - &buffer, - None, - None, - None, - element::Kind::Cursor, - ); - - elem.map(|elem| vec![PointerRenderElement::Memory(elem)]) - .unwrap_or_default() - } - PointerElement::Surface { surface } => { - let hotspot = compositor::with_states(surface, |states| { - states - .data_map - .get::() - .unwrap() - .lock() - .unwrap() - .hotspot - }); - - let elems = render_elements_from_surface_tree( - renderer, - surface, - (cursor_pos - hotspot.to_f64()).to_physical_precise_round(scale), - scale, - 1.0, - element::Kind::Cursor, - ); - - elems - } - }; - - // rust analyzer is so broken wtf why is `elem` {unknown} - cursor_ids = elements.iter().map(|elem| elem.id()).cloned().collect(); - - if let Some(dnd_icon) = dnd_icon { - elements.extend(AsRenderElements::render_elements( - &smithay::desktop::space::SurfaceTree::from_surface(dnd_icon), + let mut pointer_elements = match &pointer_elem { + PointerElement::Hidden => vec![], + PointerElement::Named { cursor, size } => { + let image = cursor.image(clock.now().into(), *size * integer_scale as u32); + let buffer = cursor_state.buffer_for_image(image, integer_scale); + let elem = MemoryRenderBufferRenderElement::from_buffer( renderer, - cursor_pos.to_physical_precise_round(scale), + location.to_f64(), + &buffer, + None, + None, + None, + element::Kind::Cursor, + ); + + elem.map(|elem| vec![PointerRenderElement::Memory(elem)]) + .unwrap_or_default() + } + PointerElement::Surface { surface } => { + let elems = render_elements_from_surface_tree( + renderer, + surface, + location, scale, 1.0, - )); + element::Kind::Cursor, + ); + + elems } + }; - pointer_render_elements = elements; + let cursor_ids = pointer_elements + .iter() + .map(|elem| elem.id()) + .cloned() + .collect(); + + if let Some(dnd_icon) = dnd_icon { + pointer_elements.extend(AsRenderElements::render_elements( + &smithay::desktop::space::SurfaceTree::from_surface(dnd_icon), + renderer, + location, + scale.into(), + 1.0, + )); } - (pointer_render_elements, cursor_ids) + (pointer_elements, cursor_ids) } diff --git a/src/render/util.rs b/src/render/util.rs index 8fded052a..2afa4874e 100644 --- a/src/render/util.rs +++ b/src/render/util.rs @@ -1,5 +1,6 @@ //! Render utilities. +pub mod damage; pub mod snapshot; pub mod surface; @@ -11,7 +12,10 @@ use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement use smithay::backend::renderer::element::{self, Element, Id}; use smithay::backend::renderer::utils::CommitCounter; use smithay::backend::renderer::{Bind, Color32F, Frame, Offscreen, Renderer, RendererSuper}; -use smithay::utils::{Point, Rectangle}; +use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer; +use smithay::reexports::wayland_server::protocol::wl_shm; +use smithay::utils::{Buffer, Point, Rectangle}; +use smithay::wayland::shm::with_buffer_contents_mut; use smithay::{ backend::renderer::{ element::RenderElement, @@ -251,3 +255,135 @@ pub fn render_opaque_regions( } } } + +/// Blits a rectangle of pixels from a source byte buffer into a shm wl buffer. +/// +/// Fails if the provided wl buffer is not shm or either the src or dst are not Argb8888. +/// +/// This function requires the src and dst to be in Argb8888 format. +pub fn blit( + src: &[u8], + src_size: Size, + src_rect: Rectangle, + + dst: &WlBuffer, +) -> anyhow::Result<()> { + if src.len() != (src_size.w * src_size.h * 4) as usize { + anyhow::bail!("src was not correct len"); + } + + let Some(src_rect) = Rectangle::from_size(src_size).intersection(src_rect) else { + anyhow::bail!("src_rect does not overlap src buffer"); + }; + + with_buffer_contents_mut(dst, |mut dst, len, data| { + if Size::new(data.width, data.height) != src_size { + anyhow::bail!("src_size is different from dst size"); + } + + if data.format != wl_shm::Format::Argb8888 { + anyhow::bail!("dst is not argb8888"); + } + + if src.len() != (data.stride * data.height) as usize { + anyhow::bail!( + "src and dst are different lens (src = {}, dst = {})", + src.len(), + len + ); + } + + dst = dst.wrapping_offset(data.offset as isize); + + let stride = data.stride; + + for row_num in src_rect.loc.y..(src_rect.loc.y + src_rect.size.h) { + let src_row = src[(stride * row_num) as usize..].as_ptr(); + // SAFETY: + // - stride * row_num is always less than len so this is always within the allocation + // - count * size_of::() always fits in an isize + let dst_row = unsafe { dst.offset((stride * row_num) as isize) }; + + unsafe { + std::ptr::copy_nonoverlapping( + src_row.offset((src_rect.loc.x * 4) as isize), + dst_row.offset((src_rect.loc.x * 4) as isize), + (src_rect.size.w * 4) as usize, + ); + } + } + + Ok(()) + }) + .context("not a shm buffer")? +} + +pub struct DynElement<'a, R: Renderer>(&'a dyn RenderElement); + +impl<'a, R: Renderer> DynElement<'a, R> { + pub fn new(elem: &'a impl RenderElement) -> Self { + Self(elem as _) + } +} + +impl<'a, R: Renderer> Element for DynElement<'a, R> { + fn id(&self) -> &Id { + self.0.id() + } + + fn current_commit(&self) -> CommitCounter { + self.0.current_commit() + } + + fn src(&self) -> Rectangle { + self.0.src() + } + + fn geometry(&self, scale: Scale) -> Rectangle { + self.0.geometry(scale) + } + + fn location(&self, scale: Scale) -> Point { + self.0.location(scale) + } + + fn transform(&self) -> Transform { + self.0.transform() + } + + fn damage_since( + &self, + scale: Scale, + commit: Option, + ) -> smithay::backend::renderer::utils::DamageSet { + self.0.damage_since(scale, commit) + } + + fn opaque_regions( + &self, + scale: Scale, + ) -> smithay::backend::renderer::utils::OpaqueRegions { + self.0.opaque_regions(scale) + } + + fn alpha(&self) -> f32 { + self.0.alpha() + } + + fn kind(&self) -> element::Kind { + self.0.kind() + } +} + +impl<'a, R: Renderer> RenderElement for DynElement<'a, R> { + fn draw( + &self, + frame: &mut ::Frame<'_, '_>, + src: Rectangle, + dst: Rectangle, + damage: &[Rectangle], + opaque_regions: &[Rectangle], + ) -> Result<(), ::Error> { + self.0.draw(frame, src, dst, damage, opaque_regions) + } +} diff --git a/src/render/util/damage.rs b/src/render/util/damage.rs new file mode 100644 index 000000000..a6a3e4ddf --- /dev/null +++ b/src/render/util/damage.rs @@ -0,0 +1,59 @@ +use smithay::{ + backend::renderer::{ + Renderer, + element::{Element, Id, RenderElement}, + utils::CommitCounter, + }, + utils::{Buffer, Physical, Rectangle, Scale, Size}, +}; + +#[derive(Debug, Clone)] +pub struct BufferDamageElement { + id: Id, + commit: CommitCounter, + geometry: Rectangle, +} + +impl BufferDamageElement { + pub fn new(geometry: Rectangle) -> Self { + Self { + id: Id::new(), + commit: Default::default(), + geometry, + } + } +} + +impl Element for BufferDamageElement { + fn id(&self) -> &Id { + &self.id + } + + fn current_commit(&self) -> CommitCounter { + self.commit + } + + fn src(&self) -> Rectangle { + Rectangle::from_size(Size::new(1.0, 1.0)) + } + + fn geometry(&self, _scale: Scale) -> Rectangle { + Rectangle::new( + (self.geometry.loc.x, self.geometry.loc.y).into(), + (self.geometry.size.w, self.geometry.size.h).into(), + ) + } +} + +impl RenderElement for BufferDamageElement { + fn draw( + &self, + _frame: &mut ::Frame<'_, '_>, + _src: Rectangle, + _dst: Rectangle, + _damage: &[Rectangle], + _opaque_regions: &[Rectangle], + ) -> Result<(), ::Error> { + Ok(()) + } +} diff --git a/src/state.rs b/src/state.rs index de7c86744..0ff9c7f62 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,7 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#[cfg(feature = "snowcap")] -use crate::protocol::snowcap_decoration::SnowcapDecorationState; use crate::{ api::signal::SignalState, backend::{ @@ -24,9 +22,12 @@ use crate::{ ext_workspace::{self, ExtWorkspaceManagerState}, foreign_toplevel::{self, ForeignToplevelManagerState}, gamma_control::GammaControlManagerState, + image_capture_source::ImageCaptureSourceState, + image_copy_capture::ImageCopyCaptureState, output_management::OutputManagementManagerState, output_power_management::OutputPowerManagementState, screencopy::ScreencopyManagerState, + snowcap_decoration::SnowcapDecorationState, }, window::{Unmapped, WindowElement, ZIndexElement, rules::WindowRuleState}, }; @@ -173,9 +174,10 @@ pub struct Pinnacle { pub single_pixel_buffer_state: SinglePixelBufferState, pub foreign_toplevel_list_state: ForeignToplevelListState, pub ext_workspace_state: ExtWorkspaceManagerState, - #[cfg(feature = "snowcap")] pub snowcap_decoration_state: SnowcapDecorationState, pub wl_drm_state: WlDrmState, + pub image_capture_source_state: ImageCaptureSourceState, + pub image_copy_capture_state: ImageCopyCaptureState, pub lock_state: LockState, @@ -265,6 +267,7 @@ impl State { foreign_toplevel::refresh(self); ext_workspace::refresh(self); self.pinnacle.refresh_idle_inhibit(); + self.process_capture_sessions(); self.backend.render_scheduled_outputs(&mut self.pinnacle); @@ -387,6 +390,10 @@ impl Pinnacle { let (blocker_cleared_tx, blocker_cleared_rx) = std::sync::mpsc::channel(); + loop_handle.insert_idle(|state| { + state.set_copy_capture_buffer_constraints(); + }); + let pinnacle = Pinnacle { loop_signal, loop_handle: loop_handle.clone(), @@ -469,9 +476,16 @@ impl Pinnacle { &display_handle, filter_restricted_client, ), - #[cfg(feature = "snowcap")] snowcap_decoration_state: SnowcapDecorationState::new::(&display_handle), wl_drm_state: WlDrmState, + image_capture_source_state: ImageCaptureSourceState::new::( + &display_handle, + filter_restricted_client, + ), + image_copy_capture_state: ImageCopyCaptureState::new::( + &display_handle, + filter_restricted_client, + ), lock_state: LockState::default(), @@ -618,7 +632,6 @@ impl Pinnacle { for window in self.space.elements_for_output(output) { window.send_frame(output, now, FRAME_CALLBACK_THROTTLE, should_send); - #[cfg(feature = "snowcap")] window.with_state(|state| { for deco in state.decoration_surfaces.iter() { deco.send_frame(output, now, FRAME_CALLBACK_THROTTLE, should_send); @@ -708,7 +721,6 @@ impl Pinnacle { } }); - #[cfg(feature = "snowcap")] window.with_state(|state| { for deco in state.decoration_surfaces.iter() { deco.with_surfaces(|surface, states| { @@ -830,7 +842,6 @@ impl Pinnacle { ); // FIXME: get the actual overlap - #[cfg(feature = "snowcap")] window.with_state(|state| { for deco in state.decoration_surfaces.iter() { deco.send_dmabuf_feedback( diff --git a/src/window.rs b/src/window.rs index 820b58849..98a5fa42e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -6,16 +6,23 @@ pub mod rules; use std::{cell::RefCell, collections::HashMap, ops::Deref, rc::Rc}; use indexmap::IndexSet; +use itertools::Itertools; use rules::{ClientRequests, WindowRules}; use smithay::{ - desktop::{Window, WindowSurface, WindowSurfaceType, space::SpaceElement}, + desktop::{ + PopupManager, Window, WindowSurface, WindowSurfaceType, space::SpaceElement, + utils::under_from_surface_tree, + }, output::{Output, WeakOutput}, reexports::{ - wayland_protocols::xdg::{ - decoration::zv1::server::zxdg_toplevel_decoration_v1, - shell::server::{ - xdg_positioner::{Anchor, ConstraintAdjustment, Gravity}, - xdg_toplevel, + wayland_protocols::{ + ext::foreign_toplevel_list::v1::server::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1, + xdg::{ + decoration::zv1::server::zxdg_toplevel_decoration_v1, + shell::server::{ + xdg_positioner::{Anchor, ConstraintAdjustment, Gravity}, + xdg_toplevel, + }, }, }, wayland_server::protocol::wl_surface::WlSurface, @@ -23,6 +30,7 @@ use smithay::{ utils::{IsAlive, Logical, Point, Rectangle, Serial, Size}, wayland::{ compositor, + foreign_toplevel_list::ForeignToplevelHandle, seat::WaylandFocus, shell::xdg::{PositionerState, SurfaceCachedState, XdgToplevelSurfaceData}, xdg_activation::XdgActivationTokenData, @@ -182,34 +190,27 @@ impl WindowElement { pub fn set_pending_geo(&self, size: Size, loc: Option>) { let (mut size, loc) = { - #[cfg(feature = "snowcap")] + // Not `should_not_have_ssd`, we need the calculation done beforehand + if self.with_state(|state| { + state.layout_mode.is_fullscreen() + || state.decoration_mode == Some(zxdg_toplevel_decoration_v1::Mode::ClientSide) + }) || self + .x11_surface() + .is_some_and(|surface| surface.is_decorated()) { - // Not `should_not_have_ssd`, we need the calculation done beforehand - if self.with_state(|state| { - state.layout_mode.is_fullscreen() - || state.decoration_mode - == Some(zxdg_toplevel_decoration_v1::Mode::ClientSide) - }) || self - .x11_surface() - .is_some_and(|surface| surface.is_decorated()) - { - (size, loc) - } else { - let mut size = size; - let mut loc = loc; - let max_bounds = self.with_state(|state| state.max_decoration_bounds()); - if let Some(loc) = loc.as_mut() { - loc.x += max_bounds.left as i32; - loc.y += max_bounds.top as i32; - } - size.w = i32::max(1, size.w - max_bounds.left as i32 - max_bounds.right as i32); - size.h = i32::max(1, size.h - max_bounds.top as i32 - max_bounds.bottom as i32); - (size, loc) + (size, loc) + } else { + let mut size = size; + let mut loc = loc; + let max_bounds = self.with_state(|state| state.max_decoration_bounds()); + if let Some(loc) = loc.as_mut() { + loc.x += max_bounds.left as i32; + loc.y += max_bounds.top as i32; } + size.w = i32::max(1, size.w - max_bounds.left as i32 - max_bounds.right as i32); + size.h = i32::max(1, size.h - max_bounds.top as i32 - max_bounds.bottom as i32); + (size, loc) } - - #[cfg(not(feature = "snowcap"))] - (size, loc) }; size.w = size.w.max(1); @@ -230,27 +231,26 @@ impl WindowElement { } } - /// Gets this window's geometry *taking into account bounds*. + /// Gets this window's geometry *taking into account decoration bounds*. pub fn geometry(&self) -> Rectangle { - #[cfg(feature = "snowcap")] - { - let mut geometry = self.0.geometry(); + let mut geometry = self.0.geometry(); - if self.should_not_have_ssd() { - return geometry; - } + if self.should_not_have_ssd() { + return geometry; + } - let max_bounds = self.with_state(|state| state.max_decoration_bounds()); + let max_bounds = self.with_state(|state| state.max_decoration_bounds()); - geometry.size.w += (max_bounds.left + max_bounds.right) as i32; - geometry.size.h += (max_bounds.top + max_bounds.bottom) as i32; - geometry.loc.x -= max_bounds.left as i32; - geometry.loc.y -= max_bounds.top as i32; + geometry.size.w += (max_bounds.left + max_bounds.right) as i32; + geometry.size.h += (max_bounds.top + max_bounds.bottom) as i32; + geometry.loc.x -= max_bounds.left as i32; + geometry.loc.y -= max_bounds.top as i32; - geometry - } + geometry + } - #[cfg(not(feature = "snowcap"))] + /// Gets this window's geometry ignoring decoration bounds. + pub fn geometry_without_decorations(&self) -> Rectangle { self.0.geometry() } @@ -261,97 +261,79 @@ impl WindowElement { point: P, surface_type: WindowSurfaceType, ) -> Option<(WlSurface, Point)> { - #[cfg(feature = "snowcap")] - { - use itertools::Itertools; - use smithay::desktop::PopupManager; - use smithay::desktop::utils::under_from_surface_tree; - - if self.should_not_have_ssd() { - return self.0.surface_under(point, surface_type); - } - - let point = point.into(); - - let max_bounds = self.with_state(|state| state.max_decoration_bounds()); - - // Check for popups. - if let Some(surface) = self.wl_surface() - && surface_type.contains(WindowSurfaceType::POPUP) - { - // Popups are located relative to the actual window, - // so offset by the decoration offset. - let bounds_offset = Point::new(max_bounds.left as i32, max_bounds.top as i32); - - for (popup, location) in PopupManager::popups_for_surface(&surface) { - let offset = self.geometry().loc + location - popup.geometry().loc; - let surf = under_from_surface_tree( - popup.wl_surface(), - point, - offset + bounds_offset, - surface_type, - ); - if surf.is_some() { - return surf; - } - } - } + if self.should_not_have_ssd() { + return self.0.surface_under(point, surface_type); + } - if !surface_type.contains(WindowSurfaceType::TOPLEVEL) { - return None; - } + let point = point.into(); - let mut decos = self.with_state(|state| state.decoration_surfaces.clone()); - decos.sort_by_key(|deco| deco.z_index()); - let mut decos = decos.into_iter().rev().peekable(); + // Popups are located relative to the actual window, + // so offset by the decoration offset. + let total_deco_offset = self.total_decoration_offset(); - // Check for decoration surfaces above the window. - for deco in decos.peeking_take_while(|deco| deco.z_index() >= 0) { - let bounds_offset = Point::new( - (max_bounds.left - deco.bounds().left) as i32, - (max_bounds.top - deco.bounds().top) as i32, - ); + // Check for popups. + if let Some(surface) = self.wl_surface() + && surface_type.contains(WindowSurfaceType::POPUP) + { + for (popup, location) in PopupManager::popups_for_surface(&surface) { + let offset = self.geometry().loc + location - popup.geometry().loc; let surf = under_from_surface_tree( - deco.wl_surface(), + popup.wl_surface(), point, - deco.location() + self.geometry().loc + bounds_offset, + offset + total_deco_offset, surface_type, ); if surf.is_some() { return surf; } } + } - // Check for the window itself. - if let Some(surface) = self.wl_surface() { - let surf = under_from_surface_tree(&surface, point, (0, 0), surface_type); - if surf.is_some() { - return surf; - } + if !surface_type.contains(WindowSurfaceType::TOPLEVEL) { + return None; + } + + let mut decos = self.with_state(|state| state.decoration_surfaces.clone()); + decos.sort_by_key(|deco| deco.z_index()); + let mut decos = decos.into_iter().rev().peekable(); + + // Check for decoration surfaces above the window. + for deco in decos.peeking_take_while(|deco| deco.z_index() >= 0) { + let offset = total_deco_offset - deco.offset(); + let surf = under_from_surface_tree( + deco.wl_surface(), + point, + deco.location() + self.geometry().loc + offset, + surface_type, + ); + if surf.is_some() { + return surf; } + } - // Check for decoration surfaces below the window. - for deco in decos { - let bounds_offset = Point::new( - (max_bounds.left - deco.bounds().left) as i32, - (max_bounds.top - deco.bounds().top) as i32, - ); - let surf = under_from_surface_tree( - deco.wl_surface(), - point, - deco.location() + self.geometry().loc + bounds_offset, - surface_type, - ); - if surf.is_some() { - return surf; - } + // Check for the window itself. + if let Some(surface) = self.wl_surface() { + let surf = under_from_surface_tree(&surface, point, (0, 0), surface_type); + if surf.is_some() { + return surf; } + } - None + // Check for decoration surfaces below the window. + for deco in decos { + let offset = total_deco_offset - deco.offset(); + let surf = under_from_surface_tree( + deco.wl_surface(), + point, + deco.location() + self.geometry().loc + offset, + surface_type, + ); + if surf.is_some() { + return surf; + } } - #[cfg(not(feature = "snowcap"))] - self.0.surface_under(point, surface_type) + None } pub fn should_not_have_ssd(&self) -> bool { @@ -390,52 +372,38 @@ impl SpaceElement for WindowElement { } fn bbox(&self) -> Rectangle { - #[cfg(feature = "snowcap")] - { - if self.should_not_have_ssd() { - return self.0.bbox(); - } - - let mut bbox = self.0.bbox(); - self.with_state(|state| { - for deco in state.decoration_surfaces.iter() { - // FIXME: verify this - bbox = bbox.merge(deco.bbox()); - } - }); - bbox + if self.should_not_have_ssd() { + return self.0.bbox(); } - #[cfg(not(feature = "snowcap"))] - self.0.bbox() + let mut bbox = self.0.bbox(); + self.with_state(|state| { + for deco in state.decoration_surfaces.iter() { + // FIXME: verify this + bbox = bbox.merge(deco.bbox()); + } + }); + bbox } fn is_in_input_region(&self, point: &Point) -> bool { - #[cfg(feature = "snowcap")] - { - use itertools::Itertools; - - if self.should_not_have_ssd() { - return self.0.is_in_input_region(point); - } + if self.should_not_have_ssd() { + return self.0.is_in_input_region(point); + } - let mut decos = self.with_state(|state| state.decoration_surfaces.clone()); - decos.sort_by_key(|deco| deco.z_index()); - let mut decos = decos.into_iter().rev().peekable(); + let point = *point; - let max_bounds = self.with_state(|state| state.max_decoration_bounds()); + let mut decos = self.with_state(|state| state.decoration_surfaces.clone()); + decos.sort_by_key(|deco| deco.z_index()); + let mut decos = decos.into_iter().rev().peekable(); - decos - .peeking_take_while(|deco| deco.z_index() >= 0) - .any(|deco| deco.surface_under(*point, WindowSurfaceType::ALL).is_some()) - || self.0.is_in_input_region( - &(*point - Point::new(max_bounds.left as f64, max_bounds.top as f64)), - ) - || decos.any(|deco| deco.surface_under(*point, WindowSurfaceType::ALL).is_some()) - } + let deco_offset = self.total_decoration_offset(); - #[cfg(not(feature = "snowcap"))] - self.0.is_in_input_region(point) + decos + .peeking_take_while(|deco| deco.z_index() >= 0) + .any(|deco| deco.surface_under(point, WindowSurfaceType::ALL).is_some()) + || self.0.is_in_input_region(&(point - deco_offset.to_f64())) + || decos.any(|deco| deco.surface_under(point, WindowSurfaceType::ALL).is_some()) } fn set_activate(&self, activated: bool) { @@ -562,6 +530,30 @@ impl Pinnacle { }) } + pub fn window_for_foreign_toplevel_handle( + &self, + handle: &ExtForeignToplevelHandleV1, + ) -> Option<&WindowElement> { + let handle = ForeignToplevelHandle::from_resource(handle)?; + + self.windows + .iter() + .chain( + self.unmapped_windows + .iter() + .map(|unmapped| &unmapped.window), + ) + .find(|win| { + win.with_state(|state| { + state + .foreign_toplevel_list_handle + .as_ref() + .map(|handle| handle.identifier()) + == Some(handle.identifier()) + }) + }) + } + /// Removes a window from the main window vec, z_index stack, and focus stacks. /// /// If `unmap` is true the window has become unmapped and will be pushed to `unmapped_windows`. diff --git a/src/window/window_state.rs b/src/window/window_state.rs index 3dacd3160..e39fb5132 100644 --- a/src/window/window_state.rs +++ b/src/window/window_state.rs @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-or-later -use std::sync::atomic::{AtomicU32, Ordering}; +use std::{ + collections::HashMap, + sync::atomic::{AtomicU32, Ordering}, +}; use indexmap::IndexSet; use smithay::{ @@ -13,9 +16,13 @@ use smithay::{ }; use tracing::warn; -#[cfg(feature = "snowcap")] -use crate::{decoration::DecorationSurface, protocol::snowcap_decoration::Bounds}; use crate::{ + decoration::DecorationSurface, + handlers::image_copy_capture::SessionDamageTrackers, + protocol::{ + image_copy_capture::session::{CursorSession, Session}, + snowcap_decoration::Bounds, + }, render::util::snapshot::WindowSnapshot, state::{Pinnacle, WithState}, tag::Tag, @@ -397,10 +404,20 @@ pub struct WindowElementState { pub snapshot: Option, pub mapped_hook_id: Option, pub foreign_toplevel_list_handle: Option, - #[cfg(feature = "snowcap")] pub decoration_surfaces: Vec, pub vrr_demand: Option, + + pub capture_sessions: HashMap, + pub cursor_sessions: Vec, +} + +impl Drop for WindowElementState { + fn drop(&mut self) { + for session in self.capture_sessions.keys() { + session.stopped(); + } + } } impl WindowElement { @@ -530,6 +547,14 @@ impl WindowElement { .and_then(|toplevel| toplevel.send_pending_configure()) } } + + pub fn total_decoration_offset(&self) -> Point { + if self.should_not_have_ssd() { + Default::default() + } else { + self.with_state(|state| state.total_decoration_offset()) + } + } } impl Pinnacle { @@ -660,9 +685,10 @@ impl WindowElementState { pending_transactions: Default::default(), layout_node: None, foreign_toplevel_list_handle: None, - #[cfg(feature = "snowcap")] decoration_surfaces: Vec::new(), vrr_demand: None, + capture_sessions: Default::default(), + cursor_sessions: Default::default(), } } @@ -680,7 +706,6 @@ impl WindowElementState { self.floating_y = loc.map(|loc| loc.y); } - #[cfg(feature = "snowcap")] pub fn max_decoration_bounds(&self) -> Bounds { let mut max_bounds = Bounds::default(); for deco in self.decoration_surfaces.iter() { @@ -694,6 +719,13 @@ impl WindowElementState { } max_bounds } + + fn total_decoration_offset(&self) -> Point { + Point::new( + self.max_decoration_bounds().left as i32, + self.max_decoration_bounds().top as i32, + ) + } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]