mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-23 02:05:33 +07:00
Implement initial window screencasting
This commit is contained in:
+1
-4
@@ -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 => (),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user