From 69f2a2995d97f24cc71f1352c0ed9de2e8b0481e Mon Sep 17 00:00:00 2001 From: Ottatop Date: Fri, 19 Sep 2025 00:31:46 -0500 Subject: [PATCH 01/14] protocol: Impl ext-image-capture-source and ext-image-copy-capture --- TODO.md | 3 +- src/backend/udev.rs | 18 +- src/backend/winit.rs | 12 +- src/handlers.rs | 44 +- src/handlers/image_capture_source.rs | 3 + src/handlers/image_copy_capture.rs | 529 +++++++++++++++++++++ src/output.rs | 8 +- src/protocol.rs | 2 + src/protocol/image_capture_source.rs | 193 ++++++++ src/protocol/image_copy_capture.rs | 237 +++++++++ src/protocol/image_copy_capture/frame.rs | 254 ++++++++++ src/protocol/image_copy_capture/session.rs | 323 +++++++++++++ src/render/pointer.rs | 134 +++--- src/render/util.rs | 132 ++++- src/render/util/damage.rs | 59 +++ src/state.rs | 16 + src/window.rs | 2 +- src/window/window_state.rs | 10 +- 18 files changed, 1868 insertions(+), 111 deletions(-) create mode 100644 src/handlers/image_capture_source.rs create mode 100644 src/handlers/image_copy_capture.rs create mode 100644 src/protocol/image_capture_source.rs create mode 100644 src/protocol/image_copy_capture.rs create mode 100644 src/protocol/image_copy_capture/frame.rs create mode 100644 src/protocol/image_copy_capture/session.rs create mode 100644 src/render/util/damage.rs diff --git a/TODO.md b/TODO.md index b7dc92fa2..50073b996 100644 --- a/TODO.md +++ b/TODO.md @@ -6,8 +6,7 @@ - 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 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..952e17520 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -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,13 @@ impl Udev { || (pinnacle.lock_state.is_locked() && output.with_state(|state| state.lock_surface.is_none())); + let scale = output.current_scale().fractional_scale(); + let (pointer_render_elements, cursor_ids) = pointer_render_elements( - output, + (pointer_location - output_geo.loc.to_f64()).to_physical_precise_round(scale), + scale, &mut renderer, &mut pinnacle.cursor_state, - &pinnacle.space, - pointer_location, pinnacle.dnd_icon.as_ref(), &pinnacle.clock, ); @@ -1561,6 +1567,10 @@ impl Udev { &pinnacle.loop_handle, cursor_ids, ); + + pinnacle.loop_handle.insert_idle(|state| { + state.process_capture_sessions(); + }); } pinnacle.update_primary_scanout_output(output, &res.states); diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 9d069871f..3cddf2ba5 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -243,12 +243,14 @@ 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 (pointer_render_elements, _cursor_ids) = pointer_render_elements( - &self.output, + (pointer_location - output_loc.to_f64()).to_physical_precise_round(scale), + scale, self.backend.renderer(), &mut pinnacle.cursor_state, - &pinnacle.space, - pointer_location, pinnacle.dnd_icon.as_ref(), &pinnacle.clock, ); @@ -374,6 +376,10 @@ impl Winit { &render_output_result, &pinnacle.loop_handle, ); + + pinnacle.loop_handle.insert_idle(|state| { + state.process_capture_sessions(); + }); } let now = pinnacle.clock.now(); diff --git a/src/handlers.rs b/src/handlers.rs index f779a9f34..7be0f3e7b 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -6,6 +6,8 @@ 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; @@ -215,9 +217,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 +226,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}"); + } } } } 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..72bb327e2 --- /dev/null +++ b/src/handlers/image_copy_capture.rs @@ -0,0 +1,529 @@ +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, Rectangle, Size, Transform}, + wayland::{ + compositor, + dmabuf::get_dmabuf, + foreign_toplevel_list::ForeignToplevelHandle, + 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 + .source_buffer_size_and_scale(&session.source()) + 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 + .windows + .iter() + .find(|win| { + win.with_state(|state| { + state + .foreign_toplevel_list_handle + .as_ref() + .is_some_and(|fth| { + Some(fth.identifier()) + == ForeignToplevelHandle::from_resource( + &ext_foreign_toplevel_handle_v1, + ) + .map(|fth| fth.identifier()) + }) + }) + }) + .cloned() + else { + return; + }; + + window.with_state_mut(|state| state.capture_sessions.insert(session, trackers)); + } + } + } + + fn new_cursor_session(&mut self, _cursor_session: CursorSession) { + // TODO: + } + + 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)); + } + } +} +delegate_image_copy_capture!(State); + +impl State { + 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); + } + + pub fn process_capture_sessions(&mut self) { + for win in self.pinnacle.windows.clone() { + let Some(surface) = win.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 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 + .source_buffer_size_and_scale(&session.source()) + else { + // TODO: stop stream or something idk + 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; + }; + + let elements = match session.cursor() { + Cursor::Hidden => self + .backend + .with_renderer(|renderer| { + win.render_elements( + renderer, + (0, 0).into(), + fractional_scale.into(), + 1.0, + ) + .surface_elements + .into_iter() + .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 pointer_loc = + self.pinnacle.seat.get_pointer().unwrap().current_location() + - win_loc.to_f64(); + let (pointer_elements, _) = pointer_render_elements( + pointer_loc.to_physical_precise_round(scale), + fractional_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(), + fractional_scale.into(), + 1.0, + ); + let elements = pointer_elements + .into_iter() + .map(OutputRenderElement::from) + .chain( + elements + .surface_elements + .into_iter() + .map(OutputRenderElement::from), + ) + .collect::>(); + elements + }) + .unwrap(), + Cursor::Standalone { pointer: _ } => { + let Some(output) = win.output(&self.pinnacle) else { + continue; + }; + let Some(win_loc) = self.pinnacle.space.element_location(&win) else { + continue; + }; + let pointer_loc = + self.pinnacle.seat.get_pointer().unwrap().current_location() + - win_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), + 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 + .source_buffer_size_and_scale(&session.source()) + else { + // TODO: stop stream or something idk + 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; + }; + + 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 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), + 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 Some(output_geo) = self.pinnacle.space.output_geometry(&output) else { + continue; + }; + 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), + 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); + } + } + + fn handle_frame( + &mut self, + frame: Frame, + elements: &[impl RenderElement], + trackers: &mut SessionDamageTrackers, + ) { + 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 (damage, _) = trackers.damage.damage_output(1, elements).unwrap(); + let damage = damage.cloned().unwrap_or_default(); + if damage.is_empty() { + frame.submit(Transform::Normal, []); + return; + } + + 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.into_iter().map(|rect| { + Rectangle::new( + (rect.loc.x, rect.loc.y).into(), + (rect.size.w, rect.size.h).into(), + ) + }), + ); + }); + } +} + +impl Pinnacle { + fn source_buffer_size_and_scale(&self, source: &Source) -> Option<(Size, f64)> { + match source { + Source::Output(wl_output) => { + let output = Output::from_resource(wl_output)?; + let size = output.current_mode()?.size; + let scale = output.current_scale().fractional_scale(); + Some(((size.w, size.h).into(), scale)) + } + Source::ForeignToplevel(ext_foreign_toplevel_handle_v1) => { + let window = self + .windows + .iter() + .find(|win| { + win.with_state(|state| { + state + .foreign_toplevel_list_handle + .as_ref() + .is_some_and(|fth| { + Some(fth.identifier()) + == ForeignToplevelHandle::from_resource( + ext_foreign_toplevel_handle_v1, + ) + .map(|fth| fth.identifier()) + }) + }) + }) + .cloned()?; + + let surface = window.wl_surface()?; + + let fractional_scale = compositor::with_states(&surface, |data| { + with_fractional_scale(data, |scale| scale.preferred_scale()) + })?; + + let size = window + .geometry() + .size + .to_f64() + .to_buffer(fractional_scale, Transform::Normal) + .to_i32_round(); + + Some((size, fractional_scale)) + } + } + } +} + +#[derive(Debug)] +pub struct SessionDamageTrackers { + damage: OutputDamageTracker, + render: OutputDamageTracker, +} + +impl SessionDamageTrackers { + pub 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), + } + } + + pub fn size(&self) -> Size { + let (size, _, _) = self.render.mode().try_into().unwrap(); + (size.w, size.h).into() + } + + pub fn scale(&self) -> f64 { + let (_, scale, _) = self.render.mode().try_into().unwrap(); + scale.x + } +} diff --git a/src/output.rs b/src/output.rs index 2692dc8fc..840a14c0e 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,8 @@ use crate::{ api::signal::Signal, backend::BackendData, config::ConnectorSavedState, - protocol::screencopy::Screencopy, + handlers::image_copy_capture::SessionDamageTrackers, + protocol::{image_copy_capture::session::Session, screencopy::Screencopy}, state::{Pinnacle, State, WithState}, tag::Tag, util::centered_loc, @@ -76,6 +77,8 @@ pub struct OutputState { pub debug_damage_tracker: OutputDamageTracker, pub is_vrr_on: bool, pub is_vrr_on_demand: bool, + + pub capture_sessions: HashMap, } impl Default for OutputState { @@ -95,6 +98,7 @@ impl Default for OutputState { ), is_vrr_on: false, is_vrr_on_demand: false, + capture_sessions: Default::default(), } } } diff --git a/src/protocol.rs b/src/protocol.rs index 2ffc60bc4..9a3ab366f 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -2,6 +2,8 @@ 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; 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..8b20390c8 --- /dev/null +++ b/src/protocol/image_copy_capture.rs @@ -0,0 +1,237 @@ +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, + cursor_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(), + cursor_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); +} + +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 { + source, + cursor, + frame: Default::default(), + shm_formats, + dmabuf_formats, + dmabuf_device, + }), + ); + let session = Session { session }; + + state + .image_copy_capture_state() + .sessions + .push(session.clone()); + + state.new_session(session); + } + Err(err) => { + data_init.init( + session, + Mutex::new(SessionData { + source, + cursor: Cursor::Hidden, + frame: Default::default(), + shm_formats: Default::default(), + dmabuf_formats: Default::default(), + dmabuf_device: Default::default(), + }), + ); + 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 { source, pointer }); + + state.new_cursor_session(CursorSession { + session: session.clone(), + }); + + state + .image_copy_capture_state() + .cursor_sessions + .push(session); + } + 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..a3de4e689 --- /dev/null +++ b/src/protocol/image_copy_capture/frame.rs @@ -0,0 +1,254 @@ +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); + } + } +} + +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, + pub(super) 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..ecc741f09 --- /dev/null +++ b/src/protocol/image_copy_capture/session.rs @@ -0,0 +1,323 @@ +use std::{ + collections::HashMap, + sync::{Mutex, MutexGuard}, +}; + +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, Size}, +}; + +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 { + pub(super) 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 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 { + 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) + } + + pub(super) fn frame(&self) -> Option { + self.data().frame.clone() + } + + fn data(&self) -> MutexGuard<'_, SessionData> { + self.session + .data::>() + .unwrap() + .lock() + .unwrap() + } +} + +pub struct SessionData { + pub(super) source: ExtImageCaptureSourceV1, + pub(super) cursor: Cursor, + pub(super) frame: Option, + pub(super) shm_formats: Vec, + pub(super) dmabuf_formats: HashMap>, + pub(super) dmabuf_device: Option, +} + +#[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 }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CursorSession { + pub(super) session: ExtImageCopyCaptureCursorSessionV1, +} + +impl CursorSession { + pub fn source(&self) -> &Source { + self.session + .data::() + .unwrap() + .source + .data::() + .unwrap() + } + + pub fn pointer(&self) -> &WlPointer { + &self.session.data::().unwrap().pointer + } +} + +pub struct CursorSessionData { + pub(super) source: ExtImageCaptureSourceV1, + pub(super) pointer: WlPointer, +} + +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: wayland_backend::server::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 } => { + 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, + }), + ); + let session = Session { session }; + + state + .image_copy_capture_state() + .sessions + .push(session.clone()); + + state.new_session(session); + } + _ => todo!(), + } + } + + fn destroyed( + _state: &mut D, + _client: wayland_backend::server::ClientId, + _resource: &ExtImageCopyCaptureCursorSessionV1, + _data: &CursorSessionData, + ) { + todo!() + } +} diff --git a/src/render/pointer.rs b/src/render/pointer.rs index 3f913b11f..d3d8d3e67 100644 --- a/src/render/pointer.rs +++ b/src/render/pointer.rs @@ -11,19 +11,14 @@ 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}, + utils::{Clock, Monotonic, Physical, Point}, wayland::compositor, }; -use crate::{ - cursor::{CursorState, XCursor}, - window::WindowElement, -}; +use crate::cursor::{CursorState, XCursor}; use super::PRenderer; @@ -44,88 +39,75 @@ 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 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.to_physical_precise_round(scale), + (location - Point::from(hotspot).downscale(integer_scale)).to_f64(), + &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, + location - hotspot.to_physical_precise_round(scale), 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..3fb5bdfab 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,129 @@ 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, |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() != len { + anyhow::bail!("src and dst are different lens"); + } + + 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..781bd7b87 100644 --- a/src/state.rs +++ b/src/state.rs @@ -24,6 +24,8 @@ 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, @@ -176,6 +178,8 @@ pub struct Pinnacle { #[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, @@ -387,6 +391,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(), @@ -472,6 +480,14 @@ impl Pinnacle { #[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(), diff --git a/src/window.rs b/src/window.rs index 820b58849..0036556a0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -779,7 +779,7 @@ impl State { pub fn map_new_window(&mut self, unmapped: Unmapped) { let _span = tracy_client::span!("State::map_new_window"); - let (window, attempt_float_on_map, focus) = if cfg!(feature = "wlcs") { + let (window, attempt_float_on_map, focus) = if true { // bruh // Relax the requirement that the window should've been configured first // for wlcs diff --git a/src/window/window_state.rs b/src/window/window_state.rs index 3dacd3160..83371d1d0 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::{ @@ -16,6 +19,8 @@ use tracing::warn; #[cfg(feature = "snowcap")] use crate::{decoration::DecorationSurface, protocol::snowcap_decoration::Bounds}; use crate::{ + handlers::image_copy_capture::SessionDamageTrackers, + protocol::image_copy_capture::session::Session, render::util::snapshot::WindowSnapshot, state::{Pinnacle, WithState}, tag::Tag, @@ -401,6 +406,8 @@ pub struct WindowElementState { pub decoration_surfaces: Vec, pub vrr_demand: Option, + + pub capture_sessions: HashMap, } impl WindowElement { @@ -663,6 +670,7 @@ impl WindowElementState { #[cfg(feature = "snowcap")] decoration_surfaces: Vec::new(), vrr_demand: None, + capture_sessions: Default::default(), } } From b801e412276faa135ef7b10e7e2ab56ee64c05f7 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 7 Oct 2025 01:18:51 -0500 Subject: [PATCH 02/14] Impl cursor capture sessions --- TODO.md | 1 + src/backend/udev.rs | 10 +- src/backend/winit.rs | 10 +- src/cursor.rs | 44 +++- src/handlers/image_copy_capture.rs | 283 ++++++++++++++++----- src/output.rs | 7 +- src/protocol/image_copy_capture.rs | 39 +-- src/protocol/image_copy_capture/frame.rs | 2 + src/protocol/image_copy_capture/session.rs | 186 ++++++++++++-- src/render/pointer.rs | 17 +- src/render/util.rs | 12 +- src/state.rs | 1 + src/window/window_state.rs | 4 +- 13 files changed, 483 insertions(+), 133 deletions(-) diff --git a/TODO.md b/TODO.md index 50073b996..5f9136a69 100644 --- a/TODO.md +++ b/TODO.md @@ -7,6 +7,7 @@ - Keyboard focus in Idea Xwayland is weird when creating a new Java file - 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 952e17520..18f953793 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, @@ -1426,8 +1426,14 @@ impl Udev { let scale = output.current_scale().fractional_scale(); + let (_, cursor_hotspot) = pinnacle + .cursor_state + .cursor_geometry_and_hotspot(pinnacle.clock.now(), scale) + .unwrap_or_default(); + let (pointer_render_elements, cursor_ids) = pointer_render_elements( - (pointer_location - output_geo.loc.to_f64()).to_physical_precise_round(scale), + (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, diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 3cddf2ba5..980a6f3c6 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}; @@ -246,8 +246,14 @@ impl Winit { 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_geometry_and_hotspot(pinnacle.clock.now(), scale) + .unwrap_or_default(); + let (pointer_render_elements, _cursor_ids) = pointer_render_elements( - (pointer_location - output_loc.to_f64()).to_physical_precise_round(scale), + (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, diff --git a/src/cursor.rs b/src/cursor.rs index 6523e4665..2fc6106d3 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,45 @@ impl CursorState { } } + pub fn cursor_geometry_and_hotspot( + &mut self, + time: Time, + scale: f64, + ) -> Option<(Rectangle, Point)> { + 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); + let geo = Rectangle::from_size((image.width as i32, image.height as i32).into()); + Some((geo, 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(); + 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, (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/handlers/image_copy_capture.rs b/src/handlers/image_copy_capture.rs index 72bb327e2..feebd1cfb 100644 --- a/src/handlers/image_copy_capture.rs +++ b/src/handlers/image_copy_capture.rs @@ -12,7 +12,7 @@ use smithay::{ }, output::Output, reexports::wayland_server::protocol::wl_shm, - utils::{Buffer, Rectangle, Size, Transform}, + utils::{Buffer, Physical, Point, Rectangle, Size, Transform}, wayland::{ compositor, dmabuf::get_dmabuf, @@ -47,13 +47,11 @@ impl ImageCopyCaptureHandler for State { } fn new_session(&mut self, session: Session) { - let Some((buffer_size, scale)) = self + // FIXME: better tracking of no cursor vs no output/window or something + let (buffer_size, scale) = self .pinnacle - .source_buffer_size_and_scale(&session.source()) - else { - session.stopped(); - return; - }; + .buffer_size_and_scale_for_session(&session) + .unwrap_or(((1, 1).into(), 1.0)); session.resized(buffer_size); let trackers = SessionDamageTrackers::new(buffer_size, scale); @@ -96,8 +94,42 @@ impl ImageCopyCaptureHandler for State { } } - fn new_cursor_session(&mut self, _cursor_session: CursorSession) { - // TODO: + 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 + .windows + .iter() + .find(|win| { + win.with_state(|state| { + state + .foreign_toplevel_list_handle + .as_ref() + .is_some_and(|fth| { + Some(fth.identifier()) + == ForeignToplevelHandle::from_resource( + ext_foreign_toplevel_handle_v1, + ) + .map(|fth| fth.identifier()) + }) + }) + }) + .cloned() + else { + return; + }; + + window.with_state_mut(|state| state.cursor_sessions.push(cursor_session)); + } + } } fn session_destroyed(&mut self, session: Session) { @@ -109,6 +141,24 @@ impl ImageCopyCaptureHandler for State { 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); @@ -148,9 +198,7 @@ impl State { 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 - .source_buffer_size_and_scale(&session.source()) + let Some((size, scale)) = self.pinnacle.buffer_size_and_scale_for_session(session) else { // TODO: stop stream or something idk continue; @@ -188,11 +236,18 @@ impl State { .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_geometry_and_hotspot(self.pinnacle.clock.now(), scale) + .unwrap_or_default(); + let pointer_loc = self.pinnacle.seat.get_pointer().unwrap().current_location() - win_loc.to_f64(); let (pointer_elements, _) = pointer_render_elements( - pointer_loc.to_physical_precise_round(scale), + pointer_loc.to_physical_precise_round(scale) + - Point::new(hotspot.x, hotspot.y), fractional_scale, renderer, &mut self.pinnacle.cursor_state, @@ -222,36 +277,27 @@ impl State { elements }) .unwrap(), - Cursor::Standalone { pointer: _ } => { - let Some(output) = win.output(&self.pinnacle) else { - continue; - }; - let Some(win_loc) = self.pinnacle.space.element_location(&win) else { - continue; - }; - let pointer_loc = - self.pinnacle.seat.get_pointer().unwrap().current_location() - - win_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), - 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() - } + Cursor::Standalone { pointer: _ } => self + .backend + .with_renderer(|renderer| { + let (pointer_elements, _) = pointer_render_elements( + (0, 0).into(), + fractional_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(), }; + // TODO: handle cursor session frames + self.handle_frame(frame, &elements, trackers); } win.with_state_mut(|state| state.capture_sessions = sessions); @@ -261,9 +307,7 @@ impl State { 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 - .source_buffer_size_and_scale(&session.source()) + let Some((size, scale)) = self.pinnacle.buffer_size_and_scale_for_session(session) else { // TODO: stop stream or something idk continue; @@ -325,17 +369,11 @@ impl State { .unwrap() } Cursor::Standalone { pointer: _ } => { - let Some(output_geo) = self.pinnacle.space.output_geometry(&output) else { - continue; - }; - 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), + (0, 0).into(), scale, renderer, &mut self.pinnacle.cursor_state, @@ -358,6 +396,102 @@ impl State { } } + pub fn update_cursor_capture_positions(&mut self) { + 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 Some((mut cursor_geo, _)) = self.pinnacle.cursor_state.cursor_geometry_and_hotspot( + self.pinnacle.clock.now(), + output.current_scale().fractional_scale(), + ) else { + continue; + }; + + 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: Point = + (cursor_loc - window_loc.to_f64()).to_physical_precise_round(fractional_scale); + let cursor_loc: Point = (cursor_loc.x, cursor_loc.y).into(); + + let Some((mut cursor_geo, _)) = self + .pinnacle + .cursor_state + .cursor_geometry_and_hotspot(self.pinnacle.clock.now(), fractional_scale) + else { + continue; + }; + + cursor_geo.loc += cursor_loc; + + let buffer_size: Size = window + .geometry() + .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); + } + } + } + fn handle_frame( &mut self, frame: Frame, @@ -456,13 +590,24 @@ impl State { } impl Pinnacle { - fn source_buffer_size_and_scale(&self, source: &Source) -> Option<(Size, f64)> { - match source { + 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 size = output.current_mode()?.size; + let output = Output::from_resource(&wl_output)?; let scale = output.current_scale().fractional_scale(); - Some(((size.w, size.h).into(), scale)) + + if matches!(session.cursor(), Cursor::Standalone { .. }) { + let (geo, _) = self + .cursor_state + .cursor_geometry_and_hotspot(self.clock.now(), scale)?; + 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 @@ -476,7 +621,7 @@ impl Pinnacle { .is_some_and(|fth| { Some(fth.identifier()) == ForeignToplevelHandle::from_resource( - ext_foreign_toplevel_handle_v1, + &ext_foreign_toplevel_handle_v1, ) .map(|fth| fth.identifier()) }) @@ -490,14 +635,22 @@ impl Pinnacle { with_fractional_scale(data, |scale| scale.preferred_scale()) })?; - let size = window - .geometry() - .size - .to_f64() - .to_buffer(fractional_scale, Transform::Normal) - .to_i32_round(); - - Some((size, fractional_scale)) + if matches!(session.cursor(), Cursor::Standalone { .. }) { + let (geo, hotspot) = self + .cursor_state + .cursor_geometry_and_hotspot(self.clock.now(), fractional_scale)?; + self.image_copy_capture_state.set_cursor_hotspot(hotspot); + Some((geo.size, fractional_scale)) + } else { + let size = window + .geometry() + .size + .to_f64() + .to_buffer(fractional_scale, Transform::Normal) + .to_i32_round(); + + Some((size, fractional_scale)) + } } } } diff --git a/src/output.rs b/src/output.rs index 840a14c0e..fc1ceecb5 100644 --- a/src/output.rs +++ b/src/output.rs @@ -18,7 +18,10 @@ use crate::{ backend::BackendData, config::ConnectorSavedState, handlers::image_copy_capture::SessionDamageTrackers, - protocol::{image_copy_capture::session::Session, screencopy::Screencopy}, + protocol::{ + image_copy_capture::session::{CursorSession, Session}, + screencopy::Screencopy, + }, state::{Pinnacle, State, WithState}, tag::Tag, util::centered_loc, @@ -79,6 +82,7 @@ pub struct OutputState { pub is_vrr_on_demand: bool, pub capture_sessions: HashMap, + pub cursor_sessions: Vec, } impl Default for OutputState { @@ -99,6 +103,7 @@ impl Default for OutputState { is_vrr_on: false, is_vrr_on_demand: false, capture_sessions: Default::default(), + cursor_sessions: Default::default(), } } } diff --git a/src/protocol/image_copy_capture.rs b/src/protocol/image_copy_capture.rs index 8b20390c8..16130002b 100644 --- a/src/protocol/image_copy_capture.rs +++ b/src/protocol/image_copy_capture.rs @@ -16,6 +16,7 @@ use smithay::{ protocol::wl_shm, }, }, + utils::{Buffer, Point}, }; use crate::protocol::image_copy_capture::session::{ @@ -30,7 +31,7 @@ const VERSION: u32 = 1; #[derive(Debug)] pub struct ImageCopyCaptureState { sessions: Vec, - cursor_sessions: Vec, + cursor_sessions: Vec, shm_formats: Vec, dmabuf_formats: HashMap>, dmabuf_device: Option, @@ -87,6 +88,12 @@ impl ImageCopyCaptureState { ); } } + + pub fn set_cursor_hotspot(&self, hotspot: Point) { + for session in self.cursor_sessions.iter() { + session.set_hotspot(hotspot); + } + } } pub trait ImageCopyCaptureHandler { @@ -94,6 +101,7 @@ pub trait ImageCopyCaptureHandler { 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 @@ -153,16 +161,16 @@ where let session = data_init.init( session, - Mutex::new(SessionData { + Mutex::new(SessionData::new( source, cursor, - frame: Default::default(), shm_formats, dmabuf_formats, dmabuf_device, - }), + None, + )), ); - let session = Session { session }; + let session = Session::new(session); state .image_copy_capture_state() @@ -174,14 +182,14 @@ where Err(err) => { data_init.init( session, - Mutex::new(SessionData { + Mutex::new(SessionData::new( source, - cursor: Cursor::Hidden, - frame: Default::default(), - shm_formats: Default::default(), - dmabuf_formats: Default::default(), - dmabuf_device: Default::default(), - }), + Cursor::Hidden, + Default::default(), + Default::default(), + Default::default(), + None, + )), ); resource.post_error( ext_image_copy_capture_manager_v1::Error::InvalidOption, @@ -194,11 +202,10 @@ where source, pointer, } => { - let session = data_init.init(session, CursorSessionData { source, pointer }); + let session = data_init.init(session, CursorSessionData::new(source, pointer)); + let session = CursorSession::new(session); - state.new_cursor_session(CursorSession { - session: session.clone(), - }); + state.new_cursor_session(session.clone()); state .image_copy_capture_state() diff --git a/src/protocol/image_copy_capture/frame.rs b/src/protocol/image_copy_capture/frame.rs index a3de4e689..4a942ec98 100644 --- a/src/protocol/image_copy_capture/frame.rs +++ b/src/protocol/image_copy_capture/frame.rs @@ -42,6 +42,8 @@ impl Drop for Frame { self.data().frame_state = FrameState::Failed; self.frame.failed(FailureReason::Unknown); } + + self.buffer().release(); } } diff --git a/src/protocol/image_copy_capture/session.rs b/src/protocol/image_copy_capture/session.rs index ecc741f09..9af371096 100644 --- a/src/protocol/image_copy_capture/session.rs +++ b/src/protocol/image_copy_capture/session.rs @@ -1,6 +1,9 @@ use std::{ collections::HashMap, - sync::{Mutex, MutexGuard}, + sync::{ + Mutex, MutexGuard, + atomic::{AtomicBool, AtomicI32, Ordering}, + }, }; use smithay::{ @@ -25,8 +28,9 @@ use smithay::{ protocol::{wl_pointer::WlPointer, wl_shm}, }, }, - utils::{Buffer, Size}, + utils::{Buffer, Point, Size}, }; +use wayland_backend::server::ClientId; use crate::protocol::{ image_capture_source::Source, @@ -39,7 +43,7 @@ use crate::protocol::{ /// An active capture session. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Session { - pub(super) session: ExtImageCopyCaptureSessionV1, + session: ExtImageCopyCaptureSessionV1, } impl Session { @@ -67,6 +71,10 @@ impl Session { self.send_buffer_constraints(); } + pub(super) fn new(session: ExtImageCopyCaptureSessionV1) -> Self { + Self { session } + } + pub(super) fn set_buffer_constraints( &self, shm_formats: Vec, @@ -133,6 +141,23 @@ impl Session { /// 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 + .data() + .cursor_session + .as_ref() + .is_some_and(|cursor_session| { + !cursor_session + .data::() + .unwrap() + .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() @@ -171,12 +196,34 @@ impl Session { } pub struct SessionData { - pub(super) source: ExtImageCaptureSourceV1, - pub(super) cursor: Cursor, - pub(super) frame: Option, - pub(super) shm_formats: Vec, - pub(super) dmabuf_formats: HashMap>, - pub(super) dmabuf_device: Option, + 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)] @@ -190,29 +237,95 @@ pub enum Cursor { Standalone { pointer: WlPointer }, } -#[derive(Debug, Clone, PartialEq, Eq)] +/// An active cursor capture session. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct CursorSession { - pub(super) session: ExtImageCopyCaptureCursorSessionV1, + session: ExtImageCopyCaptureCursorSessionV1, } impl CursorSession { + /// The source for this cursor session. pub fn source(&self) -> &Source { - self.session - .data::() - .unwrap() - .source - .data::() - .unwrap() + self.data().source.data::().unwrap() } + /// The pointer that this cursor session is capturing. pub fn pointer(&self) -> &WlPointer { - &self.session.data::().unwrap().pointer + &self.data().pointer + } + + 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); + } + } + + 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 { - pub(super) source: ExtImageCaptureSourceV1, - pub(super) pointer: WlPointer, + 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 @@ -248,7 +361,7 @@ where fn destroyed( state: &mut D, - _client: wayland_backend::server::ClientId, + _client: ClientId, resource: &ExtImageCopyCaptureSessionV1, _data: &Mutex, ) { @@ -270,7 +383,7 @@ where fn request( state: &mut D, _client: &Client, - _resource: &ExtImageCopyCaptureCursorSessionV1, + resource: &ExtImageCopyCaptureCursorSessionV1, request: ::Request, data: &CursorSessionData, _dhandle: &DisplayHandle, @@ -279,6 +392,17 @@ where 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(), @@ -297,6 +421,7 @@ where shm_formats, dmabuf_formats, dmabuf_device, + cursor_session: Some(resource.clone()), }), ); let session = Session { session }; @@ -308,16 +433,23 @@ where state.new_session(session); } - _ => todo!(), + _ => (), } } fn destroyed( - _state: &mut D, - _client: wayland_backend::server::ClientId, - _resource: &ExtImageCopyCaptureCursorSessionV1, + state: &mut D, + _client: ClientId, + resource: &ExtImageCopyCaptureCursorSessionV1, _data: &CursorSessionData, ) { - todo!() + state + .image_copy_capture_state() + .cursor_sessions + .retain(|session| session.session != *resource); + + state.cursor_session_destroyed(CursorSession { + session: resource.clone(), + }); } } diff --git a/src/render/pointer.rs b/src/render/pointer.rs index d3d8d3e67..310531b91 100644 --- a/src/render/pointer.rs +++ b/src/render/pointer.rs @@ -11,11 +11,9 @@ use smithay::{ surface::{WaylandSurfaceRenderElement, render_elements_from_surface_tree}, }, }, - input::pointer::CursorImageSurfaceData, reexports::wayland_server::protocol::wl_surface::WlSurface, render_elements, utils::{Clock, Monotonic, Physical, Point}, - wayland::compositor, }; use crate::cursor::{CursorState, XCursor}; @@ -54,11 +52,10 @@ pub fn pointer_render_elements( 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, - (location - Point::from(hotspot).downscale(integer_scale)).to_f64(), + location.to_f64(), &buffer, None, None, @@ -70,20 +67,10 @@ pub fn pointer_render_elements( .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, - location - hotspot.to_physical_precise_round(scale), + location, scale, 1.0, element::Kind::Cursor, diff --git a/src/render/util.rs b/src/render/util.rs index 3fb5bdfab..2afa4874e 100644 --- a/src/render/util.rs +++ b/src/render/util.rs @@ -276,7 +276,7 @@ pub fn blit( anyhow::bail!("src_rect does not overlap src buffer"); }; - with_buffer_contents_mut(dst, |dst, len, data| { + 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"); } @@ -285,10 +285,16 @@ pub fn blit( anyhow::bail!("dst is not argb8888"); } - if src.len() != len { - anyhow::bail!("src and dst are different lens"); + 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) { diff --git a/src/state.rs b/src/state.rs index 781bd7b87..32d87eb3c 100644 --- a/src/state.rs +++ b/src/state.rs @@ -269,6 +269,7 @@ impl State { foreign_toplevel::refresh(self); ext_workspace::refresh(self); self.pinnacle.refresh_idle_inhibit(); + self.update_cursor_capture_positions(); self.backend.render_scheduled_outputs(&mut self.pinnacle); diff --git a/src/window/window_state.rs b/src/window/window_state.rs index 83371d1d0..b4c804bba 100644 --- a/src/window/window_state.rs +++ b/src/window/window_state.rs @@ -20,7 +20,7 @@ use tracing::warn; use crate::{decoration::DecorationSurface, protocol::snowcap_decoration::Bounds}; use crate::{ handlers::image_copy_capture::SessionDamageTrackers, - protocol::image_copy_capture::session::Session, + protocol::image_copy_capture::session::{CursorSession, Session}, render::util::snapshot::WindowSnapshot, state::{Pinnacle, WithState}, tag::Tag, @@ -408,6 +408,7 @@ pub struct WindowElementState { pub vrr_demand: Option, pub capture_sessions: HashMap, + pub cursor_sessions: Vec, } impl WindowElement { @@ -671,6 +672,7 @@ impl WindowElementState { decoration_surfaces: Vec::new(), vrr_demand: None, capture_sessions: Default::default(), + cursor_sessions: Default::default(), } } From a3554c9b724269b0fd8aa89ca06a9e3512de4514 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 7 Oct 2025 16:28:38 -0500 Subject: [PATCH 03/14] Stop sessions when source is destroyed --- src/output.rs | 6 ++++++ src/window/window_state.rs | 8 ++++++++ 2 files changed, 14 insertions(+) diff --git a/src/output.rs b/src/output.rs index fc1ceecb5..f71c345da 100644 --- a/src/output.rs +++ b/src/output.rs @@ -376,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/window/window_state.rs b/src/window/window_state.rs index b4c804bba..b735b13fb 100644 --- a/src/window/window_state.rs +++ b/src/window/window_state.rs @@ -411,6 +411,14 @@ pub struct WindowElementState { pub cursor_sessions: Vec, } +impl Drop for WindowElementState { + fn drop(&mut self) { + for session in self.capture_sessions.keys() { + session.stopped(); + } + } +} + impl WindowElement { /// Unsets maximized and fullscreen states for both wayland and xwayland windows /// and unsets tiled states for wayland windows. From 25722bd7619beaba1569ccd80e324073c7054d35 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 7 Oct 2025 18:13:05 -0500 Subject: [PATCH 04/14] Don't capture ddecorations --- src/handlers/image_copy_capture.rs | 28 +++++++++++++++++++++++++--- src/render.rs | 13 ++++++++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/handlers/image_copy_capture.rs b/src/handlers/image_copy_capture.rs index feebd1cfb..d47359392 100644 --- a/src/handlers/image_copy_capture.rs +++ b/src/handlers/image_copy_capture.rs @@ -224,6 +224,7 @@ impl State { (0, 0).into(), fractional_scale.into(), 1.0, + false, ) .surface_elements .into_iter() @@ -242,9 +243,19 @@ impl State { .cursor_geometry_and_hotspot(self.pinnacle.clock.now(), scale) .unwrap_or_default(); + #[cfg(feature = "snowcap")] + let pointer_loc = + self.pinnacle.seat.get_pointer().unwrap().current_location() + - win_loc.to_f64() + - win.with_state(|state| { + let bounds = state.max_decoration_bounds(); + Point::new(bounds.left as f64, bounds.top as f64) + }); + #[cfg(not(feature = "snowcap"))] let pointer_loc = self.pinnacle.seat.get_pointer().unwrap().current_location() - win_loc.to_f64(); + let (pointer_elements, _) = pointer_render_elements( pointer_loc.to_physical_precise_round(scale) - Point::new(hotspot.x, hotspot.y), @@ -263,6 +274,7 @@ impl State { (0, 0).into(), fractional_scale.into(), 1.0, + false, ); let elements = pointer_elements .into_iter() @@ -458,8 +470,18 @@ impl State { }) .unwrap_or(1.0); + #[cfg(feature = "snowcap")] + let cursor_loc = cursor_loc + - window_loc.to_f64() + - window.with_state(|state| { + let bounds = state.max_decoration_bounds(); + Point::new(bounds.left as f64, bounds.top as f64) + }); + #[cfg(not(feature = "snowcap"))] + let cursor_loc = cursor_loc - window_loc.to_f64(); + let cursor_loc: Point = - (cursor_loc - window_loc.to_f64()).to_physical_precise_round(fractional_scale); + cursor_loc.to_physical_precise_round(fractional_scale); let cursor_loc: Point = (cursor_loc.x, cursor_loc.y).into(); let Some((mut cursor_geo, _)) = self @@ -472,7 +494,7 @@ impl State { cursor_geo.loc += cursor_loc; - let buffer_size: Size = window + let buffer_size: Size = (**window) .geometry() .size .to_f64() @@ -642,7 +664,7 @@ impl Pinnacle { self.image_copy_capture_state.set_cursor_hotspot(hotspot); Some((geo.size, fractional_scale)) } else { - let size = window + let size = (*window) .geometry() .size .to_f64() diff --git a/src/render.rs b/src/render.rs index ea6f38a05..d644bebf8 100644 --- a/src/render.rs +++ b/src/render.rs @@ -145,6 +145,7 @@ impl WindowElement { location: Point, scale: Scale, alpha: f32, + include_decorations: bool, ) -> SplitRenderElements> { let _span = tracy_client::span!("WindowElement::render_elements"); @@ -157,7 +158,13 @@ impl WindowElement { #[cfg(not(feature = "snowcap"))] let offset = Point::default(); - let window_location = (location - self.geometry().loc).to_physical_precise_round(scale); + let window_location = if include_decorations { + self.geometry().loc + } else { + (**self).geometry().loc + }; + + 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); @@ -169,7 +176,7 @@ impl WindowElement { use crate::decoration::DecorationSurface; - if self.should_not_have_ssd() { + if self.should_not_have_ssd() || !include_decorations { (Vec::new(), Vec::new()) } else { let max_bounds = state.max_decoration_bounds(); @@ -527,7 +534,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)); From 9bd3ef1115e4f72b898e488c69387445cb9837ba Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 7 Oct 2025 21:51:07 -0500 Subject: [PATCH 05/14] Fix pointer hotspot with output capture --- src/handlers/image_copy_capture.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/handlers/image_copy_capture.rs b/src/handlers/image_copy_capture.rs index d47359392..621985a1c 100644 --- a/src/handlers/image_copy_capture.rs +++ b/src/handlers/image_copy_capture.rs @@ -352,6 +352,13 @@ impl State { let Some(output_geo) = self.pinnacle.space.output_geometry(&output) else { continue; }; + + let (_, hotspot) = self + .pinnacle + .cursor_state + .cursor_geometry_and_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(); @@ -359,7 +366,8 @@ impl State { self.backend .with_renderer(|renderer| { let (pointer_elements, _) = pointer_render_elements( - pointer_loc.to_physical_precise_round(scale), + pointer_loc.to_physical_precise_round(scale) + - Point::new(hotspot.x, hotspot.y), scale, renderer, &mut self.pinnacle.cursor_state, @@ -622,9 +630,10 @@ impl Pinnacle { let scale = output.current_scale().fractional_scale(); if matches!(session.cursor(), Cursor::Standalone { .. }) { - let (geo, _) = self + let (geo, hotspot) = self .cursor_state .cursor_geometry_and_hotspot(self.clock.now(), scale)?; + self.image_copy_capture_state.set_cursor_hotspot(hotspot); Some((geo.size, scale)) } else { let size = output.current_mode()?.size; From a208fc60776bbf3a960b6b9e6c52f910adacad86 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Tue, 7 Oct 2025 23:47:38 -0500 Subject: [PATCH 06/14] Un-feature-gate snowcap decoration protocol This just made everything more cumbersome and doesn't actually affect bringing in the actual snowcap stuff --- src/decoration.rs | 5 + src/focus/pointer.rs | 1 - src/handlers.rs | 185 +++++++++----------- src/handlers/image_copy_capture.rs | 22 +-- src/handlers/xwayland.rs | 1 - src/hook.rs | 59 +++---- src/layout.rs | 17 +- src/lib.rs | 1 - src/protocol.rs | 1 - src/render.rs | 124 +++++--------- src/state.rs | 8 +- src/window.rs | 263 ++++++++++++----------------- src/window/window_state.rs | 18 +- 13 files changed, 287 insertions(+), 418 deletions(-) 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 7be0f3e7b..27742e9fa 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -9,13 +9,16 @@ 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::{ @@ -368,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; }; @@ -1034,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() - }; - - 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")?; + let deco_offset = self + .window_for_surface(&root) + .map(|win| win.with_state(|state| state.total_decoration_offset())) + .unwrap_or_default(); - ( - 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_copy_capture.rs b/src/handlers/image_copy_capture.rs index 621985a1c..52fe3de16 100644 --- a/src/handlers/image_copy_capture.rs +++ b/src/handlers/image_copy_capture.rs @@ -243,18 +243,12 @@ impl State { .cursor_geometry_and_hotspot(self.pinnacle.clock.now(), scale) .unwrap_or_default(); - #[cfg(feature = "snowcap")] let pointer_loc = self.pinnacle.seat.get_pointer().unwrap().current_location() - win_loc.to_f64() - - win.with_state(|state| { - let bounds = state.max_decoration_bounds(); - Point::new(bounds.left as f64, bounds.top as f64) - }); - #[cfg(not(feature = "snowcap"))] - let pointer_loc = - self.pinnacle.seat.get_pointer().unwrap().current_location() - - win_loc.to_f64(); + - win + .with_state(|state| state.total_decoration_offset()) + .to_f64(); let (pointer_elements, _) = pointer_render_elements( pointer_loc.to_physical_precise_round(scale) @@ -478,15 +472,11 @@ impl State { }) .unwrap_or(1.0); - #[cfg(feature = "snowcap")] let cursor_loc = cursor_loc - window_loc.to_f64() - - window.with_state(|state| { - let bounds = state.max_decoration_bounds(); - Point::new(bounds.left as f64, bounds.top as f64) - }); - #[cfg(not(feature = "snowcap"))] - let cursor_loc = cursor_loc - window_loc.to_f64(); + - window + .with_state(|state| state.total_decoration_offset()) + .to_f64(); let cursor_loc: Point = cursor_loc.to_physical_precise_round(fractional_scale); 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..ac5caef0b 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -412,21 +412,16 @@ impl State { } } - for (window, loc) in locs { + for (window, mut loc) in locs { if let Some(surface) = window.x11_surface() { // 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 - }; + if !window.should_not_have_ssd() { + let deco_offset = + window.with_state(|state| state.total_decoration_offset()); + loc += deco_offset; + } let _ = surface.configure(Rectangle::new(loc, surface.geometry().size)); } 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/protocol.rs b/src/protocol.rs index 9a3ab366f..d4187ecd3 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -7,5 +7,4 @@ 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/render.rs b/src/render.rs index d644bebf8..54eb47be3 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}, @@ -149,14 +151,7 @@ impl WindowElement { ) -> 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) - }); - - #[cfg(not(feature = "snowcap"))] - let offset = Point::default(); + let total_deco_offset = self.with_state(|state| state.total_decoration_offset()); let window_location = if include_decorations { self.geometry().loc @@ -167,29 +162,21 @@ impl WindowElement { 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() || !include_decorations { - (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) }; @@ -213,15 +200,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) => { @@ -281,57 +261,43 @@ 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 { + self.with_state(|state| { + let total_deco_offset = state.total_decoration_offset(); + + 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) => { diff --git a/src/state.rs b/src/state.rs index 32d87eb3c..8c036dc80 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::{ @@ -29,6 +27,7 @@ use crate::{ output_management::OutputManagementManagerState, output_power_management::OutputPowerManagementState, screencopy::ScreencopyManagerState, + snowcap_decoration::SnowcapDecorationState, }, window::{Unmapped, WindowElement, ZIndexElement, rules::WindowRuleState}, }; @@ -175,7 +174,6 @@ 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, @@ -478,7 +476,6 @@ 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::( @@ -635,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); @@ -725,7 +721,6 @@ impl Pinnacle { } }); - #[cfg(feature = "snowcap")] window.with_state(|state| { for deco in state.decoration_surfaces.iter() { deco.with_surfaces(|surface, states| { @@ -847,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 0036556a0..ba138fd53 100644 --- a/src/window.rs +++ b/src/window.rs @@ -6,9 +6,13 @@ 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::{ @@ -182,34 +186,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); @@ -232,26 +229,20 @@ impl WindowElement { /// Gets this window's geometry *taking into account 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 - } - - #[cfg(not(feature = "snowcap"))] - self.0.geometry() + geometry } /// Returns the surface under the given point relative to @@ -261,97 +252,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.with_state(|state| state.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 +363,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.with_state(|state| state.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) { diff --git a/src/window/window_state.rs b/src/window/window_state.rs index b735b13fb..877ee231f 100644 --- a/src/window/window_state.rs +++ b/src/window/window_state.rs @@ -16,11 +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}, + protocol::{ + image_copy_capture::session::{CursorSession, Session}, + snowcap_decoration::Bounds, + }, render::util::snapshot::WindowSnapshot, state::{Pinnacle, WithState}, tag::Tag, @@ -402,7 +404,6 @@ 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, @@ -676,7 +677,6 @@ 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(), @@ -698,7 +698,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() { @@ -712,6 +711,13 @@ impl WindowElementState { } max_bounds } + + pub 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)] From 0146c0833431c401df11a347bac487fcb3867953 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Wed, 8 Oct 2025 00:02:58 -0500 Subject: [PATCH 07/14] Extract `window_for_foreign_toplevel_handle` --- src/handlers/image_copy_capture.rs | 57 +++--------------------------- src/window.rs | 38 +++++++++++++++++--- 2 files changed, 37 insertions(+), 58 deletions(-) diff --git a/src/handlers/image_copy_capture.rs b/src/handlers/image_copy_capture.rs index 52fe3de16..7066f2b6e 100644 --- a/src/handlers/image_copy_capture.rs +++ b/src/handlers/image_copy_capture.rs @@ -16,7 +16,6 @@ use smithay::{ wayland::{ compositor, dmabuf::get_dmabuf, - foreign_toplevel_list::ForeignToplevelHandle, fractional_scale::with_fractional_scale, seat::WaylandFocus, shm::{shm_format_to_fourcc, with_buffer_contents}, @@ -68,23 +67,7 @@ impl ImageCopyCaptureHandler for State { Source::ForeignToplevel(ext_foreign_toplevel_handle_v1) => { let Some(window) = self .pinnacle - .windows - .iter() - .find(|win| { - win.with_state(|state| { - state - .foreign_toplevel_list_handle - .as_ref() - .is_some_and(|fth| { - Some(fth.identifier()) - == ForeignToplevelHandle::from_resource( - &ext_foreign_toplevel_handle_v1, - ) - .map(|fth| fth.identifier()) - }) - }) - }) - .cloned() + .window_for_foreign_toplevel_handle(&ext_foreign_toplevel_handle_v1) else { return; }; @@ -106,23 +89,7 @@ impl ImageCopyCaptureHandler for State { Source::ForeignToplevel(ext_foreign_toplevel_handle_v1) => { let Some(window) = self .pinnacle - .windows - .iter() - .find(|win| { - win.with_state(|state| { - state - .foreign_toplevel_list_handle - .as_ref() - .is_some_and(|fth| { - Some(fth.identifier()) - == ForeignToplevelHandle::from_resource( - ext_foreign_toplevel_handle_v1, - ) - .map(|fth| fth.identifier()) - }) - }) - }) - .cloned() + .window_for_foreign_toplevel_handle(ext_foreign_toplevel_handle_v1) else { return; }; @@ -631,24 +598,8 @@ impl Pinnacle { } } Source::ForeignToplevel(ext_foreign_toplevel_handle_v1) => { - let window = self - .windows - .iter() - .find(|win| { - win.with_state(|state| { - state - .foreign_toplevel_list_handle - .as_ref() - .is_some_and(|fth| { - Some(fth.identifier()) - == ForeignToplevelHandle::from_resource( - &ext_foreign_toplevel_handle_v1, - ) - .map(|fth| fth.identifier()) - }) - }) - }) - .cloned()?; + let window = + self.window_for_foreign_toplevel_handle(&ext_foreign_toplevel_handle_v1)?; let surface = window.wl_surface()?; diff --git a/src/window.rs b/src/window.rs index ba138fd53..3c65344de 100644 --- a/src/window.rs +++ b/src/window.rs @@ -15,11 +15,14 @@ use smithay::{ }, 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, @@ -27,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, @@ -521,6 +525,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`. From 7894bb618b753054973c39d70bfa8c5820e1e598 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Wed, 8 Oct 2025 00:51:06 -0500 Subject: [PATCH 08/14] Draw popups in captures --- src/handlers/image_copy_capture.rs | 39 +++++++++++++++--------------- src/render.rs | 6 ++++- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/handlers/image_copy_capture.rs b/src/handlers/image_copy_capture.rs index 7066f2b6e..7327fed31 100644 --- a/src/handlers/image_copy_capture.rs +++ b/src/handlers/image_copy_capture.rs @@ -153,16 +153,9 @@ impl State { } pub fn process_capture_sessions(&mut self) { - for win in self.pinnacle.windows.clone() { - let Some(surface) = win.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 _span = tracy_client::span!(); + 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) @@ -186,17 +179,20 @@ impl State { Cursor::Hidden => self .backend .with_renderer(|renderer| { - win.render_elements( + let elements = win.render_elements( renderer, (0, 0).into(), - fractional_scale.into(), + scale.into(), 1.0, false, - ) - .surface_elements - .into_iter() - .map(OutputRenderElement::from) - .collect::>() + ); + + elements + .popup_elements + .into_iter() + .chain(elements.surface_elements) + .map(OutputRenderElement::from) + .collect::>() }) .unwrap(), Cursor::Composited => self @@ -220,7 +216,7 @@ impl State { let (pointer_elements, _) = pointer_render_elements( pointer_loc.to_physical_precise_round(scale) - Point::new(hotspot.x, hotspot.y), - fractional_scale, + scale, renderer, &mut self.pinnacle.cursor_state, self.pinnacle.dnd_icon.as_ref(), @@ -233,7 +229,7 @@ impl State { let elements = win.render_elements( renderer, (0, 0).into(), - fractional_scale.into(), + scale.into(), 1.0, false, ); @@ -242,8 +238,9 @@ impl State { .map(OutputRenderElement::from) .chain( elements - .surface_elements + .popup_elements .into_iter() + .chain(elements.surface_elements) .map(OutputRenderElement::from), ) .collect::>(); @@ -255,7 +252,7 @@ impl State { .with_renderer(|renderer| { let (pointer_elements, _) = pointer_render_elements( (0, 0).into(), - fractional_scale, + scale, renderer, &mut self.pinnacle.cursor_state, self.pinnacle.dnd_icon.as_ref(), @@ -378,6 +375,8 @@ impl State { } pub 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() { diff --git a/src/render.rs b/src/render.rs index 54eb47be3..a64ad09b4 100644 --- a/src/render.rs +++ b/src/render.rs @@ -151,7 +151,11 @@ impl WindowElement { ) -> SplitRenderElements> { let _span = tracy_client::span!("WindowElement::render_elements"); - let total_deco_offset = self.with_state(|state| state.total_decoration_offset()); + let total_deco_offset = if include_decorations { + self.with_state(|state| state.total_decoration_offset()) + } else { + Default::default() + }; let window_location = if include_decorations { self.geometry().loc From 1b47e4da092f6595f44df516e3d600fc45d0dc88 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Wed, 8 Oct 2025 19:26:24 -0500 Subject: [PATCH 09/14] Fix xwayland mapping oops --- src/layout.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/layout.rs b/src/layout.rs index ac5caef0b..00baa944d 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -412,17 +412,20 @@ impl State { } } - for (window, mut loc) in locs { + 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 if !window.should_not_have_ssd() { let deco_offset = window.with_state(|state| state.total_decoration_offset()); - loc += deco_offset; + configure_loc += deco_offset; } - let _ = surface.configure(Rectangle::new(loc, surface.geometry().size)); + let _ = + surface.configure(Rectangle::new(configure_loc, surface.geometry().size)); } // if the window moved out of an output, we want to get it first. From d0844a2ad416d15534c5ab8c9422946a42578853 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Thu, 9 Oct 2025 20:11:58 -0500 Subject: [PATCH 10/14] Split up `cursor_geometry_and_hotspot` --- src/backend/udev.rs | 4 +-- src/backend/winit.rs | 4 +-- src/cursor.rs | 43 +++++++++++++++++++++--------- src/handlers/image_copy_capture.rs | 41 +++++++++++++--------------- 4 files changed, 53 insertions(+), 39 deletions(-) diff --git a/src/backend/udev.rs b/src/backend/udev.rs index 18f953793..cba24c36c 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -1426,9 +1426,9 @@ impl Udev { let scale = output.current_scale().fractional_scale(); - let (_, cursor_hotspot) = pinnacle + let cursor_hotspot = pinnacle .cursor_state - .cursor_geometry_and_hotspot(pinnacle.clock.now(), scale) + .cursor_hotspot(pinnacle.clock.now(), scale) .unwrap_or_default(); let (pointer_render_elements, cursor_ids) = pointer_render_elements( diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 980a6f3c6..0fe29b6be 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -246,9 +246,9 @@ impl Winit { let output_loc = pinnacle.space.output_geometry(&self.output).unwrap().loc; let scale = self.output.current_scale().fractional_scale(); - let (_, cursor_hotspot) = pinnacle + let cursor_hotspot = pinnacle .cursor_state - .cursor_geometry_and_hotspot(pinnacle.clock.now(), scale) + .cursor_hotspot(pinnacle.clock.now(), scale) .unwrap_or_default(); let (pointer_render_elements, _cursor_ids) = pointer_render_elements( diff --git a/src/cursor.rs b/src/cursor.rs index 2fc6106d3..62296d360 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -146,18 +146,43 @@ impl CursorState { } } - pub fn cursor_geometry_and_hotspot( + pub fn cursor_geometry( &mut self, time: Time, scale: f64, - ) -> Option<(Rectangle, Point)> { + ) -> 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); let geo = Rectangle::from_size((image.width as i32, image.height as i32).into()); - Some((geo, Point::from(hotspot).downscale(scale.ceil() as i32))) + 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| { @@ -172,15 +197,7 @@ impl CursorState { .to_f64() .upscale(scale) .to_i32_round(); - 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, (hotspot.x, hotspot.y).into())) + Some((hotspot.x, hotspot.y).into()) } } } diff --git a/src/handlers/image_copy_capture.rs b/src/handlers/image_copy_capture.rs index 7327fed31..ffa43e490 100644 --- a/src/handlers/image_copy_capture.rs +++ b/src/handlers/image_copy_capture.rs @@ -200,10 +200,10 @@ impl State { .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 + let hotspot = self .pinnacle .cursor_state - .cursor_geometry_and_hotspot(self.pinnacle.clock.now(), scale) + .cursor_hotspot(self.pinnacle.clock.now(), scale) .unwrap_or_default(); let pointer_loc = @@ -311,16 +311,17 @@ impl State { continue; }; - let (_, hotspot) = self + let hotspot = self .pinnacle .cursor_state - .cursor_geometry_and_hotspot(self.pinnacle.clock.now(), scale) + .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( @@ -391,12 +392,14 @@ impl State { .to_physical_precise_round(output.current_scale().fractional_scale()); let cursor_loc: Point = (cursor_loc.x, cursor_loc.y).into(); - let Some((mut cursor_geo, _)) = self.pinnacle.cursor_state.cursor_geometry_and_hotspot( - self.pinnacle.clock.now(), - output.current_scale().fractional_scale(), - ) else { - continue; - }; + 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; @@ -448,13 +451,11 @@ impl State { cursor_loc.to_physical_precise_round(fractional_scale); let cursor_loc: Point = (cursor_loc.x, cursor_loc.y).into(); - let Some((mut cursor_geo, _)) = self + let mut cursor_geo = self .pinnacle .cursor_state - .cursor_geometry_and_hotspot(self.pinnacle.clock.now(), fractional_scale) - else { - continue; - }; + .cursor_geometry(self.pinnacle.clock.now(), fractional_scale) + .unwrap_or_default(); cursor_geo.loc += cursor_loc; @@ -586,10 +587,7 @@ impl Pinnacle { let scale = output.current_scale().fractional_scale(); if matches!(session.cursor(), Cursor::Standalone { .. }) { - let (geo, hotspot) = self - .cursor_state - .cursor_geometry_and_hotspot(self.clock.now(), scale)?; - self.image_copy_capture_state.set_cursor_hotspot(hotspot); + let geo = self.cursor_state.cursor_geometry(self.clock.now(), scale)?; Some((geo.size, scale)) } else { let size = output.current_mode()?.size; @@ -607,10 +605,9 @@ impl Pinnacle { })?; if matches!(session.cursor(), Cursor::Standalone { .. }) { - let (geo, hotspot) = self + let geo = self .cursor_state - .cursor_geometry_and_hotspot(self.clock.now(), fractional_scale)?; - self.image_copy_capture_state.set_cursor_hotspot(hotspot); + .cursor_geometry(self.clock.now(), fractional_scale)?; Some((geo.size, fractional_scale)) } else { let size = (*window) From eaf70b809e9bdb5b34b55cc07d8aafdcc86e1ee1 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Thu, 9 Oct 2025 20:53:49 -0500 Subject: [PATCH 11/14] copy-capture: Set cursor hotspot better --- src/handlers/image_copy_capture.rs | 18 ++++++++++++++++++ src/protocol/image_copy_capture.rs | 7 ------- src/protocol/image_copy_capture/session.rs | 4 ++++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/handlers/image_copy_capture.rs b/src/handlers/image_copy_capture.rs index ffa43e490..d2fda6ece 100644 --- a/src/handlers/image_copy_capture.rs +++ b/src/handlers/image_copy_capture.rs @@ -175,6 +175,15 @@ impl State { 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 @@ -294,6 +303,15 @@ impl State { 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 diff --git a/src/protocol/image_copy_capture.rs b/src/protocol/image_copy_capture.rs index 16130002b..ff88d8a85 100644 --- a/src/protocol/image_copy_capture.rs +++ b/src/protocol/image_copy_capture.rs @@ -16,7 +16,6 @@ use smithay::{ protocol::wl_shm, }, }, - utils::{Buffer, Point}, }; use crate::protocol::image_copy_capture::session::{ @@ -88,12 +87,6 @@ impl ImageCopyCaptureState { ); } } - - pub fn set_cursor_hotspot(&self, hotspot: Point) { - for session in self.cursor_sessions.iter() { - session.set_hotspot(hotspot); - } - } } pub trait ImageCopyCaptureHandler { diff --git a/src/protocol/image_copy_capture/session.rs b/src/protocol/image_copy_capture/session.rs index 9af371096..2c2d1a40d 100644 --- a/src/protocol/image_copy_capture/session.rs +++ b/src/protocol/image_copy_capture/session.rs @@ -182,6 +182,10 @@ impl Session { .map(Frame::new) } + pub fn cursor_session(&self) -> Option { + self.data().cursor_session.clone().map(CursorSession::new) + } + pub(super) fn frame(&self) -> Option { self.data().frame.clone() } From f6ab9fd8b305d403c77263c103451f8985ba85ab Mon Sep 17 00:00:00 2001 From: Ottatop Date: Thu, 9 Oct 2025 22:22:46 -0500 Subject: [PATCH 12/14] copy-capture: Cleanup --- src/backend/udev.rs | 4 -- src/backend/winit.rs | 4 -- src/handlers/image_copy_capture.rs | 71 +++++++++++++++------- src/protocol/image_copy_capture.rs | 7 --- src/protocol/image_copy_capture/frame.rs | 5 +- src/protocol/image_copy_capture/session.rs | 37 +++++------ src/state.rs | 2 +- src/window.rs | 2 +- 8 files changed, 74 insertions(+), 58 deletions(-) diff --git a/src/backend/udev.rs b/src/backend/udev.rs index cba24c36c..fc6e5e40c 100644 --- a/src/backend/udev.rs +++ b/src/backend/udev.rs @@ -1573,10 +1573,6 @@ impl Udev { &pinnacle.loop_handle, cursor_ids, ); - - pinnacle.loop_handle.insert_idle(|state| { - state.process_capture_sessions(); - }); } pinnacle.update_primary_scanout_output(output, &res.states); diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 0fe29b6be..a5655ff3f 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -382,10 +382,6 @@ impl Winit { &render_output_result, &pinnacle.loop_handle, ); - - pinnacle.loop_handle.insert_idle(|state| { - state.process_capture_sessions(); - }); } let now = pinnacle.clock.now(); diff --git a/src/handlers/image_copy_capture.rs b/src/handlers/image_copy_capture.rs index d2fda6ece..72582c53e 100644 --- a/src/handlers/image_copy_capture.rs +++ b/src/handlers/image_copy_capture.rs @@ -46,11 +46,11 @@ impl ImageCopyCaptureHandler for State { } fn new_session(&mut self, session: Session) { - // FIXME: better tracking of no cursor vs no output/window or something - let (buffer_size, scale) = self - .pinnacle - .buffer_size_and_scale_for_session(&session) - .unwrap_or(((1, 1).into(), 1.0)); + 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); @@ -69,6 +69,7 @@ impl ImageCopyCaptureHandler for State { .pinnacle .window_for_foreign_toplevel_handle(&ext_foreign_toplevel_handle_v1) else { + session.stopped(); return; }; @@ -130,6 +131,7 @@ impl ImageCopyCaptureHandler for State { 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]; @@ -152,15 +154,19 @@ impl 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 { - // TODO: stop stream or something idk + session.stopped(); continue; }; @@ -275,8 +281,6 @@ impl State { .unwrap(), }; - // TODO: handle cursor session frames - self.handle_frame(frame, &elements, trackers); } win.with_state_mut(|state| state.capture_sessions = sessions); @@ -288,7 +292,7 @@ impl State { for (session, trackers) in sessions.iter_mut() { let Some((size, scale)) = self.pinnacle.buffer_size_and_scale_for_session(session) else { - // TODO: stop stream or something idk + session.stopped(); continue; }; @@ -393,7 +397,8 @@ impl State { } } - pub fn update_cursor_capture_positions(&mut self) { + /// 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(); @@ -497,12 +502,20 @@ impl State { } } + /// 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"); @@ -530,13 +543,6 @@ impl State { panic!("captured frame that doesn't have a shm or dma buffer"); }; - let (damage, _) = trackers.damage.damage_output(1, elements).unwrap(); - let damage = damage.cloned().unwrap_or_default(); - if damage.is_empty() { - frame.submit(Transform::Normal, []); - return; - } - let elements = client_damage .iter() .map(DynElement::new) @@ -583,7 +589,7 @@ impl State { frame.submit( Transform::Normal, - damage.into_iter().map(|rect| { + damage.iter().map(|rect| { Rectangle::new( (rect.loc.x, rect.loc.y).into(), (rect.size.w, rect.size.h).into(), @@ -595,6 +601,9 @@ impl State { } 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, @@ -605,7 +614,10 @@ impl Pinnacle { let scale = output.current_scale().fractional_scale(); if matches!(session.cursor(), Cursor::Standalone { .. }) { - let geo = self.cursor_state.cursor_geometry(self.clock.now(), scale)?; + 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; @@ -625,7 +637,8 @@ impl Pinnacle { if matches!(session.cursor(), Cursor::Standalone { .. }) { let geo = self .cursor_state - .cursor_geometry(self.clock.now(), fractional_scale)?; + .cursor_geometry(self.clock.now(), fractional_scale) + .unwrap_or(Rectangle::from_size((1, 1).into())); Some((geo.size, fractional_scale)) } else { let size = (*window) @@ -642,26 +655,38 @@ impl Pinnacle { } } +/// 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 { - pub fn new(size: Size, scale: f64) -> Self { + /// 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), } } - pub fn size(&self) -> Size { + /// 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() } - pub fn scale(&self) -> f64 { + /// 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/protocol/image_copy_capture.rs b/src/protocol/image_copy_capture.rs index ff88d8a85..c51662ec8 100644 --- a/src/protocol/image_copy_capture.rs +++ b/src/protocol/image_copy_capture.rs @@ -30,7 +30,6 @@ const VERSION: u32 = 1; #[derive(Debug)] pub struct ImageCopyCaptureState { sessions: Vec, - cursor_sessions: Vec, shm_formats: Vec, dmabuf_formats: HashMap>, dmabuf_device: Option, @@ -53,7 +52,6 @@ impl ImageCopyCaptureState { Self { sessions: Vec::new(), - cursor_sessions: Vec::new(), shm_formats: Vec::new(), dmabuf_formats: HashMap::new(), dmabuf_device: None, @@ -199,11 +197,6 @@ where let session = CursorSession::new(session); state.new_cursor_session(session.clone()); - - state - .image_copy_capture_state() - .cursor_sessions - .push(session); } ext_image_copy_capture_manager_v1::Request::Destroy => (), _ => (), diff --git a/src/protocol/image_copy_capture/frame.rs b/src/protocol/image_copy_capture/frame.rs index 4a942ec98..e7cd20a34 100644 --- a/src/protocol/image_copy_capture/frame.rs +++ b/src/protocol/image_copy_capture/frame.rs @@ -43,6 +43,9 @@ impl Drop for Frame { 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(); } } @@ -115,7 +118,7 @@ impl Frame { #[derive(Debug, Default)] pub struct FrameData { pub(super) frame_state: FrameState, - pub(super) client_buffer_damage: Vec>, + client_buffer_damage: Vec>, pub(super) buffer: Option, } diff --git a/src/protocol/image_copy_capture/session.rs b/src/protocol/image_copy_capture/session.rs index 2c2d1a40d..012ca6b21 100644 --- a/src/protocol/image_copy_capture/session.rs +++ b/src/protocol/image_copy_capture/session.rs @@ -139,20 +139,12 @@ impl Session { /// /// 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 - .data() - .cursor_session - .as_ref() - .is_some_and(|cursor_session| { - !cursor_session - .data::() - .unwrap() - .cursor_entered - .load(Ordering::Relaxed) - }) - { + 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; @@ -182,6 +174,7 @@ impl Session { .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) } @@ -258,6 +251,7 @@ impl CursorSession { &self.data().pointer } + /// Sets this cursor session's hotspot. pub fn set_hotspot(&self, hotspot: Point) { if self.data().hotspot() == hotspot { return; @@ -270,6 +264,9 @@ impl CursorSession { } } + /// 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() { @@ -447,13 +444,19 @@ where resource: &ExtImageCopyCaptureCursorSessionV1, _data: &CursorSessionData, ) { - state - .image_copy_capture_state() - .cursor_sessions - .retain(|session| session.session != *resource); - 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/state.rs b/src/state.rs index 8c036dc80..0ff9c7f62 100644 --- a/src/state.rs +++ b/src/state.rs @@ -267,7 +267,7 @@ impl State { foreign_toplevel::refresh(self); ext_workspace::refresh(self); self.pinnacle.refresh_idle_inhibit(); - self.update_cursor_capture_positions(); + self.process_capture_sessions(); self.backend.render_scheduled_outputs(&mut self.pinnacle); diff --git a/src/window.rs b/src/window.rs index 3c65344de..9a6e8124a 100644 --- a/src/window.rs +++ b/src/window.rs @@ -766,7 +766,7 @@ impl State { pub fn map_new_window(&mut self, unmapped: Unmapped) { let _span = tracy_client::span!("State::map_new_window"); - let (window, attempt_float_on_map, focus) = if true { + let (window, attempt_float_on_map, focus) = if cfg!(feature = "wlcs") { // bruh // Relax the requirement that the window should've been configured first // for wlcs From ed7b04ddb0b8ddf79db047786963686f74682869 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Thu, 9 Oct 2025 22:37:13 -0500 Subject: [PATCH 13/14] Fix decoration offsets when fullscreen/CSD --- src/handlers.rs | 2 +- src/handlers/image_copy_capture.rs | 11 +++-------- src/layout.rs | 7 ++----- src/render.rs | 5 ++--- src/window.rs | 4 ++-- src/window/window_state.rs | 10 +++++++++- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/handlers.rs b/src/handlers.rs index 27742e9fa..2ba1b0a9e 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -1030,7 +1030,7 @@ impl Pinnacle { let deco_offset = self .window_for_surface(&root) - .map(|win| win.with_state(|state| state.total_decoration_offset())) + .map(|win| win.total_decoration_offset()) .unwrap_or_default(); if parent == root { diff --git a/src/handlers/image_copy_capture.rs b/src/handlers/image_copy_capture.rs index 72582c53e..c2240f0b3 100644 --- a/src/handlers/image_copy_capture.rs +++ b/src/handlers/image_copy_capture.rs @@ -224,9 +224,7 @@ impl State { let pointer_loc = self.pinnacle.seat.get_pointer().unwrap().current_location() - win_loc.to_f64() - - win - .with_state(|state| state.total_decoration_offset()) - .to_f64(); + - win.total_decoration_offset().to_f64(); let (pointer_elements, _) = pointer_render_elements( pointer_loc.to_physical_precise_round(scale) @@ -464,11 +462,8 @@ impl State { }) .unwrap_or(1.0); - let cursor_loc = cursor_loc - - window_loc.to_f64() - - window - .with_state(|state| state.total_decoration_offset()) - .to_f64(); + 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); diff --git a/src/layout.rs b/src/layout.rs index 00baa944d..4766b745d 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -419,11 +419,8 @@ impl State { // FIXME: Don't do this here // `loc` includes bounds but we need to configure the x11 surface // with its actual location - if !window.should_not_have_ssd() { - let deco_offset = - window.with_state(|state| state.total_decoration_offset()); - configure_loc += deco_offset; - } + configure_loc += window.total_decoration_offset(); + let _ = surface.configure(Rectangle::new(configure_loc, surface.geometry().size)); } diff --git a/src/render.rs b/src/render.rs index a64ad09b4..c17fcb2f0 100644 --- a/src/render.rs +++ b/src/render.rs @@ -152,7 +152,7 @@ impl WindowElement { let _span = tracy_client::span!("WindowElement::render_elements"); let total_deco_offset = if include_decorations { - self.with_state(|state| state.total_decoration_offset()) + self.total_decoration_offset() } else { Default::default() }; @@ -268,9 +268,8 @@ impl WindowElement { 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 total_deco_offset = state.total_decoration_offset(); - let mut surfaces = state.decoration_surfaces.iter().collect::>(); surfaces.sort_by_key(|deco| deco.z_index()); let mut surfaces = surfaces.into_iter().rev().peekable(); diff --git a/src/window.rs b/src/window.rs index 9a6e8124a..cb34714e1 100644 --- a/src/window.rs +++ b/src/window.rs @@ -264,7 +264,7 @@ impl WindowElement { // Popups are located relative to the actual window, // so offset by the decoration offset. - let total_deco_offset = self.with_state(|state| state.total_decoration_offset()); + let total_deco_offset = self.total_decoration_offset(); // Check for popups. if let Some(surface) = self.wl_surface() @@ -392,7 +392,7 @@ impl SpaceElement for WindowElement { decos.sort_by_key(|deco| deco.z_index()); let mut decos = decos.into_iter().rev().peekable(); - let deco_offset = self.with_state(|state| state.total_decoration_offset()); + let deco_offset = self.total_decoration_offset(); decos .peeking_take_while(|deco| deco.z_index() >= 0) diff --git a/src/window/window_state.rs b/src/window/window_state.rs index 877ee231f..e39fb5132 100644 --- a/src/window/window_state.rs +++ b/src/window/window_state.rs @@ -547,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 { @@ -712,7 +720,7 @@ impl WindowElementState { max_bounds } - pub fn total_decoration_offset(&self) -> Point { + fn total_decoration_offset(&self) -> Point { Point::new( self.max_decoration_bounds().left as i32, self.max_decoration_bounds().top as i32, From 73902d267fe8aa7bd82a65f172689f680b7df1b4 Mon Sep 17 00:00:00 2001 From: Ottatop Date: Thu, 9 Oct 2025 23:10:05 -0500 Subject: [PATCH 14/14] Fix window capture session buffer sizes --- src/handlers/image_copy_capture.rs | 8 ++++---- src/render.rs | 2 +- src/window.rs | 7 ++++++- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/handlers/image_copy_capture.rs b/src/handlers/image_copy_capture.rs index c2240f0b3..cbd857ff3 100644 --- a/src/handlers/image_copy_capture.rs +++ b/src/handlers/image_copy_capture.rs @@ -477,8 +477,8 @@ impl State { cursor_geo.loc += cursor_loc; - let buffer_size: Size = (**window) - .geometry() + let buffer_size: Size = window + .geometry_without_decorations() .size .to_f64() .to_physical_precise_round(fractional_scale); @@ -636,8 +636,8 @@ impl Pinnacle { .unwrap_or(Rectangle::from_size((1, 1).into())); Some((geo.size, fractional_scale)) } else { - let size = (*window) - .geometry() + let size = window + .geometry_without_decorations() .size .to_f64() .to_buffer(fractional_scale, Transform::Normal) diff --git a/src/render.rs b/src/render.rs index c17fcb2f0..882d8a877 100644 --- a/src/render.rs +++ b/src/render.rs @@ -160,7 +160,7 @@ impl WindowElement { let window_location = if include_decorations { self.geometry().loc } else { - (**self).geometry().loc + self.geometry_without_decorations().loc }; let window_location = (location - window_location).to_physical_precise_round(scale); diff --git a/src/window.rs b/src/window.rs index cb34714e1..98a5fa42e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -231,7 +231,7 @@ 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 { let mut geometry = self.0.geometry(); @@ -249,6 +249,11 @@ impl WindowElement { geometry } + /// Gets this window's geometry ignoring decoration bounds. + pub fn geometry_without_decorations(&self) -> Rectangle { + self.0.geometry() + } + /// Returns the surface under the given point relative to /// (0, 0) of this window's root wl surface. pub fn surface_under>>(