From dc92d80b9f8de761df2aa0bfc90b61d53b9c0831 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Thu, 27 Jun 2024 11:36:24 +0400 Subject: Implement initial window screencasting --- src/dbus/mod.rs | 5 +- src/dbus/mutter_screen_cast.rs | 112 +++++++++-- src/handlers/compositor.rs | 9 + src/handlers/xdg_shell.rs | 6 + src/niri.rs | 430 +++++++++++++++++++++++++++++++++-------- src/pw_utils.rs | 262 +++++++++++++++++++++---- 6 files changed, 685 insertions(+), 139 deletions(-) (limited to 'src') diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index 179f10a7..eab72ce7 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -87,11 +87,8 @@ impl DBusServers { let (to_niri, from_screen_cast) = calloop::channel::channel(); niri.event_loop .insert_source(from_screen_cast, { - let to_niri = to_niri.clone(); move |event, _, state| match event { - calloop::channel::Event::Msg(msg) => { - state.on_screen_cast_msg(&to_niri, msg) - } + calloop::channel::Event::Msg(msg) => state.on_screen_cast_msg(msg), calloop::channel::Event::Closed => (), } }) diff --git a/src/dbus/mutter_screen_cast.rs b/src/dbus/mutter_screen_cast.rs index 9c7ffb28..4b3c2fdd 100644 --- a/src/dbus/mutter_screen_cast.rs +++ b/src/dbus/mutter_screen_cast.rs @@ -4,7 +4,6 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::{Arc, Mutex}; use serde::Deserialize; -use smithay::output::Output; use zbus::fdo::RequestNameFlags; use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value}; use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext}; @@ -47,15 +46,40 @@ struct RecordMonitorProperties { _is_recording: Option, } +#[derive(Debug, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +struct RecordWindowProperties { + #[zvariant(rename = "window-id")] + window_id: u64, + #[zvariant(rename = "cursor-mode")] + cursor_mode: Option, + #[zvariant(rename = "is-recording")] + _is_recording: Option, +} + +static STREAM_ID: AtomicUsize = AtomicUsize::new(0); + #[derive(Clone)] pub struct Stream { - // FIXME: update on scale changes and whatnot. - output: niri_ipc::Output, + target: StreamTarget, cursor_mode: CursorMode, was_started: Arc, to_niri: calloop::channel::Sender, } +#[derive(Clone)] +enum StreamTarget { + // FIXME: update on scale changes and whatnot. + Output(niri_ipc::Output), + Window { id: u64 }, +} + +#[derive(Debug, Clone)] +pub enum StreamTargetId { + Output { name: String }, + Window { id: u64 }, +} + #[derive(Debug, SerializeDict, Type, Value)] #[zvariant(signature = "dict")] struct StreamParameters { @@ -68,14 +92,13 @@ struct StreamParameters { pub enum ScreenCastToNiri { StartCast { session_id: usize, - output: String, + target: StreamTargetId, cursor_mode: CursorMode, signal_ctx: SignalContext<'static>, }, StopCast { session_id: usize, }, - Redraw(Output), } #[dbus_interface(name = "org.gnome.Mutter.ScreenCast")] @@ -176,16 +199,51 @@ impl Session { return Err(fdo::Error::Failed("monitor is disabled".to_owned())); } - static NUMBER: AtomicUsize = AtomicUsize::new(0); let path = format!( "/org/gnome/Mutter/ScreenCast/Stream/u{}", - NUMBER.fetch_add(1, Ordering::SeqCst) + STREAM_ID.fetch_add(1, Ordering::SeqCst) + ); + let path = OwnedObjectPath::try_from(path).unwrap(); + + let cursor_mode = properties.cursor_mode.unwrap_or_default(); + + let target = StreamTarget::Output(output); + let stream = Stream::new(target, cursor_mode, self.to_niri.clone()); + match server.at(&path, stream.clone()).await { + Ok(true) => { + let iface = server.interface(&path).await.unwrap(); + self.streams.lock().unwrap().push((stream, iface)); + } + Ok(false) => return Err(fdo::Error::Failed("stream path already exists".to_owned())), + Err(err) => { + return Err(fdo::Error::Failed(format!( + "error creating stream object: {err:?}" + ))) + } + } + + Ok(path) + } + + async fn record_window( + &mut self, + #[zbus(object_server)] server: &ObjectServer, + properties: RecordWindowProperties, + ) -> fdo::Result { + debug!(?properties, "record_window"); + + let path = format!( + "/org/gnome/Mutter/ScreenCast/Stream/u{}", + STREAM_ID.fetch_add(1, Ordering::SeqCst) ); let path = OwnedObjectPath::try_from(path).unwrap(); let cursor_mode = properties.cursor_mode.unwrap_or_default(); - let stream = Stream::new(output.clone(), cursor_mode, self.to_niri.clone()); + let target = StreamTarget::Window { + id: properties.window_id, + }; + let stream = Stream::new(target, cursor_mode, self.to_niri.clone()); match server.at(&path, stream.clone()).await { Ok(true) => { let iface = server.interface(&path).await.unwrap(); @@ -214,10 +272,21 @@ impl Stream { #[dbus_interface(property)] async fn parameters(&self) -> StreamParameters { - let logical = self.output.logical.as_ref().unwrap(); - StreamParameters { - position: (logical.x, logical.y), - size: (logical.width as i32, logical.height as i32), + match &self.target { + StreamTarget::Output(output) => { + let logical = output.logical.as_ref().unwrap(); + StreamParameters { + position: (logical.x, logical.y), + size: (logical.width as i32, logical.height as i32), + } + } + StreamTarget::Window { .. } => { + // Does any consumer need this? + StreamParameters { + position: (0, 0), + size: (1, 1), + } + } } } } @@ -275,13 +344,13 @@ impl Drop for Session { } impl Stream { - pub fn new( - output: niri_ipc::Output, + fn new( + target: StreamTarget, cursor_mode: CursorMode, to_niri: calloop::channel::Sender, ) -> Self { Self { - output, + target, cursor_mode, was_started: Arc::new(AtomicBool::new(false)), to_niri, @@ -295,7 +364,7 @@ impl Stream { let msg = ScreenCastToNiri::StartCast { session_id, - output: self.output.name.clone(), + target: self.target.make_id(), cursor_mode: self.cursor_mode, signal_ctx: ctxt, }; @@ -305,3 +374,14 @@ impl Stream { } } } + +impl StreamTarget { + fn make_id(&self) -> StreamTargetId { + match self { + StreamTarget::Output(output) => StreamTargetId::Output { + name: output.name.clone(), + }, + StreamTarget::Window { id } => StreamTargetId::Window { id: *id }, + } + } +} diff --git a/src/handlers/compositor.rs b/src/handlers/compositor.rs index 845b720d..34030124 100644 --- a/src/handlers/compositor.rs +++ b/src/handlers/compositor.rs @@ -209,6 +209,9 @@ impl CompositorHandler for State { let window = mapped.window.clone(); let output = output.clone(); + #[cfg(feature = "xdp-gnome-screencast")] + let id = mapped.id(); + // This is a commit of a previously-mapped toplevel. let is_mapped = with_renderer_surface_state(surface, |state| state.buffer().is_some()) @@ -235,6 +238,12 @@ impl CompositorHandler for State { let active_window = self.niri.layout.active_window().map(|(m, _)| &m.window); let was_active = active_window == Some(&window); + #[cfg(feature = "xdp-gnome-screencast")] + self.niri + .stop_casts_for_target(crate::pw_utils::CastTarget::Window { + id: u64::from(id.get()), + }); + self.niri.layout.remove_window(&window); if was_active { diff --git a/src/handlers/xdg_shell.rs b/src/handlers/xdg_shell.rs index 30b2f5d8..f3337786 100644 --- a/src/handlers/xdg_shell.rs +++ b/src/handlers/xdg_shell.rs @@ -466,6 +466,12 @@ impl XdgShellHandler for State { let window = mapped.window.clone(); let output = output.clone(); + #[cfg(feature = "xdp-gnome-screencast")] + self.niri + .stop_casts_for_target(crate::pw_utils::CastTarget::Window { + id: u64::from(mapped.id().get()), + }); + self.backend.with_primary_renderer(|renderer| { self.niri.layout.store_unmap_snapshot(renderer, &window); }); diff --git a/src/niri.rs b/src/niri.rs index 6767faaa..57fa455c 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -116,6 +116,8 @@ use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState}; use crate::protocols::gamma_control::GammaControlManagerState; use crate::protocols::screencopy::{Screencopy, ScreencopyManagerState}; use crate::pw_utils::{Cast, PipeWire}; +#[cfg(feature = "xdp-gnome-screencast")] +use crate::pw_utils::{CastSizeChange, CastTarget, PwToNiri}; use crate::render_helpers::debug::draw_opaque_regions; use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; use crate::render_helpers::renderer::NiriRenderer; @@ -277,6 +279,10 @@ pub struct Niri { // Casts are dropped before PipeWire to prevent a double-free (yay). pub casts: Vec, pub pipewire: Option, + + // Screencast output for each mapped window. + #[cfg(feature = "xdp-gnome-screencast")] + pub mapped_cast_output: HashMap, } pub struct OutputState { @@ -508,6 +514,9 @@ impl State { foreign_toplevel::refresh(self); self.niri.refresh_window_rules(); self.refresh_ipc_outputs(); + + #[cfg(feature = "xdp-gnome-screencast")] + self.niri.refresh_mapped_cast_outputs(); } pub fn move_cursor(&mut self, location: Point) { @@ -1172,15 +1181,34 @@ impl State { } #[cfg(feature = "xdp-gnome-screencast")] - pub fn on_screen_cast_msg( - &mut self, - to_niri: &calloop::channel::Sender, - msg: ScreenCastToNiri, - ) { + pub fn on_pw_msg(&mut self, msg: PwToNiri) { + match msg { + PwToNiri::StopCast { session_id } => self.niri.stop_cast(session_id), + PwToNiri::Redraw(target) => match target { + CastTarget::Output(weak) => { + if let Some(output) = weak.upgrade() { + self.niri.queue_redraw(&output); + } + } + CastTarget::Window { id } => { + self.backend.with_primary_renderer(|renderer| { + // FIXME: target presentation time at the time of window commit? + self.niri + .render_window_for_screen_cast(renderer, id, get_monotonic_time()); + }); + } + }, + } + } + + #[cfg(feature = "xdp-gnome-screencast")] + pub fn on_screen_cast_msg(&mut self, msg: ScreenCastToNiri) { + use crate::dbus::mutter_screen_cast::StreamTargetId; + match msg { ScreenCastToNiri::StartCast { session_id, - output, + target, cursor_mode, signal_ctx, } => { @@ -1201,25 +1229,65 @@ impl State { return; }; - let Some(output) = self - .niri - .global_space - .outputs() - .find(|out| out.name() == output) - .cloned() - else { - warn!("tried to start a screencast on missing output: {output}"); - return; + let (target, size, refresh) = match target { + StreamTargetId::Output { name } => { + let global_space = &self.niri.global_space; + let output = global_space.outputs().find(|out| out.name() == name); + let Some(output) = output else { + warn!("error starting screencast: requested output is missing"); + self.niri.stop_cast(session_id); + return; + }; + + let mode = output.current_mode().unwrap(); + let transform = output.current_transform(); + let size = transform.transform_size(mode.size); + let refresh = mode.refresh as u32; + (CastTarget::Output(output.downgrade()), size, refresh) + } + StreamTargetId::Window { id } => { + let mut window = None; + self.niri.layout.with_windows(|mapped, _| { + if u64::from(mapped.id().get()) != id { + return; + } + + window = Some(mapped.window.clone()); + }); + + let Some(window) = window else { + warn!("error starting screencast: requested window is missing"); + self.niri.stop_cast(session_id); + return; + }; + + // Use the cached output since it will be present even if the output was + // currently disconnected. + let Some(output) = self.niri.mapped_cast_output.get(&window) else { + warn!("error starting screencast: requested window is missing"); + self.niri.stop_cast(session_id); + return; + }; + + let scale = Scale::from(output.current_scale().fractional_scale()); + let bbox = window.bbox_with_popups(); + let size = bbox.size.to_physical_precise_ceil(scale); + let refresh = output.current_mode().unwrap().refresh as u32; + + (CastTarget::Window { id }, size, refresh) + } }; - match pw.start_cast( - to_niri.clone(), + let res = pw.start_cast( gbm, session_id, - output, + target, + size, + refresh, cursor_mode, signal_ctx, - ) { + ); + match res { Ok(cast) => { self.niri.casts.push(cast); } @@ -1230,7 +1298,6 @@ impl State { } } ScreenCastToNiri::StopCast { session_id } => self.niri.stop_cast(session_id), - ScreenCastToNiri::Redraw(output) => self.niri.queue_redraw(&output), } } @@ -1631,6 +1698,9 @@ impl Niri { pipewire, casts: vec![], + + #[cfg(feature = "xdp-gnome-screencast")] + mapped_cast_output: HashMap::new(), } } @@ -1860,6 +1930,9 @@ impl Niri { RedrawState::WaitingForEstimatedVBlankAndQueued(token) => self.event_loop.remove(token), } + #[cfg(feature = "xdp-gnome-screencast")] + self.stop_casts_for_target(CastTarget::Output(output.downgrade())); + // Disable the output global and remove some time later to give the clients some time to // process it. let global = state.global; @@ -2574,6 +2647,54 @@ impl Niri { } } + #[cfg(feature = "xdp-gnome-screencast")] + pub fn refresh_mapped_cast_outputs(&mut self) { + use std::collections::hash_map::Entry; + + let mut seen = HashSet::new(); + let mut output_changed = vec![]; + + self.layout.with_windows(|mapped, output| { + seen.insert(mapped.window.clone()); + + let Some(output) = output else { + return; + }; + + match self.mapped_cast_output.entry(mapped.window.clone()) { + Entry::Occupied(mut entry) => { + if entry.get() != output { + entry.insert(output.clone()); + output_changed.push((mapped.id(), output.clone())); + } + } + Entry::Vacant(entry) => { + entry.insert(output.clone()); + } + } + }); + + self.mapped_cast_output.retain(|win, _| seen.contains(win)); + + let mut to_stop = vec![]; + for (id, out) in output_changed { + let refresh = out.current_mode().unwrap().refresh as u32; + let target = CastTarget::Window { + id: u64::from(id.get()), + }; + for cast in self.casts.iter_mut().filter(|cast| cast.target == target) { + if let Err(err) = cast.set_refresh(refresh) { + warn!("error changing cast FPS: {err:?}"); + to_stop.push(cast.session_id); + }; + } + } + + for session_id in to_stop { + self.stop_cast(session_id); + } + } + pub fn render( &self, renderer: &mut R, @@ -2859,6 +2980,11 @@ impl Niri { backend.with_primary_renderer(|renderer| { // Render and send to PipeWire screencast streams. self.render_for_screen_cast(renderer, output, target_presentation_time); + + // FIXME: when a window is hidden, it should probably still receive frame callbacks and + // get rendered for screen cast. This is currently unimplemented, but happens to work + // by chance, since output redrawing is more eager than it should be. + self.render_windows_for_screen_cast(renderer, output, target_presentation_time); }); } @@ -3294,10 +3420,10 @@ impl Niri { output: &Output, target_presentation_time: Duration, ) { - use crate::render_helpers::render_to_dmabuf; - let _span = tracy_client::span!("Niri::render_for_screen_cast"); + let target = CastTarget::Output(output.downgrade()); + let size = output.current_mode().unwrap().size; let transform = output.current_transform(); let size = transform.transform_size(size); @@ -3313,84 +3439,213 @@ impl Niri { continue; } - if &cast.output != output { + if cast.target != target { continue; } - if cast.size.get() != size { - if cast.pending_size.get() != size { - debug!("output size changed, updating stream size"); - if let Err(err) = cast.set_size(size) { - warn!("error updating stream size, stopping screencast: {err:?}"); - casts_to_stop.push(cast.session_id); - } - } else { - debug!("stream size still hasn't changed, skipping frame"); + match cast.ensure_size(size) { + Ok(CastSizeChange::Ready) => (), + Ok(CastSizeChange::Pending) => continue, + Err(err) => { + warn!("error updating stream size, stopping screencast: {err:?}"); + casts_to_stop.push(cast.session_id); } + } - // Even in the successful case, we'll need to wait till the size actually changes. + if cast.should_skip_frame(target_presentation_time) { continue; } - let last = cast.last_frame_time; - let min = cast.min_time_between_frames.get(); - if last.is_zero() { - trace!(?target_presentation_time, ?last, "last is zero, recording"); - } else if target_presentation_time < last { - // Record frame with a warning; in case it was an overflow this will fix it. - warn!( - ?target_presentation_time, - ?last, - "target presentation time is below last, did it overflow or did we mispredict?" - ); - } else { - let diff = target_presentation_time - last; - if diff < min { - trace!( - ?target_presentation_time, - ?last, - "skipping frame because it is too soon: diff={diff:?} < min={min:?}", - ); - continue; + // FIXME: Hidden / embedded / metadata cursor + let elements = elements.get_or_insert_with(|| { + self.render(renderer, output, true, RenderTarget::Screencast) + }); + let elements = elements.iter().rev(); + + if cast.dequeue_buffer_and_render(renderer, elements, size, scale) { + cast.last_frame_time = target_presentation_time; + } + } + self.casts = casts; + + for id in casts_to_stop { + self.stop_cast(id); + } + } + + #[cfg(feature = "xdp-gnome-screencast")] + fn render_windows_for_screen_cast( + &mut self, + renderer: &mut GlesRenderer, + output: &Output, + target_presentation_time: Duration, + ) { + let _span = tracy_client::span!("Niri::render_windows_for_screen_cast"); + + let scale = Scale::from(output.current_scale().fractional_scale()); + + let mut casts_to_stop = vec![]; + + let mut casts = mem::take(&mut self.casts); + for cast in &mut casts { + if !cast.is_active.get() { + continue; + } + + let CastTarget::Window { id } = cast.target else { + continue; + }; + + let mut windows = self.layout.windows_for_output(output); + let Some(mapped) = windows.find(|win| u64::from(win.id().get()) == id) else { + continue; + }; + + let bbox = mapped.window.bbox_with_popups(); + let size = bbox.size.to_physical_precise_ceil(scale); + + match cast.ensure_size(size) { + Ok(CastSizeChange::Ready) => (), + Ok(CastSizeChange::Pending) => continue, + Err(err) => { + warn!("error updating stream size, stopping screencast: {err:?}"); + casts_to_stop.push(cast.session_id); } } - { - let mut buffer = match cast.stream.dequeue_buffer() { - Some(buffer) => buffer, - None => { - warn!("no available buffer in pw stream, skipping frame"); - continue; - } - }; + if cast.should_skip_frame(target_presentation_time) { + continue; + } - let data = &mut buffer.datas_mut()[0]; - let fd = data.as_raw().fd as i32; - let dmabuf = cast.dmabufs.borrow()[&fd].clone(); + let alpha = if mapped.is_fullscreen() { + 1. + } else { + mapped.rules().opacity.unwrap_or(1.).clamp(0., 1.) + }; + // FIXME: pointer. + let elements = mapped.render( + renderer, + mapped.window.geometry().loc.to_f64(), + scale, + alpha, + RenderTarget::Screencast, + ); + let geo = elements + .iter() + .map(|ele| ele.geometry(scale)) + .reduce(|a, b| a.merge(b)) + .unwrap_or_default(); + let elements = elements.iter().rev().map(|elem| { + RelocateRenderElement::from_element(elem, geo.loc.upscale(-1), Relocate::Relative) + }); - // FIXME: Hidden / embedded / metadata cursor - let elements = elements.get_or_insert_with(|| { - self.render::(renderer, output, true, RenderTarget::Screencast) - }); - let elements = elements.iter().rev(); + if cast.dequeue_buffer_and_render(renderer, elements, size, scale) { + cast.last_frame_time = target_presentation_time; + } + } + self.casts = casts; - if let Err(err) = - render_to_dmabuf(renderer, dmabuf, size, scale, Transform::Normal, elements) - { - warn!("error rendering to dmabuf: {err:?}"); - continue; + for id in casts_to_stop { + self.stop_cast(id); + } + } + + #[cfg(feature = "xdp-gnome-screencast")] + fn render_window_for_screen_cast( + &mut self, + renderer: &mut GlesRenderer, + window_id: u64, + target_presentation_time: Duration, + ) { + let _span = tracy_client::span!("Niri::render_window_for_screen_cast"); + + let mut window = None; + self.layout.with_windows(|mapped, _| { + if u64::from(mapped.id().get()) != window_id { + return; + } + + window = Some(mapped.window.clone()); + }); + + let Some(window) = window else { + return; + }; + + // Use the cached output since it will be present even if the output was + // currently disconnected. + let Some(output) = self.mapped_cast_output.get(&window) else { + return; + }; + + let mut windows = self.layout.windows_for_output(output); + let mapped = windows + .find(|mapped| u64::from(mapped.id().get()) == window_id) + .unwrap(); + + let scale = Scale::from(output.current_scale().fractional_scale()); + let bbox = mapped.window.bbox_with_popups(); + let size = bbox.size.to_physical_precise_ceil(scale); + + let mut elements = None; + let mut casts_to_stop = vec![]; + + let mut casts = mem::take(&mut self.casts); + for cast in &mut casts { + if !cast.is_active.get() { + continue; + } + + if cast.target != (CastTarget::Window { id: window_id }) { + continue; + } + + match cast.ensure_size(size) { + Ok(CastSizeChange::Ready) => (), + Ok(CastSizeChange::Pending) => continue, + Err(err) => { + warn!("error updating stream size, stopping screencast: {err:?}"); + casts_to_stop.push(cast.session_id); } + } - let maxsize = data.as_raw().maxsize; - let chunk = data.chunk_mut(); - *chunk.size_mut() = maxsize; - *chunk.stride_mut() = maxsize as i32 / size.h; + if cast.should_skip_frame(target_presentation_time) { + continue; } - cast.last_frame_time = target_presentation_time; + let (elements, geo) = elements.get_or_insert_with(|| { + let alpha = if mapped.is_fullscreen() { + 1. + } else { + mapped.rules().opacity.unwrap_or(1.).clamp(0., 1.) + }; + // FIXME: pointer. + let elements = mapped.render( + renderer, + mapped.window.geometry().loc.to_f64(), + scale, + alpha, + RenderTarget::Screencast, + ); + let geo = elements + .iter() + .map(|ele| ele.geometry(scale)) + .reduce(|a, b| a.merge(b)) + .unwrap_or_default(); + (elements, geo) + }); + let elements = elements.iter().rev().map(|elem| { + RelocateRenderElement::from_element(elem, geo.loc.upscale(-1), Relocate::Relative) + }); + + if cast.dequeue_buffer_and_render(renderer, elements, size, scale) { + cast.last_frame_time = target_presentation_time; + } } self.casts = casts; + drop(windows); + for id in casts_to_stop { self.stop_cast(id); } @@ -3472,6 +3727,23 @@ impl Niri { } } + #[cfg(feature = "xdp-gnome-screencast")] + pub fn stop_casts_for_target(&mut self, target: CastTarget) { + let _span = tracy_client::span!("Niri::stop_casts_for_target"); + + // This is O(N^2) but it shouldn't be a problem I think. + let ids: Vec<_> = self + .casts + .iter() + .filter(|cast| cast.target == target) + .map(|cast| cast.session_id) + .collect(); + + for id in ids { + self.stop_cast(id); + } + } + pub fn debug_toggle_damage(&mut self) { self.debug_draw_damage = !self.debug_draw_damage; diff --git a/src/pw_utils.rs b/src/pw_utils.rs index cb63cff7..eedd0f03 100644 --- a/src/pw_utils.rs +++ b/src/pw_utils.rs @@ -27,19 +27,28 @@ use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf}; use smithay::backend::allocator::gbm::{GbmBuffer, GbmBufferFlags, GbmDevice}; use smithay::backend::allocator::Fourcc; use smithay::backend::drm::DrmDeviceFd; -use smithay::output::Output; +use smithay::backend::renderer::element::RenderElement; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::output::WeakOutput; use smithay::reexports::calloop::generic::Generic; use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction}; use smithay::reexports::gbm::Modifier; -use smithay::utils::{Physical, Size}; +use smithay::utils::{Physical, Scale, Size, Transform}; use zbus::SignalContext; -use crate::dbus::mutter_screen_cast::{self, CursorMode, ScreenCastToNiri}; +use crate::dbus::mutter_screen_cast::{self, CursorMode}; use crate::niri::State; +use crate::render_helpers::render_to_dmabuf; pub struct PipeWire { _context: Context, pub core: Core, + to_niri: calloop::channel::Sender, +} + +pub enum PwToNiri { + StopCast { session_id: usize }, + Redraw(CastTarget), } pub struct Cast { @@ -47,9 +56,8 @@ pub struct Cast { pub stream: Stream, _listener: StreamListener<()>, pub is_active: Rc>, - pub output: Output, - pub size: Rc>>, - pub pending_size: Rc>>, + pub target: CastTarget, + pub size: Rc>, pub refresh: u32, pub cursor_mode: CursorMode, pub last_frame_time: Duration, @@ -57,6 +65,28 @@ pub struct Cast { pub dmabufs: Rc>>, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CastSize { + InitialPending(Size), + Ready(Size), + ChangePending { + last_negotiated: Size, + pending: Size, + }, +} + +#[derive(PartialEq, Eq)] +pub enum CastSizeChange { + Ready, + Pending, +} + +#[derive(Clone, PartialEq, Eq)] +pub enum CastTarget { + Output(WeakOutput), + Window { id: u64 }, +} + impl PipeWire { pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result { let main_loop = MainLoop::new(None).context("error creating MainLoop")?; @@ -86,45 +116,49 @@ impl PipeWire { }) .unwrap(); + let (to_niri, from_pipewire) = calloop::channel::channel(); + event_loop + .insert_source(from_pipewire, move |event, _, state| match event { + calloop::channel::Event::Msg(msg) => state.on_pw_msg(msg), + calloop::channel::Event::Closed => (), + }) + .unwrap(); + Ok(Self { _context: context, core, + to_niri, }) } + #[allow(clippy::too_many_arguments)] pub fn start_cast( &self, - to_niri: calloop::channel::Sender, gbm: GbmDevice, session_id: usize, - output: Output, + target: CastTarget, + size: Size, + refresh: u32, cursor_mode: CursorMode, signal_ctx: SignalContext<'static>, ) -> anyhow::Result { let _span = tracy_client::span!("PipeWire::start_cast"); - let to_niri_ = to_niri.clone(); + let to_niri_ = self.to_niri.clone(); let stop_cast = move || { - if let Err(err) = to_niri_.send(ScreenCastToNiri::StopCast { session_id }) { + if let Err(err) = to_niri_.send(PwToNiri::StopCast { session_id }) { warn!("error sending StopCast to niri: {err:?}"); } }; - let weak = output.downgrade(); + let target_ = target.clone(); + let to_niri_ = self.to_niri.clone(); let redraw = move || { - if let Some(output) = weak.upgrade() { - if let Err(err) = to_niri.send(ScreenCastToNiri::Redraw(output)) { - warn!("error sending Redraw to niri: {err:?}"); - } + if let Err(err) = to_niri_.send(PwToNiri::Redraw(target_.clone())) { + warn!("error sending Redraw to niri: {err:?}"); } }; let redraw_ = redraw.clone(); - let mode = output.current_mode().unwrap(); - let size = mode.size; - let transform = output.current_transform(); - let size = transform.transform_size(size); - let refresh = mode.refresh as u32; - let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new()) .context("error creating Stream")?; @@ -133,8 +167,9 @@ impl PipeWire { let is_active = Rc::new(Cell::new(false)); let min_time_between_frames = Rc::new(Cell::new(Duration::ZERO)); let dmabufs = Rc::new(RefCell::new(HashMap::new())); - let pending_size = Rc::new(Cell::new(size)); - let size = Rc::new(Cell::new(Size::from((0, 0)))); + + let pending_size = size; + let size = Rc::new(Cell::new(CastSize::InitialPending(size))); let listener = stream .add_local_listener_with_user_data(()) @@ -186,7 +221,6 @@ impl PipeWire { .param_changed({ let min_time_between_frames = min_time_between_frames.clone(); let size = size.clone(); - let pending_size = pending_size.clone(); move |stream, (), id, pod| { let id = ParamType::from_raw(id); trace!(?id, "pw stream: param_changed"); @@ -213,9 +247,18 @@ impl PipeWire { format.parse(pod).unwrap(); trace!("pw stream: got format = {format:?}"); - assert_eq!(pending_size.get().w as u32, format.size().width); - assert_eq!(pending_size.get().h as u32, format.size().height); - size.set(pending_size.get()); + let expected_size = size.get().expected_format_size(); + let format_size = + Size::from((format.size().width as i32, format.size().height as i32)); + + if format_size == expected_size { + size.set(CastSize::Ready(expected_size)); + } else { + size.set(CastSize::ChangePending { + last_negotiated: format_size, + pending: expected_size, + }); + } let max_frame_rate = format.max_framerate(); // Subtract 0.5 ms to improve edge cases when equal to refresh rate. @@ -283,7 +326,9 @@ impl PipeWire { let stop_cast = stop_cast.clone(); let size = size.clone(); move |stream, (), buffer| { - trace!("pw stream: add_buffer"); + let size = size.get().negotiated_size(); + trace!("pw stream: add_buffer, size={:?}", size); + let size = size.expect("size must be negotiated to allocate buffers"); unsafe { let spa_buffer = (*buffer).buffer; @@ -291,8 +336,6 @@ impl PipeWire { assert!((*spa_buffer).n_datas > 0); assert!((*spa_data).type_ & (1 << DataType::DmaBuf.as_raw()) > 0); - let size = size.get(); - let bo = match gbm.create_buffer_object::<()>( size.w as u32, size.h as u32, @@ -351,7 +394,9 @@ impl PipeWire { .register() .unwrap(); - let object = make_video_params(pending_size.get(), refresh); + trace!("starting pw stream with size={pending_size:?}, refresh={refresh}"); + + let object = make_video_params(pending_size, refresh); let mut buffer = vec![]; let mut params = [make_pod(&mut buffer, object)]; stream @@ -368,9 +413,8 @@ impl PipeWire { stream, _listener: listener, is_active, - output, + target, size, - pending_size, refresh, cursor_mode, last_frame_time: Duration::ZERO, @@ -382,22 +426,160 @@ impl PipeWire { } impl Cast { - pub fn set_size(&self, size: Size) -> anyhow::Result<()> { - if self.pending_size.get() == size { - return Ok(()); + pub fn ensure_size(&self, size: Size) -> anyhow::Result { + let current_size = self.size.get(); + if current_size == CastSize::Ready(size) { + return Ok(CastSizeChange::Ready); } - let _span = tracy_client::span!("Cast::set_size"); + if current_size.pending_size() == Some(size) { + debug!("stream size still hasn't changed, skipping frame"); + return Ok(CastSizeChange::Pending); + } - self.size.set(Size::from((0, 0))); - self.pending_size.set(size); + let _span = tracy_client::span!("Cast::ensure_size"); + debug!("cast size changed, updating stream size"); + + self.size.set(current_size.with_pending(size)); let object = make_video_params(size, self.refresh); let mut buffer = vec![]; let mut params = [make_pod(&mut buffer, object)]; self.stream .update_params(&mut params) - .context("error updating stream params") + .context("error updating stream params")?; + + Ok(CastSizeChange::Pending) + } + + pub fn set_refresh(&mut self, refresh: u32) -> anyhow::Result<()> { + if self.refresh == refresh { + return Ok(()); + } + + let _span = tracy_client::span!("Cast::set_refresh"); + debug!("cast FPS changed, updating stream FPS"); + self.refresh = refresh; + + let size = self.size.get().expected_format_size(); + let object = make_video_params(size, self.refresh); + let mut buffer = vec![]; + let mut params = [make_pod(&mut buffer, object)]; + self.stream + .update_params(&mut params) + .context("error updating stream params")?; + + Ok(()) + } + + pub fn should_skip_frame(&self, target_frame_time: Duration) -> bool { + let last = self.last_frame_time; + let min = self.min_time_between_frames.get(); + + if last.is_zero() { + trace!(?target_frame_time, ?last, "last is zero, recording"); + return false; + } + + if target_frame_time < last { + // Record frame with a warning; in case it was an overflow this will fix it. + warn!( + ?target_frame_time, + ?last, + "target frame time is below last, did it overflow or did we mispredict?" + ); + return false; + } + + let diff = target_frame_time - last; + if diff < min { + trace!( + ?target_frame_time, + ?last, + "skipping frame because it is too soon: diff={diff:?} < min={min:?}", + ); + return true; + } + + false + } + + pub fn dequeue_buffer_and_render( + &mut self, + renderer: &mut GlesRenderer, + elements: impl Iterator>, + size: Size, + scale: Scale, + ) -> bool { + let mut buffer = match self.stream.dequeue_buffer() { + Some(buffer) => buffer, + None => { + warn!("no available buffer in pw stream, skipping frame"); + return false; + } + }; + + let data = &mut buffer.datas_mut()[0]; + let fd = data.as_raw().fd as i32; + let dmabuf = self.dmabufs.borrow()[&fd].clone(); + + if let Err(err) = + render_to_dmabuf(renderer, dmabuf, size, scale, Transform::Normal, elements) + { + warn!("error rendering to dmabuf: {err:?}"); + return false; + } + + let maxsize = data.as_raw().maxsize; + let chunk = data.chunk_mut(); + *chunk.size_mut() = maxsize; + *chunk.stride_mut() = maxsize as i32 / size.h; + + true + } +} + +impl CastSize { + fn pending_size(self) -> Option> { + match self { + CastSize::InitialPending(pending) => Some(pending), + CastSize::Ready(_) => None, + CastSize::ChangePending { pending, .. } => Some(pending), + } + } + + fn negotiated_size(self) -> Option> { + match self { + CastSize::InitialPending(_) => None, + CastSize::Ready(size) => Some(size), + CastSize::ChangePending { + last_negotiated, .. + } => Some(last_negotiated), + } + } + + fn expected_format_size(self) -> Size { + match self { + CastSize::InitialPending(pending) => pending, + CastSize::Ready(size) => size, + CastSize::ChangePending { pending, .. } => pending, + } + } + + fn with_pending(self, pending: Size) -> Self { + match self { + CastSize::InitialPending(_) => CastSize::InitialPending(pending), + CastSize::Ready(size) => CastSize::ChangePending { + last_negotiated: size, + pending, + }, + CastSize::ChangePending { + last_negotiated, .. + } => CastSize::ChangePending { + last_negotiated, + pending, + }, + } } } -- cgit