// SPDX-License-Identifier: GPL-3.0-only use calloop::LoopHandle; use smithay::{ backend::{ allocator::{Buffer, Fourcc, format::get_transparent}, renderer::{ BufferType, Color32F, ExportMem, ImportAll, ImportMem, Offscreen, Renderer, buffer_dimensions, buffer_type, damage::{Error as DTError, OutputDamageTracker, RenderOutputResult}, element::{ RenderElement, utils::{Relocate, RelocateRenderElement}, }, gles::{GlesError, GlesRenderbuffer}, sync::SyncPoint, utils::with_renderer_surface_state, }, }, desktop::space::SpaceElement, input::Seat, output::{Output, OutputNoMode}, reexports::wayland_server::protocol::{wl_buffer::WlBuffer, wl_shm::Format as ShmFormat}, utils::{ Buffer as BufferCoords, IsAlive, Logical, Physical, Point, Rectangle, Scale, Size, Transform, }, wayland::{ dmabuf::get_dmabuf, image_copy_capture::{ BufferConstraints, CaptureFailureReason, CursorSessionRef, Frame, SessionRef, }, seat::WaylandFocus, shm::{shm_format_to_fourcc, with_buffer_contents, with_buffer_contents_mut}, }, }; use std::time::{Duration, SystemTime, UNIX_EPOCH}; use tracing::warn; use crate::{ backend::render::{ CursorMode, ElementFilter, RendererRef, cursor::{self, CursorRenderElement}, element::{AsGlowRenderer, CosmicElement, DamageElement}, render_workspace, wayland::SurfaceRenderElement, }, shell::{CosmicMappedRenderElement, CosmicSurface, WorkspaceRenderElement}, state::{Common, KmsNodes, State}, utils::prelude::{PointExt, PointGlobalExt, RectExt, RectLocalExt, SeatExt}, wayland::{ handlers::image_copy_capture::{ SessionData, SessionUserData, constraints_for_output, constraints_for_toplevel, }, protocols::workspace::WorkspaceHandle, }, }; use super::{super::data_device::get_dnd_icon, user_data::SessionHolder}; pub struct PendingImageCopyData { pub frame: Frame, pub damage: Vec>, pub sync: SyncPoint, } impl PendingImageCopyData { /// Send `success` to image copy frame, once sync point is reached pub fn send_success_when_ready( self, transform: Transform, loop_handle: &LoopHandle<'static, LoopData>, presented: impl Into, ) { let presented = presented.into(); if self.sync.is_reached() { self.frame.success(transform, self.damage, presented); } else if let Some(fence_fd) = self.sync.export() { let source = calloop::generic::Generic::new( fence_fd, calloop::Interest::READ, calloop::Mode::OneShot, ); let mut data = Some(self); loop_handle .insert_source(source, move |_, _, _| { let data = data.take().unwrap(); data.frame.success(transform, data.damage, presented); Ok(calloop::PostAction::Remove) }) .expect("Failed to wait on sync point"); } else { // Should be able to export fence; but otherwise wait let _ = self.sync.wait(); self.frame.success(transform, self.damage, presented); } } } pub fn submit_buffer( frame: Frame, renderer: &mut R, offscreen: Option<&mut R::Framebuffer<'_>>, transform: Transform, damage: Option<&[Rectangle]>, mut sync: SyncPoint, ) -> Result, R::Error> where R: ExportMem + AsGlowRenderer, { let Some(damage) = damage else { frame.success( transform, None, SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or(Duration::ZERO), ); return Ok(None); }; let buffer = frame.buffer(); let buffer_size = buffer_dimensions(&buffer).unwrap(); if let Some(fb) = offscreen { assert!(matches!(buffer_type(&buffer), Some(BufferType::Shm))); if let Err(err) = with_buffer_contents_mut(&buffer, |ptr, len, data| { let offset = data.offset; let width = data.width; let height = data.height; let stride = data.stride; let format = shm_format_to_fourcc(data.format) .expect("We should be able to convert all hardcoded shm screencopy formats"); // number of bytes per pixel // TODO: compute from data.format let pixelsize = 4i32; // ensure consistency, the SHM handler of smithay should ensure this assert!((offset + (height - 1) * stride + width * pixelsize) as usize <= len); // ensure rendering is done renderer.wait(&sync)?; let format = get_transparent(format).unwrap_or(format); let mapping = renderer.copy_framebuffer(fb, Rectangle::from_size(buffer_size), format)?; let gl_data = renderer.map_texture(&mapping)?; assert!((width * height * pixelsize) as usize <= gl_data.len()); for i in 0..height { unsafe { std::ptr::copy_nonoverlapping::( gl_data.as_ptr().offset((width * pixelsize * i) as isize), ptr.offset((offset + stride * i) as isize), (width * pixelsize) as usize, ); } } // We've already waited on the sync point and copied from the texture // TODO: Don't block in this function, and defer copy until ready? sync = SyncPoint::signaled(); Ok(()) }) .map_err(|err| R::from_gles_error(GlesError::BufferAccessError(err))) .and_then(|x| x) { frame.fail(CaptureFailureReason::Unknown); return Err(err); } } Ok(Some(PendingImageCopyData { frame, damage: damage .iter() .map(|rect| { let logical = rect.to_logical(1); logical.to_buffer(1, transform.invert(), &buffer_size.to_logical(1, transform)) }) .collect(), sync, })) } pub fn render_session( renderer: &mut R, session: &SessionData, frame: Frame, transform: Transform, render_fn: F, ) -> Result, DTError> where R: AsGlowRenderer, F: for<'d> FnOnce( &WlBuffer, &mut R, Option<&mut R::Framebuffer<'_>>, &'d mut OutputDamageTracker, usize, Vec>, ) -> Result, DTError>, { let mut session_user_data = session.lock().unwrap(); let buffer = frame.buffer(); let mut age = 1; if matches!(buffer_type(&buffer), Some(BufferType::Shm)) { let size = buffer_dimensions(&buffer).ok_or(DTError::OutputNoMode(OutputNoMode))?; let format = with_buffer_contents(&buffer, |_, _, data| { shm_format_to_fourcc(data.format) .expect("We should be able to convert all hardcoded shm screencopy formats") }) .map_err(|_| DTError::OutputNoMode(OutputNoMode))?; // Re-allocate if context id, size, or format are different session_user_data .offscreen .take_if(|(context_id, renderbuffer)| { renderer.glow_renderer().context_id() != *context_id || renderbuffer.size() != size || renderbuffer.format() != Some(format) }); if session_user_data.offscreen.is_none() { let renderbuffer = Offscreen::::create_buffer(renderer, format, size) .map_err(DTError::Rendering)?; session_user_data.offscreen = Some((renderer.glow_renderer().context_id(), renderbuffer)); // If we're allocating a new offscreen buffer, we need to re-render everything // (or copy the contexts of the shm buffer) age = 0; } } else { // If for some reason a capture session is used for shm, but then changes to dmabuf capture, // remove the offscreen buffer. session_user_data.offscreen = None; } let SessionUserData { dt, offscreen } = &mut *session_user_data; let mut fb = offscreen .as_mut() .map(|(_, tex)| renderer.bind(tex).map_err(DTError::Rendering)) .transpose()?; let res = render_fn( &frame.buffer(), renderer, fb.as_mut(), dt, age, frame.damage(), ); match res { Ok(result) => submit_buffer( frame, renderer, fb.as_mut(), transform, result.damage.map(|x| x.as_slice()), result.sync, ) .map_err(DTError::Rendering), Err(err) => { frame.fail(CaptureFailureReason::Unknown); Err(err) } } } pub fn render_workspace_to_buffer( state: &mut State, session: &SessionRef, frame: Frame, handle: WorkspaceHandle, ) { let shell = state.common.shell.read(); let Some(workspace) = shell.workspaces.space_for_handle(&handle) else { return; }; let mut output = workspace.output().clone(); let idx = shell.workspaces.idx_for_handle(&output, &handle).unwrap(); std::mem::drop(shell); let mode = output .current_mode() .map(|mode| mode.size.to_logical(1).to_buffer(1, Transform::Normal)); let buffer = frame.buffer(); let buffer_size = buffer_dimensions(&buffer).unwrap(); if mode != Some(buffer_size) { let Some(constraints) = constraints_for_output(&output, &mut state.backend) else { output.remove_session(session); return; }; session.update_constraints(constraints); if let Some(data) = session.user_data().get::() { *data.lock().unwrap() = SessionUserData::new(OutputDamageTracker::from_output(&output)); } frame.fail(CaptureFailureReason::BufferConstraints); return; } fn render_fn<'d, R>( buffer: &WlBuffer, renderer: &mut R, offscreen: Option<&mut R::Framebuffer<'_>>, dt: &'d mut OutputDamageTracker, age: usize, additional_damage: Vec>, draw_cursor: bool, common: &mut Common, output: &Output, handle: (WorkspaceHandle, usize), ) -> Result, DTError> where R: AsGlowRenderer, R::TextureId: Send + Clone + 'static, CosmicElement: RenderElement, CosmicMappedRenderElement: RenderElement, WorkspaceRenderElement: RenderElement, { let cursor_mode = if draw_cursor { CursorMode::All } else { CursorMode::None }; let area = output .current_mode() .ok_or(DTError::OutputNoMode(OutputNoMode)) .map( |mode| { mode.size .to_logical(1) .to_buffer(1, Transform::Normal) .to_f64() }, /* TODO: Mode is Buffer..., why is this Physical in the first place */ )?; let additional_damage = (!additional_damage.is_empty()).then(|| { additional_damage .into_iter() .map(|rect| { rect.to_f64() .to_logical( output.current_scale().fractional_scale(), output.current_transform(), &area, ) .to_i32_round() }) .collect() }); if let Ok(dmabuf) = get_dmabuf(buffer) { let mut dmabuf = dmabuf.clone(); let mut fb = renderer.bind(&mut dmabuf).map_err(DTError::Rendering)?; render_workspace( None, renderer, &mut fb, dt, age, additional_damage, &common.shell, None, common.clock.now(), output, None, handle, cursor_mode, ElementFilter::ExcludeWorkspaceOverview, ) .map(|res| res.0) } else { let target = offscreen.expect("shm buffers should have an offscreen target"); render_workspace( None, renderer, target, dt, age, additional_damage, &common.shell, None, common.clock.now(), output, None, handle, cursor_mode, ElementFilter::ExcludeWorkspaceOverview, ) .map(|res| res.0) } } let draw_cursor = session.draw_cursor(); let transform = output.current_transform(); let common = &mut state.common; let renderer = match state.backend.offscreen_renderer(|kms| { let render_node = kms .target_node_for_output(&output) .or(*kms.primary_node.read().unwrap())?; let target_node = get_dmabuf(&buffer) .ok() .and_then(|dma| dma.node()) .unwrap_or(render_node); let buffer_format = match buffer_type(&buffer) { Some(BufferType::Dma) => Some(get_dmabuf(&buffer).unwrap().format().code), Some(BufferType::Shm) => { with_buffer_contents(&buffer, |_, _, data| shm_format_to_fourcc(data.format)) .unwrap() } _ => None, }; Some(KmsNodes { render_node, target_node, copy_format: buffer_format.unwrap_or(Fourcc::Abgr8888), }) }) { Ok(renderer) => renderer, Err(err) => { warn!(?err, "Couldn't use node for screencopy"); frame.fail(CaptureFailureReason::Unknown); return; } }; let result = match renderer { RendererRef::Glow(renderer) => { match render_session( renderer, session.user_data().get::().unwrap(), frame, transform, |buffer, renderer, offscreen, dt, age, additional_damage| { render_fn( buffer, renderer, offscreen, dt, age, additional_damage, draw_cursor, common, &output, (handle, idx), ) }, ) { Ok(frame) => frame, Err(err) => { tracing::warn!(?err, "Failed to render to screencopy buffer"); None } } } RendererRef::GlMulti(mut renderer) => { match render_session( &mut renderer, session.user_data().get::().unwrap(), frame, transform, |buffer, renderer, offscreen, dt, age, additional_damage| { render_fn( buffer, renderer, offscreen, dt, age, additional_damage, draw_cursor, common, &output, (handle, idx), ) }, ) { Ok(frame) => frame, Err(err) => { tracing::warn!(?err, "Failed to render to screencopy buffer"); None } } } }; if let Some(pending_image_copy_data) = result { pending_image_copy_data.send_success_when_ready( transform, &common.event_loop_handle, common.clock.now(), ); } } smithay::render_elements! { pub WindowCaptureElement where R: ImportAll + ImportMem + AsGlowRenderer; WaylandElement=SurfaceRenderElement, CursorElement=RelocateRenderElement>, DamageElement=DamageElement, } pub fn render_window_to_buffer( state: &mut State, session: &SessionRef, frame: Frame, toplevel: &CosmicSurface, ) { if !toplevel.alive() { toplevel.clone().remove_session(session); return; } let buffer = frame.buffer(); let geometry = toplevel.geometry(); let buffer_size = buffer_dimensions(&buffer).unwrap(); if buffer_size != geometry.size.to_buffer(1, Transform::Normal) { let Some(constraints) = constraints_for_toplevel(toplevel, &mut state.backend) else { toplevel.clone().remove_session(session); return; }; session.update_constraints(constraints); if let Some(data) = session.user_data().get::() { let size = geometry.size.to_physical(1); *data.lock().unwrap() = SessionUserData::new(OutputDamageTracker::new(size, 1.0, Transform::Normal)); } frame.fail(CaptureFailureReason::BufferConstraints); return; } fn render_fn<'d, R>( buffer: &WlBuffer, renderer: &mut R, offscreen: Option<&mut R::Framebuffer<'_>>, dt: &'d mut OutputDamageTracker, age: usize, additional_damage: Vec>, draw_cursor: bool, common: &mut Common, toplevel: &CosmicSurface, geometry: Rectangle, ) -> Result, DTError> where R: AsGlowRenderer, R::TextureId: Send + Clone + 'static, CosmicElement: RenderElement, CosmicMappedRenderElement: RenderElement, { let mut elements: Vec<_> = additional_damage .into_iter() .filter_map(|rect| { let logical_rect = rect.to_logical( 1, Transform::Normal, &geometry.size.to_buffer(1, Transform::Normal), ); logical_rect.intersection(Rectangle::from_size(geometry.size)) }) .map(DamageElement::new) .map(WindowCaptureElement::::from) .collect(); let shell = common.shell.read(); let blur_strength = 9; // TODO let seat = shell.seats.last_active().clone(); let pointer = seat.get_pointer().unwrap(); let pointer_loc = pointer.current_location().to_i32_round().as_global(); let mut location = None; if let Some(element) = shell.element_for_surface(toplevel) && element.has_active_window(toplevel) && let Some(workspace) = shell.space_for(element) && let Some(geometry) = workspace.element_geometry(element) { let mut surface_geo = element.active_window_geometry().as_local(); surface_geo.loc += geometry.loc; let global_geo = surface_geo.to_global(workspace.output()); if global_geo.contains(pointer_loc) { location = Some((pointer_loc - global_geo.loc).as_logical().to_f64()); } }; std::mem::drop(shell); if let Some(location) = location { if draw_cursor { cursor::draw_cursor( renderer, &seat, location, 1.0.into(), 1.0, common.clock.now(), blur_strength, true, &mut |elem, hotspot| { elements.push(WindowCaptureElement::CursorElement( RelocateRenderElement::from_element( elem, Point::from((-hotspot.x, -hotspot.y)), Relocate::Relative, ), )); }, ); } // TODO cosmic-workspaces wants to omit, but metadata cursor capture in portal should // still include dnd surface in window capture buffer? if draw_cursor && let Some(dnd_icon) = get_dnd_icon(&seat) { cursor::draw_dnd_icon( renderer, &dnd_icon.surface, (location + dnd_icon.offset.to_f64()).to_i32_round(), 1.0, blur_strength, &mut |elem| { elements.push( RelocateRenderElement::from_element( CursorRenderElement::Surface(elem), Point::new(0, 0), Relocate::Relative, ) .into(), ) }, ); } } toplevel.push_render_elements( renderer, (-geometry.loc.x, -geometry.loc.y).into(), Scale::from(1.0), 1.0, None, None, false, [0; 4], blur_strength, &mut |elem| elements.push(elem.into()), None, ); if let Ok(dmabuf) = get_dmabuf(buffer) { let mut dmabuf_clone = dmabuf.clone(); let mut fb = renderer .bind(&mut dmabuf_clone) .map_err(DTError::Rendering)?; dt.render_output(renderer, &mut fb, age, &elements, Color32F::TRANSPARENT) } else { let fb = offscreen.expect("shm buffer should have an offscreen target"); dt.render_output(renderer, fb, age, &elements, Color32F::TRANSPARENT) } } let common = &mut state.common; let draw_cursor = session.draw_cursor(); let renderer = match state.backend.offscreen_renderer(|kms| { get_dmabuf(&buffer) .ok() .and_then(|dmabuf| dmabuf.node()) .or_else(|| { toplevel .wl_surface() .and_then(|wl_surface| { with_renderer_surface_state(&wl_surface, |state| { let buffer = state.buffer()?; let dmabuf = get_dmabuf(buffer).ok()?; dmabuf.node() }) }) .flatten() }) .or(*kms.primary_node.read().unwrap()) }) { Ok(renderer) => renderer, Err(err) => { warn!(?err, "Couldn't use node for screencopy"); frame.fail(CaptureFailureReason::Unknown); return; } }; let result = match renderer { RendererRef::Glow(renderer) => match render_session( renderer, session.user_data().get::().unwrap(), frame, Transform::Normal, |buffer, renderer, offscreen, dt, age, additional_damage| { render_fn( buffer, renderer, offscreen, dt, age, additional_damage, draw_cursor, common, toplevel, geometry, ) }, ) { Ok(frame) => frame, Err(err) => { tracing::warn!(?err, "Failed to render to screencopy buffer"); None } }, RendererRef::GlMulti(mut renderer) => match render_session( &mut renderer, session.user_data().get::().unwrap(), frame, Transform::Normal, |buffer, renderer, offscreen, dt, age, additional_damage| { render_fn( buffer, renderer, offscreen, dt, age, additional_damage, draw_cursor, common, toplevel, geometry, ) }, ) { Ok(frame) => frame, Err(err) => { tracing::warn!(?err, "Failed to render to screencopy buffer"); None } }, }; if let Some(pending_image_copy_data) = result { pending_image_copy_data.send_success_when_ready( Transform::Normal, &common.event_loop_handle, common.clock.now(), ); } } pub fn render_cursor_to_buffer( state: &mut State, session: &CursorSessionRef, frame: Frame, seat: &Seat, ) { let buffer = frame.buffer(); let mut cursor_size = seat .cursor_geometry((0.0, 0.0), state.common.clock.now()) .map(|(geo, _hotspot)| geo.size) .unwrap_or_else(|| Size::from((64, 64))); let buffer_size = buffer_dimensions(&buffer).unwrap(); // Client shouldn't try to allocate 0x0 buffer if cursor_size == Size::new(0, 0) { cursor_size = Size::new(1, 1); } if buffer_size != cursor_size { let constraints = BufferConstraints { size: cursor_size, shm: vec![ShmFormat::Argb8888], dma: None, }; session.update_constraints(constraints); if let Some(data) = session.user_data().get::() { *data.lock().unwrap() = SessionUserData::new(OutputDamageTracker::new( cursor_size.to_logical(1, Transform::Normal).to_physical(1), 1.0, Transform::Normal, )); } frame.fail(CaptureFailureReason::BufferConstraints); return; } fn render_fn<'d, R>( buffer: &WlBuffer, renderer: &mut R, offscreen: Option<&mut R::Framebuffer<'_>>, dt: &'d mut OutputDamageTracker, age: usize, additional_damage: Vec>, common: &mut Common, seat: &Seat, ) -> Result, DTError> where R: AsGlowRenderer, R::TextureId: Send + Clone + 'static, CosmicElement: RenderElement, CosmicMappedRenderElement: RenderElement, { let mut elements: Vec<_> = additional_damage .into_iter() .filter_map(|rect| { let logical_rect = rect.to_logical(1, Transform::Normal, &Size::from((64, 64))); logical_rect.intersection(Rectangle::from_size((64, 64).into())) }) .map(DamageElement::new) .map(WindowCaptureElement::from) .collect(); cursor::draw_cursor( renderer, seat, Point::from((0.0, 0.0)), 1.0.into(), 1.0, common.clock.now(), 0, true, &mut |elem, _| { elements.push( RelocateRenderElement::from_element(elem, (0, 0), Relocate::Relative).into(), ) }, ); if let Ok(dmabuf) = get_dmabuf(buffer) { let mut dmabuf_clone = dmabuf.clone(); let mut fb = renderer .bind(&mut dmabuf_clone) .map_err(DTError::Rendering)?; dt.render_output(renderer, &mut fb, age, &elements, [0.0, 0.0, 0.0, 0.0]) } else { let fb = offscreen.expect("shm buffers should have offscreen target"); dt.render_output(renderer, fb, age, &elements, [0.0, 0.0, 0.0, 0.0]) } } let common = &mut state.common; let renderer = match state .backend .offscreen_renderer(|kms| *kms.primary_node.read().unwrap()) { Ok(renderer) => renderer, Err(err) => { warn!(?err, "Couldn't use node for screencopy"); frame.fail(CaptureFailureReason::Unknown); return; } }; let result = match renderer { RendererRef::Glow(renderer) => { match render_session( renderer, session.user_data().get::().unwrap(), frame, Transform::Normal, |buffer, renderer, offscreen, dt, age, additional_damage| { render_fn( buffer, renderer, offscreen, dt, age, additional_damage, common, seat, ) }, ) { Ok(frame) => frame, Err(err) => { tracing::warn!(?err, "Failed to render to screencopy buffer"); None } } } RendererRef::GlMulti(mut renderer) => { match render_session( &mut renderer, session.user_data().get::().unwrap(), frame, Transform::Normal, |buffer, renderer, offscreen, dt, age, additional_damage| { render_fn( buffer, renderer, offscreen, dt, age, additional_damage, common, seat, ) }, ) { Ok(frame) => frame, Err(err) => { tracing::warn!(?err, "Failed to render to screencopy buffer"); None } } } }; if let Some(pending_image_copy_data) = result { pending_image_copy_data.send_success_when_ready( Transform::Normal, &common.event_loop_handle, common.clock.now(), ); } }