From d52ca23caa4345ddf768da5af24f47eae6fd4738 Mon Sep 17 00:00:00 2001 From: Ivan Molodetskikh Date: Fri, 8 Sep 2023 17:54:02 +0400 Subject: Add initial monitor screencast portal impl DmaBuf monitor screencasting through xdg-dekstop-portal-gnome! Somewhat limited currently, e.g. the cursor is always embedded. But gets most of the job done. --- src/backend/mod.rs | 19 ++ src/backend/tty.rs | 24 ++- src/backend/winit.rs | 13 ++ src/config.rs | 4 + src/dbus/mod.rs | 2 + src/dbus/mutter_display_config.rs | 88 +++++++++ src/dbus/mutter_screen_cast.rs | 256 +++++++++++++++++++++++++ src/main.rs | 1 + src/niri.rs | 329 +++++++++++++++++++++++++++----- src/pw_utils.rs | 382 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 1069 insertions(+), 49 deletions(-) create mode 100644 src/dbus/mutter_display_config.rs create mode 100644 src/dbus/mutter_screen_cast.rs create mode 100644 src/pw_utils.rs (limited to 'src') diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 8a59e6f3..8473ccc2 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,3 +1,8 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use smithay::backend::allocator::gbm::GbmDevice; +use smithay::backend::drm::DrmDeviceFd; use smithay::backend::renderer::gles::GlesRenderer; use smithay::output::Output; use smithay::wayland::dmabuf::DmabufFeedback; @@ -79,6 +84,20 @@ impl Backend { } } + pub fn connectors(&self) -> Arc>> { + match self { + Backend::Tty(tty) => tty.connectors(), + Backend::Winit(winit) => winit.connectors(), + } + } + + pub fn gbm_device(&self) -> Option> { + match self { + Backend::Tty(tty) => tty.gbm_device(), + Backend::Winit(_) => None, + } + } + pub fn tty(&mut self) -> &mut Tty { if let Self::Tty(v) = self { v diff --git a/src/backend/tty.rs b/src/backend/tty.rs index a3298655..343da54e 100644 --- a/src/backend/tty.rs +++ b/src/backend/tty.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::os::fd::FromRawFd; use std::path::{Path, PathBuf}; +use std::sync::{Mutex, Arc}; use std::time::Duration; use anyhow::{anyhow, Context}; @@ -44,6 +45,7 @@ pub struct Tty { udev_dispatcher: Dispatcher<'static, UdevBackend, LoopData>, primary_gpu_path: PathBuf, output_device: Option, + connectors: Arc>>, } type GbmDrmCompositor = DrmCompositor< @@ -236,6 +238,7 @@ impl Tty { udev_dispatcher, primary_gpu_path, output_device: None, + connectors: Arc::new(Mutex::new(HashMap::new())), } } @@ -549,6 +552,11 @@ impl Tty { tracy_client::internal::create_frame_name(format!("vblank on {output_name}\0").leak()) }; + self.connectors + .lock() + .unwrap() + .insert(output_name.clone(), output.clone()); + let surface = Surface { name: output_name, compositor, @@ -574,10 +582,10 @@ impl Tty { debug!("disconnecting connector: {connector:?}"); let device = self.output_device.as_mut().unwrap(); - if device.surfaces.remove(&crtc).is_none() { - debug!("crts wasn't enabled"); + let Some(surface) = device.surfaces.remove(&crtc) else { + debug!("crtc wasn't enabled"); return; - } + }; let output = niri .global_space @@ -590,6 +598,8 @@ impl Tty { .clone(); niri.remove_output(&output); + + self.connectors.lock().unwrap().remove(&surface.name); } pub fn seat_name(&self) -> String { @@ -680,6 +690,14 @@ impl Tty { pub fn dmabuf_state(&mut self) -> &mut DmabufState { &mut self.output_device.as_mut().unwrap().dmabuf_state } + + pub fn connectors(&self) -> Arc>> { + self.connectors.clone() + } + + pub fn gbm_device(&self) -> Option> { + self.output_device.as_ref().map(|d| d.gbm.clone()) + } } fn refresh_interval(mode: DrmMode) -> Duration { diff --git a/src/backend/winit.rs b/src/backend/winit.rs index be8ec29a..e7a50ac7 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; use std::time::Duration; use smithay::backend::renderer::damage::OutputDamageTracker; @@ -21,6 +23,7 @@ pub struct Winit { output: Output, backend: WinitGraphicsBackend, damage_tracker: OutputDamageTracker, + connectors: Arc>>, } impl Winit { @@ -53,6 +56,11 @@ impl Winit { ); output.set_preferred(mode); + let connectors = Arc::new(Mutex::new(HashMap::from([( + "winit".to_owned(), + output.clone(), + )]))); + let damage_tracker = OutputDamageTracker::from_output(&output); let timer = Timer::immediate(); @@ -99,6 +107,7 @@ impl Winit { output, backend, damage_tracker, + connectors, } } @@ -162,4 +171,8 @@ impl Winit { let renderer = self.backend.renderer(); renderer.set_debug_flags(renderer.debug_flags() ^ DebugFlags::TINT); } + + pub fn connectors(&self) -> Arc>> { + self.connectors.clone() + } } diff --git a/src/config.rs b/src/config.rs index 9d03275e..b624f88e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -128,12 +128,15 @@ pub enum Action { pub struct DebugConfig { #[knuffel(child, unwrap(argument), default = 1.)] pub animation_slowdown: f64, + #[knuffel(child)] + pub screen_cast_in_non_session_instances: bool, } impl Default for DebugConfig { fn default() -> Self { Self { animation_slowdown: 1., + screen_cast_in_non_session_instances: false, } } } @@ -308,6 +311,7 @@ mod tests { ]), debug: DebugConfig { animation_slowdown: 2., + ..Default::default() }, }, ); diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index 01b4c391..84a8bc30 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -1 +1,3 @@ +pub mod mutter_display_config; +pub mod mutter_screen_cast; pub mod mutter_service_channel; diff --git a/src/dbus/mutter_display_config.rs b/src/dbus/mutter_display_config.rs new file mode 100644 index 00000000..1807f01d --- /dev/null +++ b/src/dbus/mutter_display_config.rs @@ -0,0 +1,88 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use serde::Serialize; +use smithay::output::Output; +use zbus::zvariant::{OwnedValue, Type}; +use zbus::{dbus_interface, fdo}; + +pub struct DisplayConfig { + connectors: Arc>>, +} + +#[derive(Serialize, Type)] +pub struct Monitor { + names: (String, String, String, String), + modes: Vec, + properties: HashMap, +} + +#[derive(Serialize, Type)] +pub struct Mode { + id: String, + width: i32, + height: i32, + refresh_rate: f64, + preferred_scale: f64, + supported_scales: Vec, + properties: HashMap, +} + +#[derive(Serialize, Type)] +pub struct LogicalMonitor { + x: i32, + y: i32, + scale: f64, + transform: u32, + is_primary: bool, + monitors: Vec<(String, String, String, String)>, + properties: HashMap, +} + +#[dbus_interface(name = "org.gnome.Mutter.DisplayConfig")] +impl DisplayConfig { + async fn get_current_state( + &self, + ) -> fdo::Result<( + u32, + Vec, + Vec, + HashMap, + )> { + // Construct the DBus response. + let monitors: Vec = self + .connectors + .lock() + .unwrap() + .keys() + .map(|c| Monitor { + names: (c.clone(), String::new(), String::new(), String::new()), + modes: vec![], + properties: HashMap::new(), + }) + .collect(); + + let logical_monitors = monitors + .iter() + .map(|m| LogicalMonitor { + x: 0, + y: 0, + scale: 1., + transform: 0, + is_primary: false, + monitors: vec![m.names.clone()], + properties: HashMap::new(), + }) + .collect(); + + Ok((0, monitors, logical_monitors, HashMap::new())) + } + + // FIXME: monitors-changed signal. +} + +impl DisplayConfig { + pub fn new(connectors: Arc>>) -> Self { + Self { connectors } + } +} diff --git a/src/dbus/mutter_screen_cast.rs b/src/dbus/mutter_screen_cast.rs new file mode 100644 index 00000000..320319cc --- /dev/null +++ b/src/dbus/mutter_screen_cast.rs @@ -0,0 +1,256 @@ +use std::collections::HashMap; +use std::mem; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +use serde::Deserialize; +use smithay::output::Output; +use smithay::reexports::calloop; +use zbus::zvariant::{DeserializeDict, OwnedObjectPath, Type, Value}; +use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext}; + +#[derive(Clone)] +pub struct ScreenCast { + connectors: Arc>>, + to_niri: calloop::channel::Sender, + sessions: Arc)>>>, +} + +#[derive(Clone)] +pub struct Session { + id: usize, + connectors: Arc>>, + to_niri: calloop::channel::Sender, + streams: Arc)>>>, +} + +#[derive(Debug, Default, Deserialize, Type, Clone, Copy)] +pub enum CursorMode { + #[default] + Hidden = 0, + Embedded = 1, + Metadata = 2, +} + +#[derive(Debug, DeserializeDict, Type)] +#[zvariant(signature = "dict")] +struct RecordMonitorProperties { + #[zvariant(rename = "cursor-mode")] + cursor_mode: Option, + #[zvariant(rename = "is-recording")] + _is_recording: Option, +} + +#[derive(Clone)] +pub struct Stream { + output: Output, + cursor_mode: CursorMode, + was_started: Arc, + to_niri: calloop::channel::Sender, +} + +pub enum ToNiriMsg { + StartCast { + session_id: usize, + output: Output, + cursor_mode: CursorMode, + signal_ctx: SignalContext<'static>, + }, + StopCast { + session_id: usize, + }, +} + +#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")] +impl ScreenCast { + async fn create_session( + &self, + #[zbus(object_server)] server: &ObjectServer, + properties: HashMap<&str, Value<'_>>, + ) -> fdo::Result { + if properties.contains_key("remote-desktop-session-id") { + return Err(fdo::Error::Failed( + "there are no remote desktop sessions".to_owned(), + )); + } + + static NUMBER: AtomicUsize = AtomicUsize::new(0); + let session_id = NUMBER.fetch_add(1, Ordering::SeqCst); + let path = format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id); + let path = OwnedObjectPath::try_from(path).unwrap(); + + let session = Session::new(session_id, self.connectors.clone(), self.to_niri.clone()); + match server.at(&path, session.clone()).await { + Ok(true) => { + let iface = server.interface(&path).await.unwrap(); + self.sessions.lock().unwrap().push((session, iface)); + } + Ok(false) => return Err(fdo::Error::Failed("session path already exists".to_owned())), + Err(err) => { + return Err(fdo::Error::Failed(format!( + "error creating session object: {err:?}" + ))) + } + } + + Ok(path) + } + + #[dbus_interface(property)] + async fn version(&self) -> i32 { + 4 + } +} + +#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Session")] +impl Session { + async fn start(&self) { + debug!("start"); + + for (stream, iface) in &*self.streams.lock().unwrap() { + stream.start(self.id, iface.signal_context().clone()); + } + } + + pub async fn stop( + &self, + #[zbus(object_server)] server: &ObjectServer, + #[zbus(signal_context)] ctxt: SignalContext<'_>, + ) { + debug!("stop"); + + Session::closed(&ctxt).await.unwrap(); + + if let Err(err) = self.to_niri.send(ToNiriMsg::StopCast { + session_id: self.id, + }) { + warn!("error sending StopCast to niri: {err:?}"); + } + + let streams = mem::take(&mut *self.streams.lock().unwrap()); + for (_, iface) in streams.iter() { + server + .remove::(iface.signal_context().path()) + .await + .unwrap(); + } + + server.remove::(ctxt.path()).await.unwrap(); + } + + async fn record_monitor( + &mut self, + #[zbus(object_server)] server: &ObjectServer, + connector: &str, + properties: RecordMonitorProperties, + ) -> fdo::Result { + debug!(connector, ?properties, "record_monitor"); + + let Some(output) = self.connectors.lock().unwrap().get(connector).cloned() else { + return Err(fdo::Error::Failed("no such monitor".to_owned())); + }; + + static NUMBER: AtomicUsize = AtomicUsize::new(0); + let path = format!( + "/org/gnome/Mutter/ScreenCast/Stream/u{}", + NUMBER.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, 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) + } + + #[dbus_interface(signal)] + async fn closed(ctxt: &SignalContext<'_>) -> zbus::Result<()>; +} + +#[dbus_interface(name = "org.gnome.Mutter.ScreenCast.Stream")] +impl Stream { + #[dbus_interface(signal)] + pub async fn pipe_wire_stream_added(ctxt: &SignalContext<'_>, node_id: u32) + -> zbus::Result<()>; +} + +impl ScreenCast { + pub fn new( + connectors: Arc>>, + to_niri: calloop::channel::Sender, + ) -> Self { + Self { + connectors, + to_niri, + sessions: Arc::new(Mutex::new(vec![])), + } + } +} + +impl Session { + pub fn new( + id: usize, + connectors: Arc>>, + to_niri: calloop::channel::Sender, + ) -> Self { + Self { + id, + connectors, + streams: Arc::new(Mutex::new(vec![])), + to_niri, + } + } +} + +impl Drop for Session { + fn drop(&mut self) { + let _ = self.to_niri.send(ToNiriMsg::StopCast { + session_id: self.id, + }); + } +} + +impl Stream { + pub fn new( + output: Output, + cursor_mode: CursorMode, + to_niri: calloop::channel::Sender, + ) -> Self { + Self { + output, + cursor_mode, + was_started: Arc::new(AtomicBool::new(false)), + to_niri, + } + } + + fn start(&self, session_id: usize, ctxt: SignalContext<'static>) { + if self.was_started.load(Ordering::SeqCst) { + return; + } + + let msg = ToNiriMsg::StartCast { + session_id, + output: self.output.clone(), + cursor_mode: self.cursor_mode, + signal_ctx: ctxt, + }; + + if let Err(err) = self.to_niri.send(msg) { + warn!("error sending StartCast to niri: {err:?}"); + } + } +} diff --git a/src/main.rs b/src/main.rs index 81422065..6c5e9ab0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod handlers; mod input; mod layout; mod niri; +mod pw_utils; mod utils; use std::env; diff --git a/src/niri.rs b/src/niri.rs index b4302a4c..a48879e9 100644 --- a/src/niri.rs +++ b/src/niri.rs @@ -8,6 +8,7 @@ use std::{env, thread}; use anyhow::Context; use directories::UserDirs; use sd_notify::NotifyState; +use smithay::backend::allocator::dmabuf::Dmabuf; use smithay::backend::allocator::Fourcc; use smithay::backend::renderer::element::surface::{ render_elements_from_surface_tree, WaylandSurfaceRenderElement, @@ -16,7 +17,7 @@ use smithay::backend::renderer::element::texture::{TextureBuffer, TextureRenderE use smithay::backend::renderer::element::{ render_elements, AsRenderElements, Element, RenderElement, RenderElementStates, }; -use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; +use smithay::backend::renderer::gles::{GlesMapping, GlesRenderer, GlesTexture}; use smithay::backend::renderer::{Bind, ExportMem, Frame, ImportAll, Offscreen, Renderer}; use smithay::desktop::utils::{ send_dmabuf_feedback_surface_tree, send_frames_surface_tree, @@ -31,7 +32,7 @@ use smithay::input::pointer::{CursorImageAttributes, CursorImageStatus, MotionEv use smithay::input::{Seat, SeatState}; use smithay::output::Output; use smithay::reexports::calloop::generic::Generic; -use smithay::reexports::calloop::{Idle, Interest, LoopHandle, LoopSignal, Mode, PostAction}; +use smithay::reexports::calloop::{self, Idle, Interest, LoopHandle, LoopSignal, Mode, PostAction}; use smithay::reexports::nix::libc::CLOCK_MONOTONIC; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::WmCapabilities; use smithay::reexports::wayland_server::backend::{ @@ -40,7 +41,7 @@ use smithay::reexports::wayland_server::backend::{ use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::{Display, DisplayHandle}; use smithay::utils::{ - IsAlive, Logical, Physical, Point, Rectangle, Scale, Transform, SERIAL_COUNTER, + IsAlive, Logical, Physical, Point, Rectangle, Scale, Size, Transform, SERIAL_COUNTER, }; use smithay::wayland::compositor::{with_states, CompositorClientState, CompositorState}; use smithay::wayland::data_device::DataDeviceState; @@ -57,9 +58,12 @@ use time::OffsetDateTime; use crate::backend::{Backend, Tty, Winit}; use crate::config::Config; +use crate::dbus::mutter_display_config::DisplayConfig; +use crate::dbus::mutter_screen_cast::{self, ScreenCast, ToNiriMsg}; use crate::dbus::mutter_service_channel::ServiceChannel; use crate::frame_clock::FrameClock; use crate::layout::{MonitorRenderElement, MonitorSet}; +use crate::pw_utils::{Cast, PipeWire}; use crate::utils::{center, get_monotonic_time, load_default_cursor}; use crate::LoopData; @@ -102,6 +106,11 @@ pub struct Niri { pub zbus_conn: Option, pub inhibit_power_key_fd: Option, + pub screen_cast: ScreenCast, + + // Casts are dropped before PipeWire to prevent a double-free (yay). + pub casts: Vec, + pub pipewire: Option, } pub struct OutputState { @@ -137,13 +146,7 @@ impl State { Backend::Tty(Tty::new(event_loop.clone())) }; - let mut niri = Niri::new( - &config, - event_loop, - stop_signal, - display, - backend.seat_name(), - ); + let mut niri = Niri::new(&config, event_loop, stop_signal, display, &backend); backend.init(&mut niri); Self { @@ -195,7 +198,7 @@ impl Niri { event_loop: LoopHandle<'static, LoopData>, stop_signal: LoopSignal, display: &mut Display, - seat_name: String, + backend: &Backend, ) -> Self { let display_handle = display.handle(); @@ -215,7 +218,7 @@ impl Niri { let presentation_state = PresentationState::new::(&display_handle, CLOCK_MONOTONIC as u32); - let mut seat: Seat = seat_state.new_wl_seat(&display_handle, seat_name); + let mut seat: Seat = seat_state.new_wl_seat(&display_handle, backend.seat_name()); let xkb = XkbConfig { rules: &config.input.keyboard.xkb.rules, model: &config.input.keyboard.xkb.model, @@ -246,6 +249,102 @@ impl Niri { socket_name.to_string_lossy() ); + let pipewire = match PipeWire::new(&event_loop) { + Ok(pipewire) => Some(pipewire), + Err(err) => { + warn!("error starting PipeWire: {err:?}"); + None + } + }; + + let (to_niri, from_screen_cast) = calloop::channel::channel(); + event_loop + .insert_source(from_screen_cast, { + let to_niri = to_niri.clone(); + move |event, _, data| match event { + calloop::channel::Event::Msg(msg) => match msg { + ToNiriMsg::StartCast { + session_id, + output, + cursor_mode, + signal_ctx, + } => { + let _span = tracy_client::span!("StartCast"); + + debug!(session_id, "StartCast"); + + let gbm = match data.state.backend.gbm_device() { + Some(gbm) => gbm, + None => { + debug!("no GBM device available"); + return; + } + }; + + let pw = data.state.niri.pipewire.as_ref().unwrap(); + match pw.start_cast( + to_niri.clone(), + gbm, + session_id, + output, + cursor_mode, + signal_ctx, + ) { + Ok(cast) => { + data.state.niri.casts.push(cast); + } + Err(err) => { + warn!("error starting screencast: {err:?}"); + + if let Err(err) = + to_niri.send(ToNiriMsg::StopCast { session_id }) + { + warn!("error sending StopCast to niri: {err:?}"); + } + } + } + } + ToNiriMsg::StopCast { session_id } => { + let _span = tracy_client::span!("StopCast"); + + debug!(session_id, "StopCast"); + + for i in (0..data.state.niri.casts.len()).rev() { + let cast = &data.state.niri.casts[i]; + if cast.session_id != session_id { + continue; + } + + let cast = data.state.niri.casts.swap_remove(i); + if let Err(err) = cast.stream.disconnect() { + warn!("error disconnecting stream: {err:?}"); + } + } + + let server = + data.state.niri.zbus_conn.as_ref().unwrap().object_server(); + let path = + format!("/org/gnome/Mutter/ScreenCast/Session/u{}", session_id); + if let Ok(iface) = + server.interface::<_, mutter_screen_cast::Session>(path) + { + let _span = tracy_client::span!("invoking Session::stop"); + + async_io::block_on(async move { + iface + .get() + .stop(&server, iface.signal_context().clone()) + .await + }); + } + } + }, + calloop::channel::Event::Closed => (), + } + }) + .unwrap(); + let screen_cast = ScreenCast::new(backend.connectors(), to_niri); + let mut zbus_conn = None; let mut inhibit_power_key_fd = None; if std::env::var_os("NOTIFY_SOCKET").is_some() { @@ -278,7 +377,7 @@ impl Niri { } // Set up zbus, make sure it happens before anything might want it. - let conn = zbus::blocking::ConnectionBuilder::session() + let mut conn = zbus::blocking::ConnectionBuilder::session() .unwrap() .name("org.gnome.Mutter.ServiceChannel") .unwrap() @@ -286,9 +385,24 @@ impl Niri { "/org/gnome/Mutter/ServiceChannel", ServiceChannel::new(display_handle.clone()), ) - .unwrap() - .build() .unwrap(); + + if pipewire.is_some() && !config.debug.screen_cast_in_non_session_instances { + conn = conn + .name("org.gnome.Mutter.ScreenCast") + .unwrap() + .serve_at("/org/gnome/Mutter/ScreenCast", screen_cast.clone()) + .unwrap() + .name("org.gnome.Mutter.DisplayConfig") + .unwrap() + .serve_at( + "/org/gnome/Mutter/DisplayConfig", + DisplayConfig::new(backend.connectors()), + ) + .unwrap(); + } + + let conn = conn.build().unwrap(); zbus_conn = Some(conn); // Notify systemd we're ready. @@ -321,6 +435,23 @@ impl Niri { warn!("error inhibiting power key: {err:?}"); } } + } else if pipewire.is_some() && config.debug.screen_cast_in_non_session_instances { + let conn = zbus::blocking::ConnectionBuilder::session() + .unwrap() + .name("org.gnome.Mutter.ScreenCast") + .unwrap() + .serve_at("/org/gnome/Mutter/ScreenCast", screen_cast.clone()) + .unwrap() + .name("org.gnome.Mutter.DisplayConfig") + .unwrap() + .serve_at( + "/org/gnome/Mutter/DisplayConfig", + DisplayConfig::new(backend.connectors()), + ) + .unwrap() + .build() + .unwrap(); + zbus_conn = Some(conn); } let display_source = Generic::new( @@ -364,6 +495,9 @@ impl Niri { zbus_conn, inhibit_power_key_fd, + screen_cast, + pipewire, + casts: vec![], } } @@ -720,6 +854,9 @@ impl Niri { // Send the frame callbacks. self.send_frame_callbacks(output); + + // Render and send to PipeWire screencast streams. + self.send_for_screen_cast(backend, output, &elements, presentation_time); } fn send_dmabuf_feedbacks(&self, output: &Output, feedback: &DmabufFeedback) { @@ -827,47 +964,74 @@ impl Niri { feedback } - pub fn screenshot( + fn send_for_screen_cast( &mut self, - renderer: &mut GlesRenderer, + backend: &mut Backend, output: &Output, - ) -> anyhow::Result<()> { - let _span = tracy_client::span!("Niri::screenshot"); + elements: &[OutputRenderElements], + presentation_time: Duration, + ) { + let _span = tracy_client::span!("Niri::send_for_screen_cast"); let size = output.current_mode().unwrap().size; - let output_rect = Rectangle::from_loc_and_size((0, 0), size); - let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal); - let fourcc = Fourcc::Abgr8888; - let texture: GlesTexture = renderer - .create_buffer(fourcc, buffer_size) - .context("error creating texture")?; + for cast in &mut self.casts { + if !cast.is_active.get() { + continue; + } - let elements = self.render(renderer, output); + if &cast.output != output { + continue; + } + + let last = cast.last_frame_time; + let min = cast.min_time_between_frames.get(); + if !last.is_zero() && presentation_time - last < min { + trace!( + "skipping frame because it is too soon \ + last={last:?} now={presentation_time:?} diff={:?} < min={min:?}", + presentation_time - last, + ); + continue; + } + + { + let mut buffer = match cast.stream.dequeue_buffer() { + Some(buffer) => buffer, + None => { + warn!("no available buffer in pw stream, skipping frame"); + continue; + } + }; - renderer.bind(texture).context("error binding texture")?; - let mut frame = renderer - .render(size, Transform::Normal) - .context("error starting frame")?; - - frame - .clear([0.1, 0.1, 0.1, 1.], &[output_rect]) - .context("error clearing")?; - - for element in elements.into_iter().rev() { - let src = element.src(); - let dst = element.geometry(Scale::from(1.)); - element - .draw(&mut frame, src, dst, &[output_rect]) - .context("error drawing element")?; + let data = &mut buffer.datas_mut()[0]; + let fd = data.as_raw().fd as i32; + let dmabuf = cast.dmabufs.borrow()[&fd].clone(); + + // FIXME: Hidden / embedded / metadata cursor + render_to_dmabuf(backend.renderer(), dmabuf, size, elements).unwrap(); + + let maxsize = data.as_raw().maxsize; + let chunk = data.chunk_mut(); + *chunk.size_mut() = maxsize; + *chunk.stride_mut() = maxsize as i32 / size.h; + } + + cast.last_frame_time = presentation_time; } + } - let sync_point = frame.finish().context("error finishing frame")?; - sync_point.wait(); + pub fn screenshot( + &mut self, + renderer: &mut GlesRenderer, + output: &Output, + ) -> anyhow::Result<()> { + let _span = tracy_client::span!("Niri::screenshot"); + + let size = output.current_mode().unwrap().size; + let elements = self.render(renderer, output); - let mapping = renderer - .copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc) - .context("error copying framebuffer")?; + let mapping = render_and_download(renderer, size, &elements).context("error rendering")?; let copy = renderer .map_texture(&mapping) .context("error mapping texture")?; @@ -928,3 +1092,76 @@ impl ClientData for ClientState { fn initialized(&self, _client_id: ClientId) {} fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {} } + +fn render_and_download( + renderer: &mut GlesRenderer, + size: Size, + elements: &[OutputRenderElements], +) -> anyhow::Result { + let _span = tracy_client::span!("render_and_download"); + + let output_rect = Rectangle::from_loc_and_size((0, 0), size); + let buffer_size = size.to_logical(1).to_buffer(1, Transform::Normal); + let fourcc = Fourcc::Abgr8888; + + let texture: GlesTexture = renderer + .create_buffer(fourcc, buffer_size) + .context("error creating texture")?; + + renderer.bind(texture).context("error binding texture")?; + let mut frame = renderer + .render(size, Transform::Normal) + .context("error starting frame")?; + + frame + .clear([0.1, 0.1, 0.1, 1.], &[output_rect]) + .context("error clearing")?; + + for element in elements.iter().rev() { + let src = element.src(); + let dst = element.geometry(Scale::from(1.)); + element + .draw(&mut frame, src, dst, &[output_rect]) + .context("error drawing element")?; + } + + let sync_point = frame.finish().context("error finishing frame")?; + sync_point.wait(); + + let mapping = renderer + .copy_framebuffer(Rectangle::from_loc_and_size((0, 0), buffer_size), fourcc) + .context("error copying framebuffer")?; + Ok(mapping) +} + +fn render_to_dmabuf( + renderer: &mut GlesRenderer, + dmabuf: Dmabuf, + size: Size, + elements: &[OutputRenderElements], +) -> anyhow::Result<()> { + let _span = tracy_client::span!("render_to_dmabuf"); + + let output_rect = Rectangle::from_loc_and_size((0, 0), size); + + renderer.bind(dmabuf).context("error binding texture")?; + let mut frame = renderer + .render(size, Transform::Normal) + .context("error starting frame")?; + + frame + .clear([0.1, 0.1, 0.1, 1.], &[output_rect]) + .context("error clearing")?; + + for element in elements.iter().rev() { + let src = element.src(); + let dst = element.geometry(Scale::from(1.)); + element + .draw(&mut frame, src, dst, &[output_rect]) + .context("error drawing element")?; + } + + let _sync_point = frame.finish().context("error finishing frame")?; + + Ok(()) +} diff --git a/src/pw_utils.rs b/src/pw_utils.rs new file mode 100644 index 00000000..ffcf6b67 --- /dev/null +++ b/src/pw_utils.rs @@ -0,0 +1,382 @@ +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; +use std::io::Cursor; +use std::mem; +use std::os::fd::AsRawFd; +use std::rc::Rc; +use std::time::Duration; + +use anyhow::Context as _; +use pipewire::spa::data::DataType; +use pipewire::spa::format::{FormatProperties, MediaSubtype, MediaType}; +use pipewire::spa::param::format_utils::parse_format; +use pipewire::spa::param::video::{VideoFormat, VideoInfoRaw}; +use pipewire::spa::param::ParamType; +use pipewire::spa::pod::serialize::PodSerializer; +use pipewire::spa::pod::{self, ChoiceValue, Pod, Property, PropertyFlags}; +use pipewire::spa::sys::*; +use pipewire::spa::utils::{Choice, ChoiceEnum, ChoiceFlags, Fraction, Rectangle, SpaTypes}; +use pipewire::spa::Direction; +use pipewire::stream::{Stream, StreamFlags, StreamListener, StreamState}; +use pipewire::{Context, Core, MainLoop, Properties}; +use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf}; +use smithay::backend::allocator::gbm::{GbmBufferFlags, GbmDevice}; +use smithay::backend::allocator::Fourcc; +use smithay::backend::drm::DrmDeviceFd; +use smithay::output::Output; +use smithay::reexports::calloop::generic::Generic; +use smithay::reexports::calloop::{self, Interest, LoopHandle, Mode, PostAction}; +use smithay::reexports::gbm::Modifier; +use zbus::SignalContext; + +use crate::dbus::mutter_screen_cast::{self, CursorMode, ToNiriMsg}; +use crate::LoopData; + +pub struct PipeWire { + _context: Context, + pub core: Core, +} + +pub struct Cast { + pub session_id: usize, + pub stream: Rc, + _listener: StreamListener<()>, + pub is_active: Rc>, + pub output: Output, + pub cursor_mode: CursorMode, + pub last_frame_time: Duration, + pub min_time_between_frames: Rc>, + pub dmabufs: Rc>>, +} + +impl PipeWire { + pub fn new(event_loop: &LoopHandle<'static, LoopData>) -> anyhow::Result { + let main_loop = MainLoop::new().context("error creating MainLoop")?; + let context = Context::new(&main_loop).context("error creating Context")?; + let core = context.connect(None).context("error creating Core")?; + + let listener = core + .add_listener_local() + .error(|id, seq, res, message| { + warn!(id, seq, res, message, "pw error"); + }) + .register(); + mem::forget(listener); + + let generic = Generic::new(main_loop.fd().as_raw_fd(), Interest::READ, Mode::Level); + event_loop + .insert_source(generic, move |_, _, _| { + let _span = tracy_client::span!("pipewire iteration"); + main_loop.iterate(Duration::ZERO); + Ok(PostAction::Continue) + }) + .unwrap(); + + Ok(Self { + _context: context, + core, + }) + } + + pub fn start_cast( + &self, + to_niri: calloop::channel::Sender, + gbm: GbmDevice, + session_id: usize, + output: Output, + cursor_mode: CursorMode, + signal_ctx: SignalContext<'static>, + ) -> anyhow::Result { + let _span = tracy_client::span!("PipeWire::start_cast"); + + let stop_cast = move || { + if let Err(err) = to_niri.send(ToNiriMsg::StopCast { session_id }) { + warn!("error sending StopCast to niri: {err:?}"); + } + }; + + let mode = output.current_mode().unwrap(); + let size = mode.size; + let refresh = mode.refresh; + + let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new()) + .context("error creating Stream")?; + + // Like in good old wayland-rs times... + let stream = Rc::new(stream); + let node_id = Rc::new(Cell::new(None)); + 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 listener = stream + .add_local_listener_with_user_data(()) + .state_changed({ + let stream = stream.clone(); + let is_active = is_active.clone(); + let stop_cast = stop_cast.clone(); + move |old, new| { + debug!("pw stream: state changed: {old:?} -> {new:?}"); + + match new { + StreamState::Paused => { + if node_id.get().is_none() { + let id = stream.node_id(); + node_id.set(Some(id)); + debug!("pw stream: sending signal with {id}"); + + let _span = tracy_client::span!("sending PipeWireStreamAdded"); + async_io::block_on(async { + let res = mutter_screen_cast::Stream::pipe_wire_stream_added( + &signal_ctx, + id, + ) + .await; + + if let Err(err) = res { + warn!("error sending PipeWireStreamAdded: {err:?}"); + stop_cast(); + } + }); + } + + is_active.set(false); + } + StreamState::Error(_) => { + if is_active.get() { + is_active.set(false); + stop_cast(); + } + } + StreamState::Unconnected => (), + StreamState::Connecting => (), + StreamState::Streaming => { + is_active.set(true); + } + } + } + }) + .param_changed({ + let min_time_between_frames = min_time_between_frames.clone(); + move |stream, id, _data, pod| { + let id = ParamType::from_raw(id); + trace!(?id, "pw stream: param_changed"); + + if id != ParamType::Format { + return; + } + + let Some(pod) = pod else { return }; + + let (m_type, m_subtype) = match parse_format(pod) { + Ok(x) => x, + Err(err) => { + warn!("pw stream: error parsing format: {err:?}"); + return; + } + }; + + if m_type != MediaType::Video || m_subtype != MediaSubtype::Raw { + return; + } + + let mut format = VideoInfoRaw::new(); + format.parse(pod).unwrap(); + trace!("pw stream: got format = {format:?}"); + + let max_frame_rate = format.max_framerate(); + // Subtract 0.5 ms to improve edge cases when equal to refresh rate. + let min_frame_time = Duration::from_secs_f64( + max_frame_rate.denom as f64 / max_frame_rate.num as f64, + ) - Duration::from_micros(500); + min_time_between_frames.set(min_frame_time); + + const BPP: u32 = 4; + let stride = format.size().width * BPP; + let size = stride * format.size().height; + + let o1 = pod::object!( + SpaTypes::ObjectParamBuffers, + ParamType::Buffers, + Property::new( + SPA_PARAM_BUFFERS_buffers, + pod::Value::Choice(ChoiceValue::Int(Choice( + ChoiceFlags::empty(), + ChoiceEnum::Range { + default: 16, + min: 2, + max: 16 + } + ))), + ), + Property::new(SPA_PARAM_BUFFERS_blocks, pod::Value::Int(1)), + Property::new(SPA_PARAM_BUFFERS_size, pod::Value::Int(size as i32)), + Property::new(SPA_PARAM_BUFFERS_stride, pod::Value::Int(stride as i32)), + Property::new(SPA_PARAM_BUFFERS_align, pod::Value::Int(16)), + Property::new( + SPA_PARAM_BUFFERS_dataType, + pod::Value::Choice(ChoiceValue::Int(Choice( + ChoiceFlags::empty(), + ChoiceEnum::Flags { + default: 1 << DataType::DmaBuf.as_raw(), + flags: vec![1 << DataType::DmaBuf.as_raw()], + }, + ))), + ), + ); + + // FIXME: Hidden / embedded / metadata cursor + + // let o2 = pod::object!( + // SpaTypes::ObjectParamMeta, + // ParamType::Meta, + // Property::new(SPA_PARAM_META_type, + // pod::Value::Id(Id(SPA_META_Header))), + // Property::new( + // SPA_PARAM_META_size, + // pod::Value::Int(size_of::() as i32) + // ), + // ); + let mut b1 = vec![]; + // let mut b2 = vec![]; + let mut params = [ + make_pod(&mut b1, o1).as_raw_ptr().cast_const(), + // make_pod(&mut b2, o2).as_raw_ptr().cast_const(), + ]; + stream.update_params(&mut params).unwrap(); + } + }) + .add_buffer({ + let dmabufs = dmabufs.clone(); + let stop_cast = stop_cast.clone(); + move |buffer| { + trace!("pw stream: add_buffer"); + + unsafe { + let spa_buffer = (*buffer).buffer; + let spa_data = (*spa_buffer).datas; + assert!((*spa_buffer).n_datas > 0); + assert!((*spa_data).type_ & (1 << DataType::DmaBuf.as_raw()) > 0); + + let bo = match gbm.create_buffer_object::<()>( + size.w as u32, + size.h as u32, + Fourcc::Xrgb8888, + GbmBufferFlags::RENDERING | GbmBufferFlags::LINEAR, + ) { + Ok(bo) => bo, + Err(err) => { + warn!("error creating GBM buffer object: {err:?}"); + stop_cast(); + return; + } + }; + let dmabuf = match bo.export() { + Ok(dmabuf) => dmabuf, + Err(err) => { + warn!("error exporting GBM buffer object as dmabuf: {err:?}"); + stop_cast(); + return; + } + }; + + let fd = dmabuf.handles().next().unwrap().as_raw_fd(); + + (*spa_data).type_ = DataType::DmaBuf.as_raw(); + (*spa_data).maxsize = dmabuf.strides().next().unwrap() * size.h as u32; + (*spa_data).fd = fd as i64; + (*spa_data).flags = SPA_DATA_FLAG_READWRITE; + + assert!(dmabufs.borrow_mut().insert(fd, dmabuf).is_none()); + } + } + }) + .remove_buffer({ + let dmabufs = dmabufs.clone(); + move |buffer| { + trace!("pw stream: remove_buffer"); + + unsafe { + let spa_buffer = (*buffer).buffer; + let spa_data = (*spa_buffer).datas; + assert!((*spa_buffer).n_datas > 0); + + let fd = (*spa_data).fd as i32; + dmabufs.borrow_mut().remove(&fd); + } + } + }) + .register() + .unwrap(); + + let object = pod::object!( + SpaTypes::ObjectParamFormat, + ParamType::EnumFormat, + pod::property!(FormatProperties::MediaType, Id, MediaType::Video), + pod::property!(FormatProperties::MediaSubtype, Id, MediaSubtype::Raw), + pod::property!(FormatProperties::VideoFormat, Id, VideoFormat::BGRx), + Property { + key: FormatProperties::VideoModifier.as_raw(), + value: pod::Value::Long(u64::from(Modifier::Invalid) as i64), + flags: PropertyFlags::MANDATORY, + }, + pod::property!( + FormatProperties::VideoSize, + Rectangle, + Rectangle { + width: size.w as u32, + height: size.h as u32, + } + ), + pod::property!( + FormatProperties::VideoFramerate, + Fraction, + Fraction { num: 0, denom: 1 } + ), + pod::property!( + FormatProperties::VideoMaxFramerate, + Choice, + Range, + Fraction, + Fraction { + num: refresh as u32, + denom: 1000 + }, + Fraction { num: 1, denom: 1 }, + Fraction { + num: refresh as u32, + denom: 1000 + } + ), + ); + + let mut buffer = vec![]; + let mut params = [make_pod(&mut buffer, object)]; + stream + .connect( + Direction::Output, + None, + StreamFlags::DRIVER | StreamFlags::ALLOC_BUFFERS, + &mut params, + ) + .context("error connecting stream")?; + + let cast = Cast { + session_id, + stream, + _listener: listener, + is_active, + output, + cursor_mode, + last_frame_time: Duration::ZERO, + min_time_between_frames, + dmabufs, + }; + Ok(cast) + } +} + +fn make_pod(buffer: &mut Vec, object: pod::Object) -> &Pod { + PodSerializer::serialize(Cursor::new(&mut *buffer), &pod::Value::Object(object)).unwrap(); + Pod::from_bytes(buffer).unwrap() +} -- cgit