From c46ab4f1e62a3c6f4957a86c30289964cbb7734a Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Thu, 9 Feb 2023 14:00:22 -0800 Subject: [PATCH] Recapture workspace/toplevel on damage; start code for filtering For some reason using calloop makes output events not consistently occur? --- Cargo.lock | 5 +- Cargo.toml | 1 + src/main.rs | 10 +- src/wayland.rs | 293 ++++++++++++++++++++++++++++++++++++++----------- 4 files changed, 242 insertions(+), 67 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c67cf0..99ba224 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -397,7 +397,7 @@ dependencies = [ [[package]] name = "cosmic-client-toolkit" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-protocols#1ebab50966cfa91afc988f6d8cf83307cbe30446" +source = "git+https://github.com/pop-os/cosmic-protocols#7eac0d9e15b9dc4269ceb1ab7dd027b8589c601d" dependencies = [ "cosmic-protocols", "gl_generator", @@ -409,7 +409,7 @@ dependencies = [ [[package]] name = "cosmic-protocols" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-protocols#1ebab50966cfa91afc988f6d8cf83307cbe30446" +source = "git+https://github.com/pop-os/cosmic-protocols#7eac0d9e15b9dc4269ceb1ab7dd027b8589c601d" dependencies = [ "bitflags", "wayland-backend", @@ -457,6 +457,7 @@ dependencies = [ name = "cosmic-workspaces" version = "0.1.0" dependencies = [ + "calloop", "cosmic-client-toolkit", "futures-channel", "libcosmic", diff --git a/Cargo.toml b/Cargo.toml index faf7dc6..a9eb7d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +calloop = "0.10.5" cctk = { package = "cosmic-client-toolkit", git = "https://github.com/pop-os/cosmic-protocols" } futures-channel = "0.3.25" libcosmic = { git = "https://github.com/pop-os/libcosmic", default-features = false, features = ["tokio", "wayland"] } diff --git a/src/main.rs b/src/main.rs index bceee5e..e38fa09 100644 --- a/src/main.rs +++ b/src/main.rs @@ -93,6 +93,7 @@ struct App { toplevel_manager: Option, seats: Vec, visible: bool, + wayland_cmd_sender: Option>, } impl App { @@ -140,7 +141,7 @@ impl App { get_layer_surface(SctkLayerSurfaceSettings { id, keyboard_interactivity: KeyboardInteractivity::Exclusive, - namespace: "workspaces".into(), + namespace: "workspace-overview".into(), layer: Layer::Overlay, size: Some((Some(width as _), Some(height as _))), output: IcedOutput::Output(output), @@ -267,6 +268,9 @@ impl Application for App { wayland::Event::Connection(conn) => { self.conn = Some(conn); } + wayland::Event::CmdSender(sender) => { + self.wayland_cmd_sender = Some(sender); + } wayland::Event::ToplevelManager(manager) => { self.toplevel_manager = Some(manager); } @@ -437,7 +441,7 @@ fn workspace_sidebar_entry(workspace: &Workspace) -> cosmic::Element { widget::column![ close_button(Msg::CloseWorkspace(workspace.handle.clone())), widget::button(widget::Image::new(workspace.img.clone().unwrap_or_else( - || widget::image::Handle::from_pixels(0, 0, vec![0, 0, 0, 255]) + || widget::image::Handle::from_pixels(1, 1, vec![0, 0, 0, 255]) ))) .style(theme) .on_press(Msg::ActivateWorkspace(workspace.handle.clone())), @@ -464,7 +468,7 @@ fn toplevel_preview<'a>(toplevel: &'a Toplevel) -> cosmic::Element<'a, Msg> { widget::column![ close_button(Msg::CloseToplevel(toplevel.handle.clone())), widget::button(widget::Image::new(toplevel.img.clone().unwrap_or_else( - || widget::image::Handle::from_pixels(0, 0, vec![0, 0, 0, 255]), + || widget::image::Handle::from_pixels(1, 1, vec![0, 0, 0, 255]), ))) .on_press(Msg::ActivateToplevel(toplevel.handle.clone())), widget::text(&toplevel.info.title) diff --git a/src/wayland.rs b/src/wayland.rs index 472a2cf..79e712b 100644 --- a/src/wayland.rs +++ b/src/wayland.rs @@ -19,6 +19,8 @@ use cctk::{ }, sctk::{ self, + event_loop::WaylandSource, + globals::ProvidesBoundGlobal, output::{OutputHandler, OutputState}, registry::{ProvidesRegistryState, RegistryState}, seat::{SeatHandler, SeatState}, @@ -40,12 +42,21 @@ use cosmic::iced::{ widget::image, }; use futures_channel::mpsc; -use std::{collections::HashMap, sync::Mutex, thread}; +use std::{ + cell::RefCell, + collections::HashMap, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, + }, + thread, +}; // TODO define subscription for a particular output/workspace/toplevel (but we want to rate limit?) #[derive(Clone, Debug)] pub enum Event { + CmdSender(calloop::channel::Sender), Connection(Connection), ToplevelManager(zcosmic_toplevel_manager_v1::ZcosmicToplevelManagerV1), WorkspaceManager(zcosmic_workspace_manager_v1::ZcosmicWorkspaceManagerV1), @@ -71,19 +82,136 @@ pub fn subscription() -> iced::Subscription { iced::subscription::run("wayland-sub", async { start() }.flatten_stream()) } +#[derive(Clone, PartialEq, Eq)] enum CaptureSource { Toplevel(zcosmic_toplevel_handle_v1::ZcosmicToplevelHandleV1), - Workspace(zcosmic_workspace_handle_v1::ZcosmicWorkspaceHandleV1), + Workspace( + zcosmic_workspace_handle_v1::ZcosmicWorkspaceHandleV1, + wl_output::WlOutput, + ), } -struct Frame { - session_data: ScreencopySessionData, - buffer: Mutex>, +impl std::hash::Hash for CaptureSource { + fn hash(&self, state: &mut H) + where + H: std::hash::Hasher, + { + match self { + Self::Toplevel(handle) => handle.id(), + Self::Workspace(handle, output) => handle.id(), + } + .hash(state) + } +} + +#[derive(Clone, Debug, Default)] +pub struct CaptureFilter { + pub workspaces_on_outputs: Vec, + pub toplevels_on_workspaces: Vec, +} + +#[derive(Debug)] +pub enum Cmd { + CaptureFilter(CaptureFilter), +} + +struct Buffer { + pool: RawPool, + buffer: wl_buffer::WlBuffer, + buffer_info: BufferInfo, +} + +impl Buffer { + fn new( + buffer_info: BufferInfo, + shm: &impl ProvidesBoundGlobal, + qh: &QueueHandle, + ) -> Self { + // Assume format is already known to be valid + let mut pool = + RawPool::new((buffer_info.stride * buffer_info.height) as usize, shm).unwrap(); + let format = wl_shm::Format::try_from(buffer_info.format).unwrap(); + let buffer = pool.create_buffer( + 0, + buffer_info.width as i32, + buffer_info.height as i32, + buffer_info.stride as i32, + format, + (), + qh, + ); + Self { + pool, + buffer, + buffer_info, + } + } +} + +impl Drop for Buffer { + fn drop(&mut self) { + self.buffer.destroy(); + } +} + +struct Capture { + buffer: Mutex>, source: CaptureSource, - first_frame: bool, + first_frame: AtomicBool, + cancelled: AtomicBool, } -impl ScreencopySessionDataExt for Frame { +impl Capture { + fn new(source: CaptureSource) -> Capture { + Capture { + buffer: Mutex::new(None), + source, + first_frame: AtomicBool::new(true), + cancelled: AtomicBool::new(false), + } + } + + fn cancel(&self) { + self.cancelled.store(true, Ordering::SeqCst); + } + + fn capture( + self: &Arc, + manager: &zcosmic_screencopy_manager_v1::ZcosmicScreencopyManagerV1, + qh: &QueueHandle, + ) { + let udata = SessionData { + session_data: Default::default(), + capture: self.clone(), + }; + match &self.source { + CaptureSource::Toplevel(toplevel) => { + manager.capture_toplevel( + toplevel, + zcosmic_screencopy_manager_v1::CursorMode::Hidden, + &qh, + udata, + ); + } + CaptureSource::Workspace(workspace, output) => { + manager.capture_workspace( + workspace, + output, + zcosmic_screencopy_manager_v1::CursorMode::Hidden, + &qh, + udata, + ); + } + } + } +} + +struct SessionData { + session_data: ScreencopySessionData, + capture: Arc, +} + +impl ScreencopySessionDataExt for SessionData { fn screencopy_session_data(&self) -> &ScreencopySessionData { &self.session_data } @@ -102,12 +230,35 @@ pub struct AppData { sender: mpsc::Sender, output_names: HashMap>, seats: Vec, + capture_filter: CaptureFilter, + captures: RefCell>>, } impl AppData { fn send_event(&mut self, event: Event) { let _ = block_on(self.sender.send(event)); } + + // Handle message from main thread + fn handle_cmd(&mut self, cmd: Cmd) { + match cmd { + Cmd::CaptureFilter(filter) => { + self.capture_filter = filter; + self.invalidate_capture_filter(); + } + } + } + + fn invalidate_capture_filter(&mut self) { + // XXX drain filter + // TODO cancel captures if needed, enable capture + } + + fn add_capture_source(&self, source: CaptureSource) { + let capture = Arc::new(Capture::new(source.clone())); + capture.capture(&self.screencopy_state.screencopy_manager, &self.qh); + self.captures.borrow_mut().insert(source, capture); + } } impl ProvidesRegistryState for AppData { @@ -208,19 +359,7 @@ impl ToplevelInfoHandler for AppData { let info = self.toplevel_info_state.info(&toplevel).unwrap(); self.send_event(Event::NewToplevel(toplevel.clone(), info.clone())); - // XXX first_frame - let udata = Frame { - session_data: Default::default(), - buffer: Mutex::new(None), - source: CaptureSource::Toplevel(toplevel.clone()), - first_frame: true, - }; - let frame = self.screencopy_state.screencopy_manager.capture_toplevel( - toplevel, - zcosmic_screencopy_manager_v1::CursorMode::Hidden, - &self.qh, - udata, - ); + self.add_capture_source(CaptureSource::Toplevel(toplevel.clone())); } fn update_toplevel( @@ -256,20 +395,10 @@ impl WorkspaceHandler for AppData { let output_name = self.output_names.get(&output.id()).unwrap().clone(); workspaces.push((output_name, workspace.clone())); - // XXX first_frame - let udata = Frame { - session_data: Default::default(), - buffer: Mutex::new(None), - source: CaptureSource::Workspace(workspace.handle.clone()), - first_frame: true, - }; - let frame = self.screencopy_state.screencopy_manager.capture_workspace( - &workspace.handle, - output, - zcosmic_screencopy_manager_v1::CursorMode::Hidden, - &self.qh, - udata, - ); + self.add_capture_source(CaptureSource::Workspace( + workspace.handle.clone(), + output.clone(), + )); } } } @@ -290,6 +419,12 @@ impl ScreencopyHandler for AppData { session: &zcosmic_screencopy_session_v1::ZcosmicScreencopySessionV1, buffer_infos: &[BufferInfo], ) { + let capture = &session.data::().unwrap().capture; + if capture.cancelled.load(Ordering::SeqCst) { + session.destroy(); + return; + } + let buffer_info = buffer_infos .iter() .find(|x| { @@ -300,35 +435,28 @@ impl ScreencopyHandler for AppData { let buf_len = buffer_info.stride * buffer_info.height; // XXX fix in compositor - if buffer_info.width == 0 || buffer_info.height == 0 { + if buffer_info.width == 0 || buffer_info.height == 0 || buffer_info.stride == 0 { session.destroy(); return; } - // TODO: reuse pool? swapping? - let mut pool = RawPool::new(buf_len as usize, &self.shm_state).unwrap(); - let buffer = pool.create_buffer( - 0, - buffer_info.width as i32, - buffer_info.height as i32, - buffer_info.stride as i32, - wl_shm::Format::Abgr8888, - (), - qh, - ); + let mut buffer = capture.buffer.lock().unwrap(); + // Create new buffer if none, or different format + if !buffer + .as_ref() + .map_or(false, |x| &x.buffer_info == buffer_info) + { + *buffer = Some(Buffer::new(buffer_info.clone(), &self.shm_state, qh)); + } + let buffer = buffer.as_ref().unwrap(); - let frame = session.data::().unwrap(); - - session.attach_buffer(&buffer, None, 0); // XXX age? - if frame.first_frame { + session.attach_buffer(&buffer.buffer, None, 0); // XXX age? + if capture.first_frame.load(Ordering::SeqCst) { session.commit(zcosmic_screencopy_session_v1::Options::empty()); } else { session.commit(zcosmic_screencopy_session_v1::Options::OnDamage); - // TODO } conn.flush().unwrap(); - - *frame.buffer.lock().unwrap() = Some((pool, buffer, buffer_info.clone())); } fn ready( @@ -337,19 +465,32 @@ impl ScreencopyHandler for AppData { qh: &QueueHandle, session: &zcosmic_screencopy_session_v1::ZcosmicScreencopySessionV1, ) { - let frame = session.data::().unwrap(); - let (mut pool, buffer, buffer_info) = frame.buffer.lock().unwrap().take().unwrap(); + let capture = &session.data::().unwrap().capture; + if capture.cancelled.load(Ordering::SeqCst) { + session.destroy(); + return; + } + + let mut buffer = capture.buffer.lock().unwrap(); + let mut buffer = buffer.as_mut().unwrap(); // XXX is this at all a performance issue? - let image = - image::Handle::from_pixels(buffer_info.width, buffer_info.height, pool.mmap().to_vec()); - let event = match &frame.source { + let image = image::Handle::from_pixels( + buffer.buffer_info.width, + buffer.buffer_info.height, + buffer.pool.mmap().to_vec(), + ); + let event = match &capture.source { CaptureSource::Toplevel(toplevel) => Event::ToplevelCapture(toplevel.clone(), image), - CaptureSource::Workspace(workspace) => { + CaptureSource::Workspace(workspace, _) => { Event::WorkspaceCapture(workspace.clone(), image) } }; self.send_event(event); session.destroy(); + + // Capture again on damage + capture.first_frame.store(false, Ordering::SeqCst); + capture.capture(&self.screencopy_state.screencopy_manager, &self.qh); } fn failed( @@ -390,6 +531,10 @@ impl Dispatch for AppData { _: &Connection, _qh: &QueueHandle, ) { + match event { + wl_buffer::Event::Release => {} + _ => unreachable!(), + } } } @@ -415,6 +560,8 @@ fn start() -> mpsc::Receiver { sender, output_names: HashMap::new(), seats: Vec::new(), + capture_filter: CaptureFilter::default(), + captures: RefCell::new(HashMap::new()), }; app_data.send_event(Event::Connection(conn)); @@ -426,8 +573,30 @@ fn start() -> mpsc::Receiver { app_data.send_event(Event::WorkspaceManager(manager.clone())); } - thread::spawn(move || loop { - event_queue.blocking_dispatch(&mut app_data).unwrap(); + // XXX also monitor cmd sender? Use calloop? + thread::spawn(move || { + //event_queue.blocking_dispatch(&mut app_data).unwrap(); + + let (cmd_sender, cmd_channel) = calloop::channel::channel(); + app_data.send_event(Event::CmdSender(cmd_sender)); + + let mut event_loop = calloop::EventLoop::try_new().unwrap(); + WaylandSource::new(event_queue) + .unwrap() + .insert(event_loop.handle()) + .unwrap(); + event_loop + .handle() + .insert_source(cmd_channel, |event, _, app_data| { + if let calloop::channel::Event::Msg(msg) = event { + app_data.handle_cmd(msg) + } + }) + .unwrap(); + + loop { + event_loop.dispatch(None, &mut app_data).unwrap(); + } }); receiver @@ -440,4 +609,4 @@ sctk::delegate_shm!(AppData); cctk::delegate_toplevel_info!(AppData); cctk::delegate_toplevel_manager!(AppData); cctk::delegate_workspace!(AppData); -cctk::delegate_screencopy!(AppData, session: [Frame]); +cctk::delegate_screencopy!(AppData, session: [SessionData]);