Implement initial window screencasting

This commit is contained in:
Ivan Molodetskikh
2024-06-27 11:36:24 +04:00
parent 0757ad08e7
commit dc92d80b9f
6 changed files with 694 additions and 148 deletions
+1 -4
View File
@@ -87,11 +87,8 @@ impl DBusServers {
let (to_niri, from_screen_cast) = calloop::channel::channel(); let (to_niri, from_screen_cast) = calloop::channel::channel();
niri.event_loop niri.event_loop
.insert_source(from_screen_cast, { .insert_source(from_screen_cast, {
let to_niri = to_niri.clone();
move |event, _, state| match event { move |event, _, state| match event {
calloop::channel::Event::Msg(msg) => { calloop::channel::Event::Msg(msg) => state.on_screen_cast_msg(msg),
state.on_screen_cast_msg(&to_niri, msg)
}
calloop::channel::Event::Closed => (), calloop::channel::Event::Closed => (),
} }
}) })
+96 -16
View File
@@ -4,7 +4,6 @@ use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use serde::Deserialize; use serde::Deserialize;
use smithay::output::Output;
use zbus::fdo::RequestNameFlags; use zbus::fdo::RequestNameFlags;
use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value}; use zbus::zvariant::{DeserializeDict, OwnedObjectPath, SerializeDict, Type, Value};
use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext}; use zbus::{dbus_interface, fdo, InterfaceRef, ObjectServer, SignalContext};
@@ -47,15 +46,40 @@ struct RecordMonitorProperties {
_is_recording: Option<bool>, _is_recording: Option<bool>,
} }
#[derive(Debug, DeserializeDict, Type)]
#[zvariant(signature = "dict")]
struct RecordWindowProperties {
#[zvariant(rename = "window-id")]
window_id: u64,
#[zvariant(rename = "cursor-mode")]
cursor_mode: Option<CursorMode>,
#[zvariant(rename = "is-recording")]
_is_recording: Option<bool>,
}
static STREAM_ID: AtomicUsize = AtomicUsize::new(0);
#[derive(Clone)] #[derive(Clone)]
pub struct Stream { pub struct Stream {
// FIXME: update on scale changes and whatnot. target: StreamTarget,
output: niri_ipc::Output,
cursor_mode: CursorMode, cursor_mode: CursorMode,
was_started: Arc<AtomicBool>, was_started: Arc<AtomicBool>,
to_niri: calloop::channel::Sender<ScreenCastToNiri>, to_niri: calloop::channel::Sender<ScreenCastToNiri>,
} }
#[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)] #[derive(Debug, SerializeDict, Type, Value)]
#[zvariant(signature = "dict")] #[zvariant(signature = "dict")]
struct StreamParameters { struct StreamParameters {
@@ -68,14 +92,13 @@ struct StreamParameters {
pub enum ScreenCastToNiri { pub enum ScreenCastToNiri {
StartCast { StartCast {
session_id: usize, session_id: usize,
output: String, target: StreamTargetId,
cursor_mode: CursorMode, cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>, signal_ctx: SignalContext<'static>,
}, },
StopCast { StopCast {
session_id: usize, session_id: usize,
}, },
Redraw(Output),
} }
#[dbus_interface(name = "org.gnome.Mutter.ScreenCast")] #[dbus_interface(name = "org.gnome.Mutter.ScreenCast")]
@@ -176,16 +199,51 @@ impl Session {
return Err(fdo::Error::Failed("monitor is disabled".to_owned())); return Err(fdo::Error::Failed("monitor is disabled".to_owned()));
} }
static NUMBER: AtomicUsize = AtomicUsize::new(0);
let path = format!( let path = format!(
"/org/gnome/Mutter/ScreenCast/Stream/u{}", "/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 path = OwnedObjectPath::try_from(path).unwrap();
let cursor_mode = properties.cursor_mode.unwrap_or_default(); let cursor_mode = properties.cursor_mode.unwrap_or_default();
let stream = Stream::new(output.clone(), cursor_mode, self.to_niri.clone()); 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<OwnedObjectPath> {
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 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 { match server.at(&path, stream.clone()).await {
Ok(true) => { Ok(true) => {
let iface = server.interface(&path).await.unwrap(); let iface = server.interface(&path).await.unwrap();
@@ -214,10 +272,21 @@ impl Stream {
#[dbus_interface(property)] #[dbus_interface(property)]
async fn parameters(&self) -> StreamParameters { async fn parameters(&self) -> StreamParameters {
let logical = self.output.logical.as_ref().unwrap(); match &self.target {
StreamParameters { StreamTarget::Output(output) => {
position: (logical.x, logical.y), let logical = output.logical.as_ref().unwrap();
size: (logical.width as i32, logical.height as i32), 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 { impl Stream {
pub fn new( fn new(
output: niri_ipc::Output, target: StreamTarget,
cursor_mode: CursorMode, cursor_mode: CursorMode,
to_niri: calloop::channel::Sender<ScreenCastToNiri>, to_niri: calloop::channel::Sender<ScreenCastToNiri>,
) -> Self { ) -> Self {
Self { Self {
output, target,
cursor_mode, cursor_mode,
was_started: Arc::new(AtomicBool::new(false)), was_started: Arc::new(AtomicBool::new(false)),
to_niri, to_niri,
@@ -295,7 +364,7 @@ impl Stream {
let msg = ScreenCastToNiri::StartCast { let msg = ScreenCastToNiri::StartCast {
session_id, session_id,
output: self.output.name.clone(), target: self.target.make_id(),
cursor_mode: self.cursor_mode, cursor_mode: self.cursor_mode,
signal_ctx: ctxt, 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 },
}
}
}
+9
View File
@@ -209,6 +209,9 @@ impl CompositorHandler for State {
let window = mapped.window.clone(); let window = mapped.window.clone();
let output = output.clone(); let output = output.clone();
#[cfg(feature = "xdp-gnome-screencast")]
let id = mapped.id();
// This is a commit of a previously-mapped toplevel. // This is a commit of a previously-mapped toplevel.
let is_mapped = let is_mapped =
with_renderer_surface_state(surface, |state| state.buffer().is_some()) 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 active_window = self.niri.layout.active_window().map(|(m, _)| &m.window);
let was_active = active_window == Some(&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); self.niri.layout.remove_window(&window);
if was_active { if was_active {
+6
View File
@@ -466,6 +466,12 @@ impl XdgShellHandler for State {
let window = mapped.window.clone(); let window = mapped.window.clone();
let output = output.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.backend.with_primary_renderer(|renderer| {
self.niri.layout.store_unmap_snapshot(renderer, &window); self.niri.layout.store_unmap_snapshot(renderer, &window);
}); });
+360 -88
View File
@@ -116,6 +116,8 @@ use crate::protocols::foreign_toplevel::{self, ForeignToplevelManagerState};
use crate::protocols::gamma_control::GammaControlManagerState; use crate::protocols::gamma_control::GammaControlManagerState;
use crate::protocols::screencopy::{Screencopy, ScreencopyManagerState}; use crate::protocols::screencopy::{Screencopy, ScreencopyManagerState};
use crate::pw_utils::{Cast, PipeWire}; 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::debug::draw_opaque_regions;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement; use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::renderer::NiriRenderer; use crate::render_helpers::renderer::NiriRenderer;
@@ -277,6 +279,10 @@ pub struct Niri {
// Casts are dropped before PipeWire to prevent a double-free (yay). // Casts are dropped before PipeWire to prevent a double-free (yay).
pub casts: Vec<Cast>, pub casts: Vec<Cast>,
pub pipewire: Option<PipeWire>, pub pipewire: Option<PipeWire>,
// Screencast output for each mapped window.
#[cfg(feature = "xdp-gnome-screencast")]
pub mapped_cast_output: HashMap<Window, Output>,
} }
pub struct OutputState { pub struct OutputState {
@@ -508,6 +514,9 @@ impl State {
foreign_toplevel::refresh(self); foreign_toplevel::refresh(self);
self.niri.refresh_window_rules(); self.niri.refresh_window_rules();
self.refresh_ipc_outputs(); self.refresh_ipc_outputs();
#[cfg(feature = "xdp-gnome-screencast")]
self.niri.refresh_mapped_cast_outputs();
} }
pub fn move_cursor(&mut self, location: Point<f64, Logical>) { pub fn move_cursor(&mut self, location: Point<f64, Logical>) {
@@ -1172,15 +1181,34 @@ impl State {
} }
#[cfg(feature = "xdp-gnome-screencast")] #[cfg(feature = "xdp-gnome-screencast")]
pub fn on_screen_cast_msg( pub fn on_pw_msg(&mut self, msg: PwToNiri) {
&mut self, match msg {
to_niri: &calloop::channel::Sender<ScreenCastToNiri>, PwToNiri::StopCast { session_id } => self.niri.stop_cast(session_id),
msg: ScreenCastToNiri, 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 { match msg {
ScreenCastToNiri::StartCast { ScreenCastToNiri::StartCast {
session_id, session_id,
output, target,
cursor_mode, cursor_mode,
signal_ctx, signal_ctx,
} => { } => {
@@ -1201,25 +1229,65 @@ impl State {
return; return;
}; };
let Some(output) = self let (target, size, refresh) = match target {
.niri StreamTargetId::Output { name } => {
.global_space let global_space = &self.niri.global_space;
.outputs() let output = global_space.outputs().find(|out| out.name() == name);
.find(|out| out.name() == output) let Some(output) = output else {
.cloned() warn!("error starting screencast: requested output is missing");
else { self.niri.stop_cast(session_id);
warn!("tried to start a screencast on missing output: {output}"); return;
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( let res = pw.start_cast(
to_niri.clone(),
gbm, gbm,
session_id, session_id,
output, target,
size,
refresh,
cursor_mode, cursor_mode,
signal_ctx, signal_ctx,
) { );
match res {
Ok(cast) => { Ok(cast) => {
self.niri.casts.push(cast); self.niri.casts.push(cast);
} }
@@ -1230,7 +1298,6 @@ impl State {
} }
} }
ScreenCastToNiri::StopCast { session_id } => self.niri.stop_cast(session_id), 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, pipewire,
casts: vec![], 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), 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 // Disable the output global and remove some time later to give the clients some time to
// process it. // process it.
let global = state.global; 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<R: NiriRenderer>( pub fn render<R: NiriRenderer>(
&self, &self,
renderer: &mut R, renderer: &mut R,
@@ -2859,6 +2980,11 @@ impl Niri {
backend.with_primary_renderer(|renderer| { backend.with_primary_renderer(|renderer| {
// Render and send to PipeWire screencast streams. // Render and send to PipeWire screencast streams.
self.render_for_screen_cast(renderer, output, target_presentation_time); 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, output: &Output,
target_presentation_time: Duration, target_presentation_time: Duration,
) { ) {
use crate::render_helpers::render_to_dmabuf;
let _span = tracy_client::span!("Niri::render_for_screen_cast"); let _span = tracy_client::span!("Niri::render_for_screen_cast");
let target = CastTarget::Output(output.downgrade());
let size = output.current_mode().unwrap().size; let size = output.current_mode().unwrap().size;
let transform = output.current_transform(); let transform = output.current_transform();
let size = transform.transform_size(size); let size = transform.transform_size(size);
@@ -3313,81 +3439,32 @@ impl Niri {
continue; continue;
} }
if &cast.output != output { if cast.target != target {
continue; continue;
} }
if cast.size.get() != size { match cast.ensure_size(size) {
if cast.pending_size.get() != size { Ok(CastSizeChange::Ready) => (),
debug!("output size changed, updating stream size"); Ok(CastSizeChange::Pending) => continue,
if let Err(err) = cast.set_size(size) { Err(err) => {
warn!("error updating stream size, stopping screencast: {err:?}"); warn!("error updating stream size, stopping screencast: {err:?}");
casts_to_stop.push(cast.session_id); casts_to_stop.push(cast.session_id);
}
} else {
debug!("stream size still hasn't changed, skipping frame");
} }
}
// Even in the successful case, we'll need to wait till the size actually changes. if cast.should_skip_frame(target_presentation_time) {
continue; continue;
} }
let last = cast.last_frame_time; // FIXME: Hidden / embedded / metadata cursor
let min = cast.min_time_between_frames.get(); let elements = elements.get_or_insert_with(|| {
if last.is_zero() { self.render(renderer, output, true, RenderTarget::Screencast)
trace!(?target_presentation_time, ?last, "last is zero, recording"); });
} else if target_presentation_time < last { let elements = elements.iter().rev();
// Record frame with a warning; in case it was an overflow this will fix it.
warn!( if cast.dequeue_buffer_and_render(renderer, elements, size, scale) {
?target_presentation_time, cast.last_frame_time = 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;
}
} }
{
let mut buffer = match cast.stream.dequeue_buffer() {
Some(buffer) => buffer,
None => {
warn!("no available buffer in pw stream, skipping frame");
continue;
}
};
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
let elements = elements.get_or_insert_with(|| {
self.render::<GlesRenderer>(renderer, output, true, RenderTarget::Screencast)
});
let elements = elements.iter().rev();
if let Err(err) =
render_to_dmabuf(renderer, dmabuf, size, scale, Transform::Normal, elements)
{
warn!("error rendering to dmabuf: {err:?}");
continue;
}
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 = target_presentation_time;
} }
self.casts = casts; self.casts = casts;
@@ -3396,6 +3473,184 @@ impl Niri {
} }
} }
#[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);
}
}
if cast.should_skip_frame(target_presentation_time) {
continue;
}
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)
});
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_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);
}
}
if cast.should_skip_frame(target_presentation_time) {
continue;
}
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);
}
}
pub fn render_for_screencopy( pub fn render_for_screencopy(
&mut self, &mut self,
backend: &mut Backend, backend: &mut Backend,
@@ -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) { pub fn debug_toggle_damage(&mut self) {
self.debug_draw_damage = !self.debug_draw_damage; self.debug_draw_damage = !self.debug_draw_damage;
+222 -40
View File
@@ -27,19 +27,28 @@ use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
use smithay::backend::allocator::gbm::{GbmBuffer, GbmBufferFlags, GbmDevice}; use smithay::backend::allocator::gbm::{GbmBuffer, GbmBufferFlags, GbmDevice};
use smithay::backend::allocator::Fourcc; use smithay::backend::allocator::Fourcc;
use smithay::backend::drm::DrmDeviceFd; 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::generic::Generic;
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction}; use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
use smithay::reexports::gbm::Modifier; use smithay::reexports::gbm::Modifier;
use smithay::utils::{Physical, Size}; use smithay::utils::{Physical, Scale, Size, Transform};
use zbus::SignalContext; 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::niri::State;
use crate::render_helpers::render_to_dmabuf;
pub struct PipeWire { pub struct PipeWire {
_context: Context, _context: Context,
pub core: Core, pub core: Core,
to_niri: calloop::channel::Sender<PwToNiri>,
}
pub enum PwToNiri {
StopCast { session_id: usize },
Redraw(CastTarget),
} }
pub struct Cast { pub struct Cast {
@@ -47,9 +56,8 @@ pub struct Cast {
pub stream: Stream, pub stream: Stream,
_listener: StreamListener<()>, _listener: StreamListener<()>,
pub is_active: Rc<Cell<bool>>, pub is_active: Rc<Cell<bool>>,
pub output: Output, pub target: CastTarget,
pub size: Rc<Cell<Size<i32, Physical>>>, pub size: Rc<Cell<CastSize>>,
pub pending_size: Rc<Cell<Size<i32, Physical>>>,
pub refresh: u32, pub refresh: u32,
pub cursor_mode: CursorMode, pub cursor_mode: CursorMode,
pub last_frame_time: Duration, pub last_frame_time: Duration,
@@ -57,6 +65,28 @@ pub struct Cast {
pub dmabufs: Rc<RefCell<HashMap<i32, Dmabuf>>>, pub dmabufs: Rc<RefCell<HashMap<i32, Dmabuf>>>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CastSize {
InitialPending(Size<i32, Physical>),
Ready(Size<i32, Physical>),
ChangePending {
last_negotiated: Size<i32, Physical>,
pending: Size<i32, Physical>,
},
}
#[derive(PartialEq, Eq)]
pub enum CastSizeChange {
Ready,
Pending,
}
#[derive(Clone, PartialEq, Eq)]
pub enum CastTarget {
Output(WeakOutput),
Window { id: u64 },
}
impl PipeWire { impl PipeWire {
pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> { pub fn new(event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
let main_loop = MainLoop::new(None).context("error creating MainLoop")?; let main_loop = MainLoop::new(None).context("error creating MainLoop")?;
@@ -86,45 +116,49 @@ impl PipeWire {
}) })
.unwrap(); .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 { Ok(Self {
_context: context, _context: context,
core, core,
to_niri,
}) })
} }
#[allow(clippy::too_many_arguments)]
pub fn start_cast( pub fn start_cast(
&self, &self,
to_niri: calloop::channel::Sender<ScreenCastToNiri>,
gbm: GbmDevice<DrmDeviceFd>, gbm: GbmDevice<DrmDeviceFd>,
session_id: usize, session_id: usize,
output: Output, target: CastTarget,
size: Size<i32, Physical>,
refresh: u32,
cursor_mode: CursorMode, cursor_mode: CursorMode,
signal_ctx: SignalContext<'static>, signal_ctx: SignalContext<'static>,
) -> anyhow::Result<Cast> { ) -> anyhow::Result<Cast> {
let _span = tracy_client::span!("PipeWire::start_cast"); 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 || { 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:?}"); 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 || { let redraw = move || {
if let Some(output) = weak.upgrade() { if let Err(err) = to_niri_.send(PwToNiri::Redraw(target_.clone())) {
if let Err(err) = to_niri.send(ScreenCastToNiri::Redraw(output)) { warn!("error sending Redraw to niri: {err:?}");
warn!("error sending Redraw to niri: {err:?}");
}
} }
}; };
let redraw_ = redraw.clone(); 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()) let stream = Stream::new(&self.core, "niri-screen-cast-src", Properties::new())
.context("error creating Stream")?; .context("error creating Stream")?;
@@ -133,8 +167,9 @@ impl PipeWire {
let is_active = Rc::new(Cell::new(false)); let is_active = Rc::new(Cell::new(false));
let min_time_between_frames = Rc::new(Cell::new(Duration::ZERO)); let min_time_between_frames = Rc::new(Cell::new(Duration::ZERO));
let dmabufs = Rc::new(RefCell::new(HashMap::new())); 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 let listener = stream
.add_local_listener_with_user_data(()) .add_local_listener_with_user_data(())
@@ -186,7 +221,6 @@ impl PipeWire {
.param_changed({ .param_changed({
let min_time_between_frames = min_time_between_frames.clone(); let min_time_between_frames = min_time_between_frames.clone();
let size = size.clone(); let size = size.clone();
let pending_size = pending_size.clone();
move |stream, (), id, pod| { move |stream, (), id, pod| {
let id = ParamType::from_raw(id); let id = ParamType::from_raw(id);
trace!(?id, "pw stream: param_changed"); trace!(?id, "pw stream: param_changed");
@@ -213,9 +247,18 @@ impl PipeWire {
format.parse(pod).unwrap(); format.parse(pod).unwrap();
trace!("pw stream: got format = {format:?}"); trace!("pw stream: got format = {format:?}");
assert_eq!(pending_size.get().w as u32, format.size().width); let expected_size = size.get().expected_format_size();
assert_eq!(pending_size.get().h as u32, format.size().height); let format_size =
size.set(pending_size.get()); 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(); let max_frame_rate = format.max_framerate();
// Subtract 0.5 ms to improve edge cases when equal to refresh rate. // 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 stop_cast = stop_cast.clone();
let size = size.clone(); let size = size.clone();
move |stream, (), buffer| { 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 { unsafe {
let spa_buffer = (*buffer).buffer; let spa_buffer = (*buffer).buffer;
@@ -291,8 +336,6 @@ impl PipeWire {
assert!((*spa_buffer).n_datas > 0); assert!((*spa_buffer).n_datas > 0);
assert!((*spa_data).type_ & (1 << DataType::DmaBuf.as_raw()) > 0); assert!((*spa_data).type_ & (1 << DataType::DmaBuf.as_raw()) > 0);
let size = size.get();
let bo = match gbm.create_buffer_object::<()>( let bo = match gbm.create_buffer_object::<()>(
size.w as u32, size.w as u32,
size.h as u32, size.h as u32,
@@ -351,7 +394,9 @@ impl PipeWire {
.register() .register()
.unwrap(); .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 buffer = vec![];
let mut params = [make_pod(&mut buffer, object)]; let mut params = [make_pod(&mut buffer, object)];
stream stream
@@ -368,9 +413,8 @@ impl PipeWire {
stream, stream,
_listener: listener, _listener: listener,
is_active, is_active,
output, target,
size, size,
pending_size,
refresh, refresh,
cursor_mode, cursor_mode,
last_frame_time: Duration::ZERO, last_frame_time: Duration::ZERO,
@@ -382,22 +426,160 @@ impl PipeWire {
} }
impl Cast { impl Cast {
pub fn set_size(&self, size: Size<i32, Physical>) -> anyhow::Result<()> { pub fn ensure_size(&self, size: Size<i32, Physical>) -> anyhow::Result<CastSizeChange> {
if self.pending_size.get() == size { let current_size = self.size.get();
return Ok(()); 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))); let _span = tracy_client::span!("Cast::ensure_size");
self.pending_size.set(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 object = make_video_params(size, self.refresh);
let mut buffer = vec![]; let mut buffer = vec![];
let mut params = [make_pod(&mut buffer, object)]; let mut params = [make_pod(&mut buffer, object)];
self.stream self.stream
.update_params(&mut params) .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<Item = impl RenderElement<GlesRenderer>>,
size: Size<i32, Physical>,
scale: Scale<f64>,
) -> 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<Size<i32, Physical>> {
match self {
CastSize::InitialPending(pending) => Some(pending),
CastSize::Ready(_) => None,
CastSize::ChangePending { pending, .. } => Some(pending),
}
}
fn negotiated_size(self) -> Option<Size<i32, Physical>> {
match self {
CastSize::InitialPending(_) => None,
CastSize::Ready(size) => Some(size),
CastSize::ChangePending {
last_negotiated, ..
} => Some(last_negotiated),
}
}
fn expected_format_size(self) -> Size<i32, Physical> {
match self {
CastSize::InitialPending(pending) => pending,
CastSize::Ready(size) => size,
CastSize::ChangePending { pending, .. } => pending,
}
}
fn with_pending(self, pending: Size<i32, Physical>) -> 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,
},
}
} }
} }