diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/backend/mod.rs | 19 | ||||
| -rw-r--r-- | src/backend/tty.rs | 24 | ||||
| -rw-r--r-- | src/backend/winit.rs | 13 | ||||
| -rw-r--r-- | src/config.rs | 4 | ||||
| -rw-r--r-- | src/dbus/mod.rs | 2 | ||||
| -rw-r--r-- | src/dbus/mutter_display_config.rs | 88 | ||||
| -rw-r--r-- | src/dbus/mutter_screen_cast.rs | 256 | ||||
| -rw-r--r-- | src/main.rs | 1 | ||||
| -rw-r--r-- | src/niri.rs | 329 | ||||
| -rw-r--r-- | src/pw_utils.rs | 382 |
10 files changed, 1069 insertions, 49 deletions
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<Mutex<HashMap<String, Output>>> { + match self { + Backend::Tty(tty) => tty.connectors(), + Backend::Winit(winit) => winit.connectors(), + } + } + + pub fn gbm_device(&self) -> Option<GbmDevice<DrmDeviceFd>> { + 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<OutputDevice>, + connectors: Arc<Mutex<HashMap<String, Output>>>, } 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<Mutex<HashMap<String, Output>>> { + self.connectors.clone() + } + + pub fn gbm_device(&self) -> Option<GbmDevice<DrmDeviceFd>> { + 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<GlesRenderer>, damage_tracker: OutputDamageTracker, + connectors: Arc<Mutex<HashMap<String, Output>>>, } 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<Mutex<HashMap<String, Output>>> { + 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<Mutex<HashMap<String, Output>>>, +} + +#[derive(Serialize, Type)] +pub struct Monitor { + names: (String, String, String, String), + modes: Vec<Mode>, + properties: HashMap<String, OwnedValue>, +} + +#[derive(Serialize, Type)] +pub struct Mode { + id: String, + width: i32, + height: i32, + refresh_rate: f64, + preferred_scale: f64, + supported_scales: Vec<f64>, + properties: HashMap<String, OwnedValue>, +} + +#[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<String, OwnedValue>, +} + +#[dbus_interface(name = "org.gnome.Mutter.DisplayConfig")] +impl DisplayConfig { + async fn get_current_state( + &self, + ) -> fdo::Result<( + u32, + Vec<Monitor>, + Vec<LogicalMonitor>, + HashMap<String, OwnedValue>, + )> { + // Construct the DBus response. + let monitors: Vec<Monitor> = 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<Mutex<HashMap<String, Output>>>) -> 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<Mutex<HashMap<String, Output>>>, + to_niri: calloop::channel::Sender<ToNiriMsg>, + sessions: Arc<Mutex<Vec<(Session, InterfaceRef<Session>)>>>, +} + +#[derive(Clone)] +pub struct Session { + id: usize, + connectors: Arc<Mutex<HashMap<String, Output>>>, + to_niri: calloop::channel::Sender<ToNiriMsg>, + streams: Arc<Mutex<Vec<(Stream, InterfaceRef<Stream>)>>>, +} + +#[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<CursorMode>, + #[zvariant(rename = "is-recording")] + _is_recording: Option<bool>, +} + +#[derive(Clone)] +pub struct Stream { + output: Output, + cursor_mode: CursorMode, + was_started: Arc<AtomicBool>, + to_niri: calloop::channel::Sender<ToNiriMsg>, +} + +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<OwnedObjectPath> { + 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::<Stream, _>(iface.signal_context().path()) + .await + .unwrap(); + } + + server.remove::<Session, _>(ctxt.path()).await.unwrap(); + } + + async fn record_monitor( + &mut self, + #[zbus(object_server)] server: &ObjectServer, + connector: &str, + properties: RecordMonitorProperties, + ) -> fdo::Result<OwnedObjectPath> { + 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<Mutex<HashMap<String, Output>>>, + to_niri: calloop::channel::Sender<ToNiriMsg>, + ) -> Self { + Self { + connectors, + to_niri, + sessions: Arc::new(Mutex::new(vec![])), + } + } +} + +impl Session { + pub fn new( + id: usize, + connectors: Arc<Mutex<HashMap<String, Output>>>, + to_niri: calloop::channel::Sender<ToNiriMsg>, + ) -> 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<ToNiriMsg>, + ) -> 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<zbus::blocking::Connection>, pub inhibit_power_key_fd: Option<zbus::zvariant::OwnedFd>, + pub screen_cast: ScreenCast, + + // Casts are dropped before PipeWire to prevent a double-free (yay). + pub casts: Vec<Cast>, + pub pipewire: Option<PipeWire>, } 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<State>, - seat_name: String, + backend: &Backend, ) -> Self { let display_handle = display.handle(); @@ -215,7 +218,7 @@ impl Niri { let presentation_state = PresentationState::new::<State>(&display_handle, CLOCK_MONOTONIC as u32); - let mut seat: Seat<State> = seat_state.new_wl_seat(&display_handle, seat_name); + let mut seat: Seat<State> = 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<GlesRenderer>], + 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<i32, Physical>, + elements: &[OutputRenderElements<GlesRenderer>], +) -> anyhow::Result<GlesMapping> { + 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")?; |
