Move xdp-gnome-screencast code into separate module

This commit is contained in:
Ivan Molodetskikh
2026-01-12 07:14:24 +03:00
parent fd2916eb72
commit afe27a143b
5 changed files with 686 additions and 678 deletions
-13
View File
@@ -1,13 +0,0 @@
use anyhow::bail;
use smithay::reexports::calloop::LoopHandle;
use crate::niri::State;
pub struct PipeWire;
pub struct Cast;
impl PipeWire {
pub fn new(_event_loop: &LoopHandle<'static, State>) -> anyhow::Result<Self> {
bail!("PipeWire support is disabled (see \"xdp-gnome-screencast\" feature)");
}
}
+2 -8
View File
@@ -19,17 +19,11 @@ pub mod niri;
pub mod protocols;
pub mod render_helpers;
pub mod rubber_band;
#[cfg(feature = "xdp-gnome-screencast")]
pub mod screencasting;
pub mod ui;
pub mod utils;
pub mod window;
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub mod dummy_pw_utils;
#[cfg(feature = "xdp-gnome-screencast")]
pub mod pw_utils;
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub use dummy_pw_utils as pw_utils;
#[cfg(test)]
mod tests;
+7 -655
View File
@@ -123,8 +123,6 @@ use crate::dbus::freedesktop_login1::Login1ToNiri;
use crate::dbus::gnome_shell_introspect::{self, IntrospectToNiri, NiriToIntrospect};
#[cfg(feature = "dbus")]
use crate::dbus::gnome_shell_screenshot::{NiriToScreenshot, ScreenshotToNiri};
#[cfg(feature = "xdp-gnome-screencast")]
use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri};
use crate::frame_clock::FrameClock;
use crate::handlers::{configure_lock_surface, XDG_ACTIVATION_TOKEN_TIMEOUT};
use crate::input::pick_color_grab::PickColorGrab;
@@ -150,9 +148,6 @@ use crate::protocols::mutter_x11_interop::MutterX11InteropManagerState;
use crate::protocols::output_management::OutputManagementManagerState;
use crate::protocols::screencopy::{Screencopy, ScreencopyBuffer, ScreencopyManagerState};
use crate::protocols::virtual_pointer::VirtualPointerManagerState;
use crate::pw_utils::{Cast, PipeWire};
#[cfg(feature = "xdp-gnome-screencast")]
use crate::pw_utils::{CastSizeChange, CursorData, PwToNiri};
use crate::render_helpers::debug::draw_opaque_regions;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
@@ -163,6 +158,8 @@ use crate::render_helpers::{
encompassing_geo, render_to_dmabuf, render_to_encompassing_texture, render_to_shm,
render_to_texture, render_to_vec, shaders, RenderTarget,
};
#[cfg(feature = "xdp-gnome-screencast")]
use crate::screencasting::Screencasting;
use crate::ui::config_error_notification::ConfigErrorNotification;
use crate::ui::exit_confirm_dialog::{ExitConfirmDialog, ExitConfirmDialogRenderElement};
use crate::ui::hotkey_overlay::HotkeyOverlay;
@@ -179,7 +176,7 @@ use crate::utils::{
logical_output, make_screenshot_path, output_matches_name, output_size, panel_orientation,
send_scale_transform, write_png_rgba8, xwayland,
};
use crate::window::mapped::{MappedId, WindowCastRenderElements};
use crate::window::mapped::MappedId;
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped, WindowRef};
const CLEAR_COLOR_LOCKED: [f32; 4] = [0.3, 0.1, 0.1, 1.];
@@ -409,19 +406,8 @@ pub struct Niri {
pub satellite: Option<Satellite>,
// Casts are dropped before PipeWire to prevent a double-free (yay).
pub casts: Vec<Cast>,
pub pipewire: Option<PipeWire>,
#[cfg(feature = "xdp-gnome-screencast")]
pub pw_to_niri: calloop::channel::Sender<PwToNiri>,
// Screencast output for each mapped window.
#[cfg(feature = "xdp-gnome-screencast")]
pub mapped_cast_output: HashMap<Window, Output>,
/// Window ID for the "dynamic cast" special window for the xdp-gnome picker.
#[cfg(feature = "xdp-gnome-screencast")]
pub dynamic_cast_id_for_portal: MappedId,
pub casting: Screencasting,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -1997,326 +1983,9 @@ impl State {
self.niri.queue_redraw_all();
}
#[cfg(feature = "xdp-gnome-screencast")]
pub fn on_pw_msg(&mut self, msg: PwToNiri) {
match msg {
PwToNiri::StopCast { session_id } => self.niri.stop_cast(session_id),
PwToNiri::Redraw { stream_id } => self.redraw_cast(stream_id),
PwToNiri::FatalError => {
warn!("stopping PipeWire due to fatal error");
if let Some(pw) = self.niri.pipewire.take() {
let ids: Vec<_> = self.niri.casts.iter().map(|cast| cast.session_id).collect();
for id in ids {
self.niri.stop_cast(id);
}
self.niri.event_loop.remove(pw.token);
}
}
}
}
#[cfg(feature = "xdp-gnome-screencast")]
fn redraw_cast(&mut self, stream_id: usize) {
let _span = tracy_client::span!("State::redraw_cast");
let casts = &mut self.niri.casts;
let Some(idx) = casts.iter().position(|cast| cast.stream_id == stream_id) else {
warn!("cast to redraw is missing");
return;
};
let cast = &mut casts[idx];
let id = match &cast.target {
CastTarget::Nothing => {
self.backend.with_primary_renderer(|renderer| {
if cast.dequeue_buffer_and_clear(renderer) {
cast.last_frame_time = get_monotonic_time();
}
});
return;
}
CastTarget::Output(weak) => {
if let Some(output) = weak.upgrade() {
self.niri.queue_redraw(&output);
}
return;
}
CastTarget::Window { id } => *id,
};
// Lack of partial borrowing strikes again...
let mut casts = mem::take(&mut self.niri.casts);
let cast = &mut casts[idx];
let mut stop = false;
// Use a loop {} so we can break instead of early-return.
#[allow(clippy::never_loop)]
loop {
let mut windows = self.niri.layout.windows();
let Some((_, mapped)) = windows.find(|(_, mapped)| mapped.id().get() == id) else {
break;
};
// 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(&mapped.window) else {
break;
};
let scale = Scale::from(output.current_scale().fractional_scale());
let bbox = mapped
.window
.bbox_with_popups()
.to_physical_precise_up(scale);
match cast.ensure_size(bbox.size) {
Ok(CastSizeChange::Ready) => (),
Ok(CastSizeChange::Pending) => break,
Err(err) => {
warn!("error updating stream size, stopping screencast: {err:?}");
stop = true;
break;
}
}
self.backend.with_primary_renderer(|renderer| {
let mut elements = Vec::new();
mapped.render_for_screen_cast(renderer, scale, &mut |elem| {
elements.push(CastRenderElement::from(elem))
});
let mut pointer_elements = Vec::new();
let mut pointer_location = Point::default();
if self.niri.pointer_visibility.is_visible() {
if let Some((pointer_pos, win_pos)) =
self.niri.pointer_pos_for_window_cast(mapped)
{
// Pointer location must be relative to the screencast buffer.
// - win_pos is the position of the main window surface in output-local
// coordinates
// - bbox.loc moves us relative to the screencast buffer
let buf_pos = win_pos + bbox.loc.to_f64().to_logical(scale);
let output_pos =
self.niri.global_space.output_geometry(output).unwrap().loc;
pointer_location = pointer_pos - output_pos.to_f64() - buf_pos;
let pos = buf_pos.to_physical_precise_round(scale).upscale(-1);
self.niri.render_pointer(renderer, output, &mut |elem| {
let elem =
RelocateRenderElement::from_element(elem, pos, Relocate::Relative);
pointer_elements.push(CastRenderElement::from(elem));
});
}
}
let cursor_data = CursorData::compute(&pointer_elements, pointer_location, scale);
if cast.dequeue_buffer_and_render(
renderer,
&elements,
&cursor_data,
bbox.size,
scale,
) {
cast.last_frame_time = get_monotonic_time();
}
});
break;
}
let session_id = cast.session_id;
self.niri.casts = casts;
if stop {
self.niri.stop_cast(session_id);
}
}
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub fn set_dynamic_cast_target(&mut self, _target: CastTarget) {}
#[cfg(feature = "xdp-gnome-screencast")]
pub fn set_dynamic_cast_target(&mut self, target: CastTarget) {
let _span = tracy_client::span!("State::set_dynamic_cast_target");
let mut refresh = None;
match &target {
// Leave refresh as is when clearing. Chances are, the next refresh will match it,
// then we'll avoid reconfiguring.
CastTarget::Nothing => (),
CastTarget::Output(output) => {
if let Some(output) = output.upgrade() {
refresh = Some(output.current_mode().unwrap().refresh as u32);
}
}
CastTarget::Window { id } => {
let mut windows = self.niri.layout.windows();
if let Some((_, mapped)) = windows.find(|(_, mapped)| mapped.id().get() == *id) {
if let Some(output) = self.niri.mapped_cast_output.get(&mapped.window) {
refresh = Some(output.current_mode().unwrap().refresh as u32);
}
}
}
}
let mut to_redraw = Vec::new();
let mut to_stop = Vec::new();
for cast in &mut self.niri.casts {
if !cast.dynamic_target {
continue;
}
if let Some(refresh) = refresh {
if let Err(err) = cast.set_refresh(refresh) {
warn!("error changing cast FPS: {err:?}");
to_stop.push(cast.session_id);
continue;
}
}
cast.target = target.clone();
to_redraw.push(cast.stream_id);
}
for id in to_redraw {
self.redraw_cast(id);
}
}
#[cfg(feature = "xdp-gnome-screencast")]
pub fn on_screen_cast_msg(&mut self, msg: ScreenCastToNiri) {
use smithay::reexports::gbm::Modifier;
use crate::dbus::mutter_screen_cast::StreamTargetId;
match msg {
ScreenCastToNiri::StartCast {
session_id,
stream_id,
target,
cursor_mode,
signal_ctx,
} => {
let _span = tracy_client::span!("StartCast");
debug!(session_id, stream_id, "StartCast");
let Some(gbm) = self.backend.gbm_device() else {
warn!("error starting screencast: no GBM device available");
self.niri.stop_cast(session_id);
return;
};
let pw = if let Some(pw) = &self.niri.pipewire {
pw
} else {
match PipeWire::new(self.niri.event_loop.clone(), self.niri.pw_to_niri.clone())
{
Ok(pipewire) => self.niri.pipewire.insert(pipewire),
Err(err) => {
warn!(
"error starting screencast: PipeWire failed to initialize: {err:?}"
);
self.niri.stop_cast(session_id);
return;
}
}
};
let mut dynamic_target = false;
let (target, size, refresh, alpha) = match target {
StreamTargetId::Output { name } => {
let global_space = &self.niri.global_space;
let output = global_space.outputs().find(|out| out.name() == name);
let Some(output) = output else {
warn!("error starting screencast: requested output is missing");
self.niri.stop_cast(session_id);
return;
};
let mode = output.current_mode().unwrap();
let transform = output.current_transform();
let size = transform.transform_size(mode.size);
let refresh = mode.refresh as u32;
(CastTarget::Output(output.downgrade()), size, refresh, false)
}
StreamTargetId::Window { id }
if id == self.niri.dynamic_cast_id_for_portal.get() =>
{
dynamic_target = true;
// All dynamic casts start as Nothing to avoid surprises and exposing
// sensitive info.
(CastTarget::Nothing, Size::from((1, 1)), 1000, true)
}
StreamTargetId::Window { id } => {
let Some(window) = self.niri.layout.windows().find_map(|(_, mapped)| {
(mapped.id().get() == id).then_some(&mapped.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().to_physical_precise_up(scale);
let refresh = output.current_mode().unwrap().refresh as u32;
(CastTarget::Window { id }, bbox.size, refresh, true)
}
};
let mut render_formats = self
.backend
.with_primary_renderer(|renderer| {
renderer.egl_context().dmabuf_render_formats().clone()
})
.unwrap_or_default();
{
let config = self.niri.config.borrow();
if config.debug.force_pipewire_invalid_modifier {
render_formats = render_formats
.into_iter()
.filter(|f| f.modifier == Modifier::Invalid)
.collect();
}
}
let res = pw.start_cast(
gbm,
render_formats,
session_id,
stream_id,
target,
dynamic_target,
size,
refresh,
alpha,
cursor_mode,
signal_ctx,
);
match res {
Ok(cast) => {
self.niri.casts.push(cast);
}
Err(err) => {
warn!("error starting screencast: {err:?}");
self.niri.stop_cast(session_id);
}
}
}
ScreenCastToNiri::StopCast { session_id } => self.niri.stop_cast(session_id),
}
}
#[cfg(feature = "dbus")]
pub fn on_screen_shot_msg(
&mut self,
@@ -2389,7 +2058,7 @@ impl State {
#[cfg(feature = "xdp-gnome-screencast")]
windows.insert(
self.niri.dynamic_cast_id_for_portal.get(),
self.niri.casting.dynamic_cast_id_for_portal.get(),
gnome_shell_introspect::WindowProperties {
title: String::from("niri Dynamic Cast Target"),
app_id: String::from("rs.bxt.niri.desktop"),
@@ -2682,16 +2351,7 @@ impl Niri {
};
#[cfg(feature = "xdp-gnome-screencast")]
let pw_to_niri = {
let (pw_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();
pw_to_niri
};
let screencasting = Screencasting::new(&event_loop);
let display_source = Generic::new(display, Interest::READ, Mode::Level);
event_loop
@@ -2858,16 +2518,8 @@ impl Niri {
satellite: None,
pipewire: None,
casts: vec![],
#[cfg(feature = "xdp-gnome-screencast")]
pw_to_niri,
#[cfg(feature = "xdp-gnome-screencast")]
mapped_cast_output: HashMap::new(),
#[cfg(feature = "xdp-gnome-screencast")]
dynamic_cast_id_for_portal: MappedId::next(),
casting: screencasting,
};
niri.reset_pointer_inactivity_timer();
@@ -3145,7 +2797,6 @@ impl Niri {
RedrawState::WaitingForEstimatedVBlankAndQueued(token) => self.event_loop.remove(token),
}
#[cfg(feature = "xdp-gnome-screencast")]
self.stop_casts_for_target(CastTarget::Output(output.downgrade()));
self.remove_screencopy_output(output);
@@ -4275,66 +3926,6 @@ impl Niri {
}
}
#[cfg(feature = "xdp-gnome-screencast")]
pub fn refresh_mapped_cast_window_rules(&mut self) {
// O(N^2) but should be fine since there aren't many casts usually.
self.layout.with_windows_mut(|mapped, _| {
let id = mapped.id().get();
// Find regardless of cast.is_active.
let value = self
.casts
.iter()
.any(|cast| cast.target == (CastTarget::Window { id }));
mapped.set_is_window_cast_target(value);
});
}
#[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: 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 advance_animations(&mut self) {
let _span = tracy_client::span!("Niri::advance_animations");
@@ -5280,172 +4871,6 @@ impl Niri {
feedback
}
#[cfg(feature = "xdp-gnome-screencast")]
fn render_for_screen_cast(
&mut self,
renderer: &mut GlesRenderer,
output: &Output,
target_presentation_time: Duration,
) {
let _span = tracy_client::span!("Niri::render_for_screen_cast");
let target = CastTarget::Output(output.downgrade());
let size = output.current_mode().unwrap().size;
let transform = output.current_transform();
let size = transform.transform_size(size);
let scale = Scale::from(output.current_scale().fractional_scale());
let mut elements = Vec::new();
let mut pointer = Vec::new();
let mut cursor_data = None;
let mut casts_to_stop = vec![];
let mut casts = mem::take(&mut self.casts);
for cast in &mut casts {
if !cast.is_active() {
continue;
}
if cast.target != target {
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.check_time_and_schedule(output, target_presentation_time) {
continue;
}
if cursor_data.is_none() {
// FIXME: support debug draw opaque regions.
self.render_inner(
renderer,
output,
false,
RenderTarget::Screencast,
&mut |elem| elements.push(elem.into()),
);
if self.pointer_visibility.is_visible() {
self.render_pointer(renderer, output, &mut |elem| pointer.push(elem.into()));
}
let output_pos = self.global_space.output_geometry(output).unwrap().loc;
let pointer_pos = self
.tablet_cursor_location
.unwrap_or_else(|| self.seat.get_pointer().unwrap().current_location());
let pointer_pos = pointer_pos - output_pos.to_f64();
cursor_data = Some(CursorData::compute(&pointer, pointer_pos, scale));
}
let cursor_data = cursor_data.as_ref().unwrap();
if cast.dequeue_buffer_and_render(renderer, &elements, cursor_data, size, scale) {
cast.last_frame_time = target_presentation_time;
}
}
self.casts = casts;
for id in casts_to_stop {
self.stop_cast(id);
}
}
#[cfg(feature = "xdp-gnome-screencast")]
fn render_windows_for_screen_cast(
&mut self,
renderer: &mut GlesRenderer,
output: &Output,
target_presentation_time: Duration,
) {
let _span = tracy_client::span!("Niri::render_windows_for_screen_cast");
let scale = Scale::from(output.current_scale().fractional_scale());
let mut casts_to_stop = vec![];
let mut casts = mem::take(&mut self.casts);
for cast in &mut casts {
if !cast.is_active() {
continue;
}
let CastTarget::Window { id } = cast.target else {
continue;
};
let mut windows = self.layout.windows_for_output(output);
let Some(mapped) = windows.find(|win| win.id().get() == id) else {
continue;
};
let bbox = mapped
.window
.bbox_with_popups()
.to_physical_precise_up(scale);
match cast.ensure_size(bbox.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.check_time_and_schedule(output, target_presentation_time) {
continue;
}
let mut elements = Vec::new();
mapped.render_for_screen_cast(renderer, scale, &mut |elem| {
elements.push(CastRenderElement::from(elem))
});
let mut pointer_elements = Vec::new();
let mut pointer_location = Point::default();
if self.pointer_visibility.is_visible() {
if let Some((pointer_pos, win_pos)) = self.pointer_pos_for_window_cast(mapped) {
// Pointer location must be relative to the screencast buffer.
// - win_pos is the position of the main window surface in output-local
// coordinates
// - bbox.loc moves us relative to the screencast buffer
let buf_pos = win_pos + bbox.loc.to_f64().to_logical(scale);
let output_pos = self.global_space.output_geometry(output).unwrap().loc;
pointer_location = pointer_pos - output_pos.to_f64() - buf_pos;
let pos = buf_pos.to_physical_precise_round(scale).upscale(-1);
self.render_pointer(renderer, output, &mut |elem| {
let elem =
RelocateRenderElement::from_element(elem, pos, Relocate::Relative);
pointer_elements.push(CastRenderElement::from(elem));
});
}
}
let cursor_data = CursorData::compute(&pointer_elements, pointer_location, scale);
if cast.dequeue_buffer_and_render(renderer, &elements, &cursor_data, bbox.size, scale) {
cast.last_frame_time = target_presentation_time;
}
}
self.casts = casts;
for id in casts_to_stop {
self.stop_cast(id);
}
}
pub fn render_for_screencopy_with_damage(
&mut self,
renderer: &mut GlesRenderer,
@@ -5618,73 +5043,9 @@ impl Niri {
Ok((sync, damages))
}
#[cfg(feature = "xdp-gnome-screencast")]
fn stop_cast(&mut self, session_id: usize) {
let _span = tracy_client::span!("Niri::stop_cast");
debug!(session_id, "StopCast");
for i in (0..self.casts.len()).rev() {
let cast = &self.casts[i];
if cast.session_id != session_id {
continue;
}
let cast = self.casts.swap_remove(i);
if let Err(err) = cast.stream.disconnect() {
warn!("error disconnecting stream: {err:?}");
}
}
let dbus = &self.dbus.as_ref().unwrap();
let server = dbus.conn_screen_cast.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.inner(), iface.signal_emitter().clone())
.await
});
}
}
#[cfg(not(feature = "xdp-gnome-screencast"))]
pub fn stop_casts_for_target(&mut self, _target: CastTarget) {}
#[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 mut saw_dynamic = false;
let mut ids = Vec::new();
for cast in &self.casts {
if cast.target != target {
continue;
}
if cast.dynamic_target {
saw_dynamic = true;
continue;
}
ids.push(cast.session_id);
}
for id in ids {
self.stop_cast(id);
}
// We don't stop dynamic casts, instead we switch them to Nothing.
if saw_dynamic {
self.event_loop
.insert_idle(|state| state.set_dynamic_cast_target(CastTarget::Nothing));
}
}
pub fn remove_screencopy_output(&mut self, output: &Output) {
let _span = tracy_client::span!("Niri::remove_screencopy_output");
for queue in self.screencopy_state.queues_mut() {
@@ -6741,15 +6102,6 @@ niri_render_elements! {
}
}
niri_render_elements! {
CastRenderElement<R> => {
Output = OutputRenderElements<R>,
Window = WindowCastRenderElements<R>,
Pointer = PointerRenderElements<R>,
RelocatedPointer = RelocateRenderElement<PointerRenderElements<R>>,
}
}
niri_render_elements! {
OutputRenderElements<R> => {
Monitor = MonitorRenderElement<R>,
+674
View File
@@ -0,0 +1,674 @@
use std::collections::hash_map::Entry;
use std::collections::{HashMap, HashSet};
use std::mem;
use std::time::Duration;
use calloop::LoopHandle;
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
use smithay::backend::renderer::gles::GlesRenderer;
use smithay::desktop::Window;
use smithay::output::Output;
use smithay::reexports::gbm::Modifier;
use smithay::utils::{Point, Scale, Size};
use crate::dbus::mutter_screen_cast::{self, ScreenCastToNiri, StreamTargetId};
use crate::niri::{CastTarget, Niri, OutputRenderElements, PointerRenderElements, State};
use crate::niri_render_elements;
use crate::render_helpers::RenderTarget;
use crate::utils::get_monotonic_time;
use crate::window::mapped::{MappedId, WindowCastRenderElements};
mod pw_utils;
use pw_utils::{Cast, CastSizeChange, CursorData, PipeWire, PwToNiri};
pub struct Screencasting {
pub casts: Vec<Cast>,
pub pw_to_niri: calloop::channel::Sender<PwToNiri>,
/// Screencast output for each mapped window.
pub mapped_cast_output: HashMap<Window, Output>,
/// Window ID for the "dynamic cast" special window for the xdp-gnome picker.
pub dynamic_cast_id_for_portal: MappedId,
// Drop PipeWire last, and specifically after casts, to prevent a double-free (yay).
pub pipewire: Option<PipeWire>,
}
impl Screencasting {
pub fn new(event_loop: &LoopHandle<'static, State>) -> Self {
let pw_to_niri = {
let (pw_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();
pw_to_niri
};
Self {
casts: vec![],
pw_to_niri,
mapped_cast_output: HashMap::new(),
dynamic_cast_id_for_portal: MappedId::next(),
pipewire: None,
}
}
}
impl State {
pub fn on_pw_msg(&mut self, msg: PwToNiri) {
match msg {
PwToNiri::StopCast { session_id } => self.niri.stop_cast(session_id),
PwToNiri::Redraw { stream_id } => self.redraw_cast(stream_id),
PwToNiri::FatalError => {
warn!("stopping PipeWire due to fatal error");
let casting = &mut self.niri.casting;
if let Some(pw) = casting.pipewire.take() {
let ids: Vec<_> = casting.casts.iter().map(|cast| cast.session_id).collect();
for id in ids {
self.niri.stop_cast(id);
}
self.niri.event_loop.remove(pw.token);
}
}
}
}
fn redraw_cast(&mut self, stream_id: usize) {
let _span = tracy_client::span!("State::redraw_cast");
let casts = &mut self.niri.casting.casts;
let Some(idx) = casts.iter().position(|cast| cast.stream_id == stream_id) else {
warn!("cast to redraw is missing");
return;
};
let cast = &mut casts[idx];
let id = match &cast.target {
CastTarget::Nothing => {
self.backend.with_primary_renderer(|renderer| {
if cast.dequeue_buffer_and_clear(renderer) {
cast.last_frame_time = get_monotonic_time();
}
});
return;
}
CastTarget::Output(weak) => {
if let Some(output) = weak.upgrade() {
self.niri.queue_redraw(&output);
}
return;
}
CastTarget::Window { id } => *id,
};
// Lack of partial borrowing strikes again...
let mut casts = mem::take(&mut self.niri.casting.casts);
let cast = &mut casts[idx];
let mut stop = false;
// Use a loop {} so we can break instead of early-return.
#[allow(clippy::never_loop)]
loop {
let mut windows = self.niri.layout.windows();
let Some((_, mapped)) = windows.find(|(_, mapped)| mapped.id().get() == id) else {
break;
};
// Use the cached output since it will be present even if the output was
// currently disconnected.
let Some(output) = self.niri.casting.mapped_cast_output.get(&mapped.window) else {
break;
};
let scale = Scale::from(output.current_scale().fractional_scale());
let bbox = mapped
.window
.bbox_with_popups()
.to_physical_precise_up(scale);
match cast.ensure_size(bbox.size) {
Ok(CastSizeChange::Ready) => (),
Ok(CastSizeChange::Pending) => break,
Err(err) => {
warn!("error updating stream size, stopping screencast: {err:?}");
stop = true;
break;
}
}
self.backend.with_primary_renderer(|renderer| {
let mut elements = Vec::new();
mapped.render_for_screen_cast(renderer, scale, &mut |elem| {
elements.push(CastRenderElement::from(elem))
});
let mut pointer_elements = Vec::new();
let mut pointer_location = Point::default();
if self.niri.pointer_visibility.is_visible() {
if let Some((pointer_pos, win_pos)) =
self.niri.pointer_pos_for_window_cast(mapped)
{
// Pointer location must be relative to the screencast buffer.
// - win_pos is the position of the main window surface in output-local
// coordinates
// - bbox.loc moves us relative to the screencast buffer
let buf_pos = win_pos + bbox.loc.to_f64().to_logical(scale);
let output_pos =
self.niri.global_space.output_geometry(output).unwrap().loc;
pointer_location = pointer_pos - output_pos.to_f64() - buf_pos;
let pos = buf_pos.to_physical_precise_round(scale).upscale(-1);
self.niri.render_pointer(renderer, output, &mut |elem| {
let elem =
RelocateRenderElement::from_element(elem, pos, Relocate::Relative);
pointer_elements.push(CastRenderElement::from(elem));
});
}
}
let cursor_data = CursorData::compute(&pointer_elements, pointer_location, scale);
if cast.dequeue_buffer_and_render(
renderer,
&elements,
&cursor_data,
bbox.size,
scale,
) {
cast.last_frame_time = get_monotonic_time();
}
});
break;
}
let session_id = cast.session_id;
self.niri.casting.casts = casts;
if stop {
self.niri.stop_cast(session_id);
}
}
pub fn set_dynamic_cast_target(&mut self, target: CastTarget) {
let _span = tracy_client::span!("State::set_dynamic_cast_target");
let mut refresh = None;
match &target {
// Leave refresh as is when clearing. Chances are, the next refresh will match it,
// then we'll avoid reconfiguring.
CastTarget::Nothing => (),
CastTarget::Output(output) => {
if let Some(output) = output.upgrade() {
refresh = Some(output.current_mode().unwrap().refresh as u32);
}
}
CastTarget::Window { id } => {
let mut windows = self.niri.layout.windows();
if let Some((_, mapped)) = windows.find(|(_, mapped)| mapped.id().get() == *id) {
if let Some(output) = self.niri.casting.mapped_cast_output.get(&mapped.window) {
refresh = Some(output.current_mode().unwrap().refresh as u32);
}
}
}
}
let mut to_redraw = Vec::new();
let mut to_stop = Vec::new();
for cast in &mut self.niri.casting.casts {
if !cast.dynamic_target {
continue;
}
if let Some(refresh) = refresh {
if let Err(err) = cast.set_refresh(refresh) {
warn!("error changing cast FPS: {err:?}");
to_stop.push(cast.session_id);
continue;
}
}
cast.target = target.clone();
to_redraw.push(cast.stream_id);
}
for id in to_redraw {
self.redraw_cast(id);
}
}
pub fn on_screen_cast_msg(&mut self, msg: ScreenCastToNiri) {
match msg {
ScreenCastToNiri::StartCast {
session_id,
stream_id,
target,
cursor_mode,
signal_ctx,
} => {
let _span = tracy_client::span!("StartCast");
debug!(session_id, stream_id, "StartCast");
let Some(gbm) = self.backend.gbm_device() else {
warn!("error starting screencast: no GBM device available");
self.niri.stop_cast(session_id);
return;
};
let pw = if let Some(pw) = &self.niri.casting.pipewire {
pw
} else {
match PipeWire::new(
self.niri.event_loop.clone(),
self.niri.casting.pw_to_niri.clone(),
) {
Ok(pipewire) => self.niri.casting.pipewire.insert(pipewire),
Err(err) => {
warn!(
"error starting screencast: PipeWire failed to initialize: {err:?}"
);
self.niri.stop_cast(session_id);
return;
}
}
};
let mut dynamic_target = false;
let (target, size, refresh, alpha) = match target {
StreamTargetId::Output { name } => {
let global_space = &self.niri.global_space;
let output = global_space.outputs().find(|out| out.name() == name);
let Some(output) = output else {
warn!("error starting screencast: requested output is missing");
self.niri.stop_cast(session_id);
return;
};
let mode = output.current_mode().unwrap();
let transform = output.current_transform();
let size = transform.transform_size(mode.size);
let refresh = mode.refresh as u32;
(CastTarget::Output(output.downgrade()), size, refresh, false)
}
StreamTargetId::Window { id }
if id == self.niri.casting.dynamic_cast_id_for_portal.get() =>
{
dynamic_target = true;
// All dynamic casts start as Nothing to avoid surprises and exposing
// sensitive info.
(CastTarget::Nothing, Size::from((1, 1)), 1000, true)
}
StreamTargetId::Window { id } => {
let Some(window) = self.niri.layout.windows().find_map(|(_, mapped)| {
(mapped.id().get() == id).then_some(&mapped.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.casting.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().to_physical_precise_up(scale);
let refresh = output.current_mode().unwrap().refresh as u32;
(CastTarget::Window { id }, bbox.size, refresh, true)
}
};
let mut render_formats = self
.backend
.with_primary_renderer(|renderer| {
renderer.egl_context().dmabuf_render_formats().clone()
})
.unwrap_or_default();
{
let config = self.niri.config.borrow();
if config.debug.force_pipewire_invalid_modifier {
render_formats = render_formats
.into_iter()
.filter(|f| f.modifier == Modifier::Invalid)
.collect();
}
}
let res = pw.start_cast(
gbm,
render_formats,
session_id,
stream_id,
target,
dynamic_target,
size,
refresh,
alpha,
cursor_mode,
signal_ctx,
);
match res {
Ok(cast) => {
self.niri.casting.casts.push(cast);
}
Err(err) => {
warn!("error starting screencast: {err:?}");
self.niri.stop_cast(session_id);
}
}
}
ScreenCastToNiri::StopCast { session_id } => self.niri.stop_cast(session_id),
}
}
}
impl Niri {
pub fn refresh_mapped_cast_window_rules(&mut self) {
// O(N^2) but should be fine since there aren't many casts usually.
self.layout.with_windows_mut(|mapped, _| {
let id = mapped.id().get();
// Find regardless of cast.is_active.
let value = self
.casting
.casts
.iter()
.any(|cast| cast.target == (CastTarget::Window { id }));
mapped.set_is_window_cast_target(value);
});
}
pub fn refresh_mapped_cast_outputs(&mut self) {
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.casting.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.casting
.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: id.get() };
for cast in self
.casting
.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_for_screen_cast(
&mut self,
renderer: &mut GlesRenderer,
output: &Output,
target_presentation_time: Duration,
) {
let _span = tracy_client::span!("Niri::render_for_screen_cast");
let target = CastTarget::Output(output.downgrade());
let size = output.current_mode().unwrap().size;
let transform = output.current_transform();
let size = transform.transform_size(size);
let scale = Scale::from(output.current_scale().fractional_scale());
let mut elements = Vec::new();
let mut pointer = Vec::new();
let mut cursor_data = None;
let mut casts_to_stop = vec![];
let mut casts = mem::take(&mut self.casting.casts);
for cast in &mut casts {
if !cast.is_active() {
continue;
}
if cast.target != target {
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.check_time_and_schedule(output, target_presentation_time) {
continue;
}
if cursor_data.is_none() {
// FIXME: support debug draw opaque regions.
self.render_inner(
renderer,
output,
false,
RenderTarget::Screencast,
&mut |elem| elements.push(elem.into()),
);
if self.pointer_visibility.is_visible() {
self.render_pointer(renderer, output, &mut |elem| pointer.push(elem.into()));
}
let output_pos = self.global_space.output_geometry(output).unwrap().loc;
let pointer_pos = self
.tablet_cursor_location
.unwrap_or_else(|| self.seat.get_pointer().unwrap().current_location());
let pointer_pos = pointer_pos - output_pos.to_f64();
cursor_data = Some(CursorData::compute(&pointer, pointer_pos, scale));
}
let cursor_data = cursor_data.as_ref().unwrap();
if cast.dequeue_buffer_and_render(renderer, &elements, cursor_data, size, scale) {
cast.last_frame_time = target_presentation_time;
}
}
self.casting.casts = casts;
for id in casts_to_stop {
self.stop_cast(id);
}
}
pub 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.casting.casts);
for cast in &mut casts {
if !cast.is_active() {
continue;
}
let CastTarget::Window { id } = cast.target else {
continue;
};
let mut windows = self.layout.windows_for_output(output);
let Some(mapped) = windows.find(|win| win.id().get() == id) else {
continue;
};
let bbox = mapped
.window
.bbox_with_popups()
.to_physical_precise_up(scale);
match cast.ensure_size(bbox.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.check_time_and_schedule(output, target_presentation_time) {
continue;
}
let mut elements = Vec::new();
mapped.render_for_screen_cast(renderer, scale, &mut |elem| {
elements.push(CastRenderElement::from(elem))
});
let mut pointer_elements = Vec::new();
let mut pointer_location = Point::default();
if self.pointer_visibility.is_visible() {
if let Some((pointer_pos, win_pos)) = self.pointer_pos_for_window_cast(mapped) {
// Pointer location must be relative to the screencast buffer.
// - win_pos is the position of the main window surface in output-local
// coordinates
// - bbox.loc moves us relative to the screencast buffer
let buf_pos = win_pos + bbox.loc.to_f64().to_logical(scale);
let output_pos = self.global_space.output_geometry(output).unwrap().loc;
pointer_location = pointer_pos - output_pos.to_f64() - buf_pos;
let pos = buf_pos.to_physical_precise_round(scale).upscale(-1);
self.render_pointer(renderer, output, &mut |elem| {
let elem =
RelocateRenderElement::from_element(elem, pos, Relocate::Relative);
pointer_elements.push(CastRenderElement::from(elem));
});
}
}
let cursor_data = CursorData::compute(&pointer_elements, pointer_location, scale);
if cast.dequeue_buffer_and_render(renderer, &elements, &cursor_data, bbox.size, scale) {
cast.last_frame_time = target_presentation_time;
}
}
self.casting.casts = casts;
for id in casts_to_stop {
self.stop_cast(id);
}
}
fn stop_cast(&mut self, session_id: usize) {
let _span = tracy_client::span!("Niri::stop_cast");
debug!(session_id, "StopCast");
for i in (0..self.casting.casts.len()).rev() {
let cast = &self.casting.casts[i];
if cast.session_id != session_id {
continue;
}
let cast = self.casting.casts.swap_remove(i);
if let Err(err) = cast.stream.disconnect() {
warn!("error disconnecting stream: {err:?}");
}
}
let dbus = &self.dbus.as_ref().unwrap();
let server = dbus.conn_screen_cast.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.inner(), iface.signal_emitter().clone())
.await
});
}
}
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 mut saw_dynamic = false;
let mut ids = Vec::new();
for cast in &self.casting.casts {
if cast.target != target {
continue;
}
if cast.dynamic_target {
saw_dynamic = true;
continue;
}
ids.push(cast.session_id);
}
for id in ids {
self.stop_cast(id);
}
// We don't stop dynamic casts, instead we switch them to Nothing.
if saw_dynamic {
self.event_loop
.insert_idle(|state| state.set_dynamic_cast_target(CastTarget::Nothing));
}
}
}
niri_render_elements! {
CastRenderElement<R> => {
Output = OutputRenderElements<R>,
Window = WindowCastRenderElements<R>,
Pointer = PointerRenderElements<R>,
RelocatedPointer = RelocateRenderElement<PointerRenderElements<R>>,
}
}
@@ -50,10 +50,11 @@ use smithay::utils::{Logical, Physical, Point, Scale, Size, Transform};
use zbus::object_server::SignalEmitter;
use crate::dbus::mutter_screen_cast::{self, CursorMode};
use crate::niri::{CastRenderElement, CastTarget, State};
use crate::niri::{CastTarget, State};
use crate::render_helpers::{
clear_dmabuf, encompassing_geo, render_and_download, render_to_dmabuf,
};
use crate::screencasting::CastRenderElement;
use crate::utils::get_monotonic_time;
// Give a 0.1 ms allowance for presentation time errors.
@@ -1040,7 +1041,7 @@ impl Cast {
let source = Generic::new(sync_fd, Interest::READ, Mode::OneShot);
self.event_loop
.insert_source(source, move |_, _, state| {
for cast in &mut state.niri.casts {
for cast in &mut state.niri.casting.casts {
if cast.stream_id == stream_id {
cast.queue_completed_buffers();
}