mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-21 02:01:55 +07:00
Implement cursor metadata in window screencast
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
This commit is contained in:
committed by
Ivan Molodetskikh
parent
0fb6c5706b
commit
05599ce2c4
@@ -30,7 +30,7 @@ pub struct Session {
|
||||
stopped: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Type, Clone, Copy)]
|
||||
#[derive(Debug, Default, Deserialize, Type, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CursorMode {
|
||||
#[default]
|
||||
Hidden = 0,
|
||||
|
||||
+141
-48
@@ -152,7 +152,7 @@ use crate::protocols::screencopy::{Screencopy, ScreencopyBuffer, ScreencopyManag
|
||||
use crate::protocols::virtual_pointer::VirtualPointerManagerState;
|
||||
use crate::pw_utils::{Cast, PipeWire};
|
||||
#[cfg(feature = "xdp-gnome-screencast")]
|
||||
use crate::pw_utils::{CastSizeChange, PwToNiri};
|
||||
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;
|
||||
@@ -179,7 +179,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;
|
||||
use crate::window::mapped::{MappedId, WindowCastRenderElements};
|
||||
use crate::window::{InitialConfigureState, Mapped, ResolvedWindowRules, Unmapped, WindowRef};
|
||||
|
||||
const CLEAR_COLOR_LOCKED: [f32; 4] = [0.3, 0.1, 0.1, 1.];
|
||||
@@ -2020,64 +2020,109 @@ impl State {
|
||||
let _span = tracy_client::span!("State::redraw_cast");
|
||||
|
||||
let casts = &mut self.niri.casts;
|
||||
let Some(cast) = casts.iter_mut().find(|cast| cast.stream_id == stream_id) else {
|
||||
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];
|
||||
|
||||
match &cast.target {
|
||||
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 } => {
|
||||
let mut windows = self.niri.layout.windows();
|
||||
let Some((_, mapped)) = windows.find(|(_, mapped)| mapped.id().get() == *id) else {
|
||||
return;
|
||||
};
|
||||
CastTarget::Window { id } => *id,
|
||||
};
|
||||
|
||||
// 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 {
|
||||
return;
|
||||
};
|
||||
// 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;
|
||||
};
|
||||
|
||||
let scale = Scale::from(output.current_scale().fractional_scale());
|
||||
let bbox = mapped
|
||||
.window
|
||||
.bbox_with_popups()
|
||||
.to_physical_precise_up(scale);
|
||||
// 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;
|
||||
};
|
||||
|
||||
match cast.ensure_size(bbox.size) {
|
||||
Ok(CastSizeChange::Ready) => (),
|
||||
Ok(CastSizeChange::Pending) => return,
|
||||
Err(err) => {
|
||||
warn!("error updating stream size, stopping screencast: {err:?}");
|
||||
drop(windows);
|
||||
let session_id = cast.session_id;
|
||||
self.niri.stop_cast(session_id);
|
||||
return;
|
||||
}
|
||||
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| {
|
||||
// FIXME: pointer.
|
||||
let mut elements = Vec::new();
|
||||
mapped.render_for_screen_cast(renderer, scale, &mut |elem| elements.push(elem));
|
||||
|
||||
if cast.dequeue_buffer_and_render(renderer, &elements, bbox.size, scale) {
|
||||
cast.last_frame_time = get_monotonic_time();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5247,7 +5292,10 @@ impl Niri {
|
||||
|
||||
let scale = Scale::from(output.current_scale().fractional_scale());
|
||||
|
||||
let mut elements = None;
|
||||
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);
|
||||
@@ -5273,12 +5321,29 @@ impl Niri {
|
||||
continue;
|
||||
}
|
||||
|
||||
// FIXME: Hidden / embedded / metadata cursor
|
||||
let elements = elements.get_or_insert_with(|| {
|
||||
self.render(renderer, output, true, RenderTarget::Screencast)
|
||||
});
|
||||
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 cast.dequeue_buffer_and_render(renderer, elements, size, scale) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -5335,11 +5400,30 @@ impl Niri {
|
||||
continue;
|
||||
}
|
||||
|
||||
// FIXME: pointer.
|
||||
let mut elements = Vec::new();
|
||||
mapped.render_for_screen_cast(renderer, scale, &mut |elem| elements.push(elem));
|
||||
mapped.render_for_screen_cast(renderer, scale, &mut |elem| {
|
||||
elements.push(CastRenderElement::from(elem))
|
||||
});
|
||||
|
||||
if cast.dequeue_buffer_and_render(renderer, &elements, bbox.size, scale) {
|
||||
let mut pointer_elements = Vec::new();
|
||||
let mut pointer_location = Point::default();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -6645,6 +6729,15 @@ 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>,
|
||||
|
||||
+317
-20
@@ -1,12 +1,13 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cmp::min;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
use std::iter::zip;
|
||||
use std::mem;
|
||||
use std::os::fd::{AsFd, AsRawFd, BorrowedFd};
|
||||
use std::ptr::NonNull;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
use std::{mem, slice};
|
||||
|
||||
use anyhow::Context as _;
|
||||
use calloop::timer::{TimeoutAction, Timer};
|
||||
@@ -29,31 +30,45 @@ use pipewire::spa::utils::{
|
||||
};
|
||||
use pipewire::spa::{self};
|
||||
use pipewire::stream::{Stream, StreamFlags, StreamListener, StreamRc, StreamState};
|
||||
use pipewire::sys::{pw_buffer, pw_stream_queue_buffer};
|
||||
use pipewire::sys::{pw_buffer, pw_check_library_version, pw_stream_queue_buffer};
|
||||
use smithay::backend::allocator::dmabuf::{AsDmabuf, Dmabuf};
|
||||
use smithay::backend::allocator::format::FormatSet;
|
||||
use smithay::backend::allocator::gbm::{GbmBuffer, GbmBufferFlags, GbmDevice};
|
||||
use smithay::backend::allocator::{Format, Fourcc};
|
||||
use smithay::backend::drm::DrmDeviceFd;
|
||||
use smithay::backend::renderer::damage::OutputDamageTracker;
|
||||
use smithay::backend::renderer::element::RenderElement;
|
||||
use smithay::backend::renderer::element::utils::{Relocate, RelocateRenderElement};
|
||||
use smithay::backend::renderer::element::{Element, RenderElement};
|
||||
use smithay::backend::renderer::gles::GlesRenderer;
|
||||
use smithay::backend::renderer::sync::SyncPoint;
|
||||
use smithay::backend::renderer::ExportMem;
|
||||
use smithay::output::{Output, OutputModeSource};
|
||||
use smithay::reexports::calloop::generic::Generic;
|
||||
use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction};
|
||||
use smithay::reexports::gbm::Modifier;
|
||||
use smithay::utils::{Physical, Scale, Size, Transform};
|
||||
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::{CastTarget, State};
|
||||
use crate::render_helpers::{clear_dmabuf, render_to_dmabuf};
|
||||
use crate::niri::{CastRenderElement, CastTarget, State};
|
||||
use crate::render_helpers::{
|
||||
clear_dmabuf, encompassing_geo, render_and_download, render_to_dmabuf,
|
||||
};
|
||||
use crate::utils::get_monotonic_time;
|
||||
|
||||
// Give a 0.1 ms allowance for presentation time errors.
|
||||
const CAST_DELAY_ALLOWANCE: Duration = Duration::from_micros(100);
|
||||
|
||||
const CURSOR_FORMAT: spa_video_format = SPA_VIDEO_FORMAT_BGRA;
|
||||
const CURSOR_BPP: u32 = 4;
|
||||
const CURSOR_WIDTH: u32 = 384;
|
||||
const CURSOR_HEIGHT: u32 = 384;
|
||||
const CURSOR_BITMAP_SIZE: usize = (CURSOR_WIDTH * CURSOR_HEIGHT * CURSOR_BPP) as usize;
|
||||
const CURSOR_META_SIZE: usize =
|
||||
mem::size_of::<spa_meta_cursor>() + mem::size_of::<spa_meta_bitmap>() + CURSOR_BITMAP_SIZE;
|
||||
const BITMAP_META_OFFSET: usize = mem::size_of::<spa_meta_cursor>();
|
||||
const BITMAP_DATA_OFFSET: usize = mem::size_of::<spa_meta_bitmap>();
|
||||
|
||||
pub struct PipeWire {
|
||||
_context: ContextRc,
|
||||
pub core: CoreRc,
|
||||
@@ -79,7 +94,7 @@ pub struct Cast {
|
||||
pub dynamic_target: bool,
|
||||
formats: FormatSet,
|
||||
offer_alpha: bool,
|
||||
pub cursor_mode: CursorMode,
|
||||
cursor_mode: CursorMode,
|
||||
pub last_frame_time: Duration,
|
||||
scheduled_redraw: Option<RegistrationToken>,
|
||||
// Incremented once per successful frame, stored in buffer meta.
|
||||
@@ -124,6 +139,8 @@ enum CastState {
|
||||
plane_count: i32,
|
||||
// Lazily-initialized to keep the initialization to a single place.
|
||||
damage_tracker: Option<OutputDamageTracker>,
|
||||
cursor_damage_tracker: Option<OutputDamageTracker>,
|
||||
last_cursor_location: Option<Point<i32, Physical>>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -133,6 +150,49 @@ pub enum CastSizeChange {
|
||||
Pending,
|
||||
}
|
||||
|
||||
/// Data for drawing a cursor either as metadata or embedded.
|
||||
///
|
||||
/// We have weird borrowed references here in order to support both metadata and embedded cases.
|
||||
/// The cursor damage tracker needs a slice of impl Element at (0, 0), so we pass it `relocated`
|
||||
/// (luckily, &impl Element also impls Element). Then, if we need to embed the cursor, we chain the
|
||||
/// elements to the main video buffer elements, so we need the same type. We use `original` for
|
||||
/// this; `E` is expected to match the type of the main video buffer elements.
|
||||
#[derive(Debug)]
|
||||
pub struct CursorData<'a, E> {
|
||||
/// Cursor elements at their original location.
|
||||
original: &'a [E],
|
||||
/// Cursor elements relocated to (0, 0).
|
||||
relocated: Vec<RelocateRenderElement<&'a E>>,
|
||||
/// Location of the cursor's hotspot in the video buffer.
|
||||
location: Point<i32, Physical>,
|
||||
/// Location of the cursor's hotspot on the cursor bitmap.
|
||||
hotspot: Point<i32, Physical>,
|
||||
/// Size of the elements' encompassing geo.
|
||||
size: Size<i32, Physical>,
|
||||
/// Scale the elements should be rendered at.
|
||||
scale: Scale<f64>,
|
||||
}
|
||||
|
||||
impl<'a, E: Element> CursorData<'a, E> {
|
||||
pub fn compute(elements: &'a [E], location: Point<f64, Logical>, scale: Scale<f64>) -> Self {
|
||||
let location = location.to_physical_precise_round(scale);
|
||||
|
||||
let geo = encompassing_geo(scale, elements.iter());
|
||||
let relocated = Vec::from_iter(elements.iter().map(|elem| {
|
||||
RelocateRenderElement::from_element(elem, geo.loc.upscale(-1), Relocate::Relative)
|
||||
}));
|
||||
|
||||
Self {
|
||||
original: elements,
|
||||
relocated,
|
||||
location,
|
||||
hotspot: location - geo.loc,
|
||||
size: geo.size,
|
||||
scale,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! make_params {
|
||||
($params:ident, $formats:expr, $size:expr, $refresh:expr, $alpha:expr) => {
|
||||
let mut b1 = Vec::new();
|
||||
@@ -215,7 +275,7 @@ impl PipeWire {
|
||||
size: Size<i32, Physical>,
|
||||
refresh: u32,
|
||||
alpha: bool,
|
||||
cursor_mode: CursorMode,
|
||||
mut cursor_mode: CursorMode,
|
||||
signal_ctx: SignalEmitter<'static>,
|
||||
) -> anyhow::Result<Cast> {
|
||||
let _span = tracy_client::span!("PipeWire::start_cast");
|
||||
@@ -241,6 +301,14 @@ impl PipeWire {
|
||||
)
|
||||
.context("error creating Stream")?;
|
||||
|
||||
if cursor_mode == CursorMode::Metadata && !pw_version_supports_cursor_metadata() {
|
||||
debug!(
|
||||
"metadata cursor mode requested, but PipeWire is too old (need >= 1.4.8); \
|
||||
switching to embedded cursor"
|
||||
);
|
||||
cursor_mode = CursorMode::Embedded;
|
||||
}
|
||||
|
||||
let pending_size = Size::from((size.w as u32, size.h as u32));
|
||||
|
||||
// Like in good old wayland-rs times...
|
||||
@@ -477,11 +545,16 @@ impl PipeWire {
|
||||
let modifier = *modifier;
|
||||
let plane_count = *plane_count;
|
||||
|
||||
let damage_tracker =
|
||||
if let CastState::Ready { damage_tracker, .. } = &mut *state {
|
||||
damage_tracker.take()
|
||||
let (damage_tracker, cursor_damage_tracker) =
|
||||
if let CastState::Ready {
|
||||
damage_tracker,
|
||||
cursor_damage_tracker,
|
||||
..
|
||||
} = &mut *state
|
||||
{
|
||||
(damage_tracker.take(), cursor_damage_tracker.take())
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
|
||||
debug!(stream_id, "pw stream: moving to ready state");
|
||||
@@ -492,6 +565,8 @@ impl PipeWire {
|
||||
modifier,
|
||||
plane_count,
|
||||
damage_tracker,
|
||||
cursor_damage_tracker,
|
||||
last_cursor_location: None,
|
||||
};
|
||||
|
||||
plane_count
|
||||
@@ -526,6 +601,8 @@ impl PipeWire {
|
||||
modifier,
|
||||
plane_count: plane_count as i32,
|
||||
damage_tracker: None,
|
||||
cursor_damage_tracker: None,
|
||||
last_cursor_location: None,
|
||||
};
|
||||
|
||||
plane_count as i32
|
||||
@@ -563,8 +640,6 @@ impl PipeWire {
|
||||
),
|
||||
);
|
||||
|
||||
// FIXME: Hidden / embedded / metadata cursor
|
||||
|
||||
let o2 = pod::object!(
|
||||
SpaTypes::ObjectParamMeta,
|
||||
ParamType::Meta,
|
||||
@@ -579,7 +654,24 @@ impl PipeWire {
|
||||
);
|
||||
let mut b1 = vec![];
|
||||
let mut b2 = vec![];
|
||||
let mut params = [make_pod(&mut b1, o1), make_pod(&mut b2, o2)];
|
||||
let mut params = vec![make_pod(&mut b1, o1), make_pod(&mut b2, o2)];
|
||||
|
||||
let mut b_cursor = vec![];
|
||||
if cursor_mode == CursorMode::Metadata {
|
||||
let o_cursor = pod::object!(
|
||||
SpaTypes::ObjectParamMeta,
|
||||
ParamType::Meta,
|
||||
Property::new(
|
||||
SPA_PARAM_META_type,
|
||||
pod::Value::Id(spa::utils::Id(SPA_META_Cursor))
|
||||
),
|
||||
Property::new(
|
||||
SPA_PARAM_META_size,
|
||||
pod::Value::Int(CURSOR_META_SIZE as i32)
|
||||
),
|
||||
);
|
||||
params.push(make_pod(&mut b_cursor, o_cursor));
|
||||
}
|
||||
|
||||
if let Err(err) = stream.update_params(&mut params) {
|
||||
warn!(stream_id, "error updating stream params: {err:?}");
|
||||
@@ -961,21 +1053,36 @@ impl Cast {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn dequeue_buffer_and_render(
|
||||
&mut self,
|
||||
renderer: &mut GlesRenderer,
|
||||
elements: &[impl RenderElement<GlesRenderer>],
|
||||
elements: &[CastRenderElement<GlesRenderer>],
|
||||
cursor_data: &CursorData<CastRenderElement<GlesRenderer>>,
|
||||
size: Size<i32, Physical>,
|
||||
scale: Scale<f64>,
|
||||
) -> bool {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
|
||||
let CastState::Ready { damage_tracker, .. } = &mut inner.state else {
|
||||
let CastState::Ready {
|
||||
damage_tracker,
|
||||
cursor_damage_tracker,
|
||||
last_cursor_location,
|
||||
..
|
||||
} = &mut inner.state
|
||||
else {
|
||||
error!("cast must be in Ready state to render");
|
||||
return false;
|
||||
};
|
||||
let damage_tracker = damage_tracker
|
||||
.get_or_insert_with(|| OutputDamageTracker::new(size, scale, Transform::Normal));
|
||||
let cursor_damage_tracker = cursor_damage_tracker.get_or_insert_with(|| {
|
||||
OutputDamageTracker::new(
|
||||
Size::from((CURSOR_WIDTH as _, CURSOR_HEIGHT as _)),
|
||||
scale,
|
||||
Transform::Normal,
|
||||
)
|
||||
});
|
||||
|
||||
// Size change will drop the damage tracker, but scale change won't, so check it here.
|
||||
let OutputModeSource::Static { scale: t_scale, .. } = damage_tracker.mode() else {
|
||||
@@ -983,13 +1090,31 @@ impl Cast {
|
||||
};
|
||||
if *t_scale != scale {
|
||||
*damage_tracker = OutputDamageTracker::new(size, scale, Transform::Normal);
|
||||
*cursor_damage_tracker = OutputDamageTracker::new(
|
||||
Size::from((CURSOR_WIDTH as _, CURSOR_HEIGHT as _)),
|
||||
scale,
|
||||
Transform::Normal,
|
||||
);
|
||||
}
|
||||
|
||||
let (damage, _states) = damage_tracker.damage_output(1, elements).unwrap();
|
||||
if damage.is_none() {
|
||||
|
||||
let mut has_cursor_update = false;
|
||||
let mut redraw_cursor = false;
|
||||
if self.cursor_mode != CursorMode::Hidden {
|
||||
let (damage, _states) = cursor_damage_tracker
|
||||
.damage_output(1, &cursor_data.relocated)
|
||||
.unwrap();
|
||||
redraw_cursor = damage.is_some();
|
||||
has_cursor_update =
|
||||
redraw_cursor || *last_cursor_location != Some(cursor_data.location);
|
||||
}
|
||||
|
||||
if damage.is_none() && !has_cursor_update {
|
||||
trace!("no damage, skipping frame");
|
||||
return false;
|
||||
}
|
||||
*last_cursor_location = Some(cursor_data.location);
|
||||
drop(inner);
|
||||
|
||||
let Some(pw_buffer) = self.dequeue_available_buffer() else {
|
||||
@@ -1001,6 +1126,19 @@ impl Cast {
|
||||
unsafe {
|
||||
let spa_buffer = (*buffer).buffer;
|
||||
|
||||
let mut pointer_elements = None;
|
||||
if self.cursor_mode == CursorMode::Metadata {
|
||||
add_cursor_metadata(renderer, spa_buffer, cursor_data, redraw_cursor);
|
||||
} else if self.cursor_mode != CursorMode::Hidden {
|
||||
// Embed the cursor into the main render.
|
||||
pointer_elements = Some(cursor_data.original.iter());
|
||||
}
|
||||
let pointer_elements = pointer_elements.into_iter().flatten();
|
||||
let elements = pointer_elements.chain(elements);
|
||||
|
||||
// FIXME: would be good to skip rendering the full frame if only the pointer changed.
|
||||
// Unfortunately, I think the OBS PipeWire code needs to be updated first to cleanly
|
||||
// allow for that codepath.
|
||||
let fd = (*(*spa_buffer).datas).fd;
|
||||
let dmabuf = self.inner.borrow().dmabufs[&fd].clone();
|
||||
|
||||
@@ -1010,7 +1148,7 @@ impl Cast {
|
||||
size,
|
||||
scale,
|
||||
Transform::Normal,
|
||||
elements.iter().rev(),
|
||||
elements.rev(),
|
||||
) {
|
||||
Ok(sync_point) => {
|
||||
mark_buffer_as_good(pw_buffer, &mut self.sequence_counter);
|
||||
@@ -1031,8 +1169,14 @@ impl Cast {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
|
||||
// Clear out the damage tracker if we're in Ready state.
|
||||
if let CastState::Ready { damage_tracker, .. } = &mut inner.state {
|
||||
if let CastState::Ready {
|
||||
damage_tracker,
|
||||
cursor_damage_tracker,
|
||||
..
|
||||
} = &mut inner.state
|
||||
{
|
||||
*damage_tracker = None;
|
||||
*cursor_damage_tracker = None;
|
||||
};
|
||||
drop(inner);
|
||||
|
||||
@@ -1045,6 +1189,10 @@ impl Cast {
|
||||
unsafe {
|
||||
let spa_buffer = (*buffer).buffer;
|
||||
|
||||
if self.cursor_mode == CursorMode::Metadata {
|
||||
add_invisible_cursor(spa_buffer);
|
||||
}
|
||||
|
||||
let fd = (*(*spa_buffer).datas).fd;
|
||||
let dmabuf = self.inner.borrow().dmabufs[&fd].clone();
|
||||
|
||||
@@ -1083,6 +1231,12 @@ impl CastState {
|
||||
}
|
||||
}
|
||||
|
||||
fn pw_version_supports_cursor_metadata() -> bool {
|
||||
// This PipeWire version fixed a critical memory issue with cursor metadata:
|
||||
// https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/2538
|
||||
unsafe { pw_check_library_version(1, 4, 8) }
|
||||
}
|
||||
|
||||
fn make_video_params(
|
||||
formats: &FormatSet,
|
||||
size: Size<u32, Physical>,
|
||||
@@ -1279,3 +1433,146 @@ unsafe fn find_meta_header(buffer: *mut spa_buffer) -> Option<NonNull<spa_meta_h
|
||||
let p = spa_buffer_find_meta_data(buffer, SPA_META_Header, size_of::<spa_meta_header>()).cast();
|
||||
NonNull::new(p)
|
||||
}
|
||||
|
||||
unsafe fn add_invisible_cursor(spa_buffer: *mut spa_buffer) {
|
||||
unsafe {
|
||||
let cursor_meta_ptr: *mut spa_meta_cursor = spa_buffer_find_meta_data(
|
||||
spa_buffer,
|
||||
SPA_META_Cursor,
|
||||
mem::size_of::<spa_meta_cursor>(),
|
||||
)
|
||||
.cast();
|
||||
let Some(cursor_meta) = cursor_meta_ptr.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// The cursor is present but invisible.
|
||||
cursor_meta.id = 1;
|
||||
cursor_meta.position.x = 0;
|
||||
cursor_meta.position.y = 0;
|
||||
cursor_meta.hotspot.x = 0;
|
||||
cursor_meta.hotspot.y = 0;
|
||||
cursor_meta.bitmap_offset = BITMAP_META_OFFSET as _;
|
||||
|
||||
let bitmap_meta_ptr = cursor_meta_ptr
|
||||
.byte_add(BITMAP_META_OFFSET)
|
||||
.cast::<spa_meta_bitmap>();
|
||||
let bitmap_meta = &mut *bitmap_meta_ptr;
|
||||
|
||||
// HACK: PipeWire docs say offset = 0 means invisible.
|
||||
//
|
||||
// Unfortunately, OBS doesn't actually check that, instead it checks that size isn't zero:
|
||||
// https://github.com/obsproject/obs-studio/blob/f4aaa5f0417c5ec40a3799551e125129fce1e007/plugins/linux-pipewire/pipewire.c#L900
|
||||
//
|
||||
// Unfortunately, libwebrtc, on top of ignoring offset, also treats size = 0 as "preserve
|
||||
// previous cursor":
|
||||
// https://webrtc.googlesource.com/src/+/97b46e12582606a238d4f0c8524365cf5bdcb411/modules/desktop_capture/linux/wayland/shared_screencast_stream.cc#765
|
||||
//
|
||||
// So, send a 1x1 transparent pixel instead...
|
||||
bitmap_meta.offset = BITMAP_DATA_OFFSET as _;
|
||||
bitmap_meta.size.width = 1;
|
||||
bitmap_meta.size.height = 1;
|
||||
bitmap_meta.stride = CURSOR_BPP as i32;
|
||||
bitmap_meta.format = CURSOR_FORMAT;
|
||||
|
||||
let bitmap_data = bitmap_meta_ptr.cast::<u8>().add(BITMAP_DATA_OFFSET);
|
||||
let bitmap_slice = slice::from_raw_parts_mut(bitmap_data, CURSOR_BITMAP_SIZE);
|
||||
bitmap_slice[..4].copy_from_slice(&[0, 0, 0, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn add_cursor_metadata(
|
||||
renderer: &mut GlesRenderer,
|
||||
spa_buffer: *mut spa_buffer,
|
||||
cursor_data: &CursorData<impl RenderElement<GlesRenderer>>,
|
||||
redraw: bool,
|
||||
) {
|
||||
unsafe {
|
||||
let cursor_meta_ptr: *mut spa_meta_cursor = spa_buffer_find_meta_data(
|
||||
spa_buffer,
|
||||
SPA_META_Cursor,
|
||||
mem::size_of::<spa_meta_cursor>(),
|
||||
)
|
||||
.cast();
|
||||
let Some(cursor_meta) = cursor_meta_ptr.as_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
cursor_meta.id = 1;
|
||||
cursor_meta.position.x = cursor_data.location.x;
|
||||
cursor_meta.position.y = cursor_data.location.y;
|
||||
cursor_meta.hotspot.x = cursor_data.hotspot.x;
|
||||
cursor_meta.hotspot.y = cursor_data.hotspot.y;
|
||||
|
||||
if !redraw {
|
||||
trace!("cursor not damaged, skipping rerendering");
|
||||
cursor_meta.bitmap_offset = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
cursor_meta.bitmap_offset = BITMAP_META_OFFSET as _;
|
||||
|
||||
let bitmap_meta_ptr = cursor_meta_ptr
|
||||
.byte_add(BITMAP_META_OFFSET)
|
||||
.cast::<spa_meta_bitmap>();
|
||||
let bitmap_meta = &mut *bitmap_meta_ptr;
|
||||
|
||||
// Start with a 1x1 transparent pixel; see comment in add_invisible_cursor().
|
||||
bitmap_meta.offset = BITMAP_DATA_OFFSET as _;
|
||||
bitmap_meta.size.width = 1;
|
||||
bitmap_meta.size.height = 1;
|
||||
bitmap_meta.stride = CURSOR_BPP as i32;
|
||||
bitmap_meta.format = CURSOR_FORMAT;
|
||||
|
||||
let bitmap_data = bitmap_meta_ptr.cast::<u8>().add(BITMAP_DATA_OFFSET);
|
||||
let bitmap_slice = slice::from_raw_parts_mut(bitmap_data, CURSOR_BITMAP_SIZE);
|
||||
bitmap_slice[..4].copy_from_slice(&[0, 0, 0, 0]);
|
||||
|
||||
let size = Size::new(
|
||||
min(cursor_data.size.w, CURSOR_WIDTH as i32),
|
||||
min(cursor_data.size.h, CURSOR_HEIGHT as i32),
|
||||
);
|
||||
if size.w == 0 || size.h == 0 {
|
||||
trace!("cursor is invisible, skipping rendering");
|
||||
return;
|
||||
}
|
||||
|
||||
let _span = tracy_client::span!("add_cursor_metadata render cursor");
|
||||
|
||||
// FIXME: use a reliable buffer whenever we're rendering the cursor.
|
||||
//
|
||||
// PipeWire buffers are not normally guaranteed to reach the destination, so our buffer
|
||||
// with the rendered cursor bitmap may not reach the consumer.
|
||||
//
|
||||
// Reliable buffers should be available starting from 1.6.0:
|
||||
// https://gitlab.freedesktop.org/pipewire/pipewire/-/issues/4885
|
||||
let mapping = match render_and_download(
|
||||
renderer,
|
||||
size,
|
||||
cursor_data.scale,
|
||||
Transform::Normal,
|
||||
Fourcc::Argb8888,
|
||||
cursor_data.relocated.iter().rev(),
|
||||
) {
|
||||
Ok(mapping) => mapping,
|
||||
Err(err) => {
|
||||
warn!("error rendering cursor: {err:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let pixels = match renderer.map_texture(&mapping) {
|
||||
Ok(pixels) => pixels,
|
||||
Err(err) => {
|
||||
warn!("error mapping cursor texture: {err:?}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
bitmap_slice[..pixels.len()].copy_from_slice(pixels);
|
||||
|
||||
// Fill the metadata now that everything succeeded.
|
||||
bitmap_meta.size.width = size.w as _;
|
||||
bitmap_meta.size.height = size.h as _;
|
||||
bitmap_meta.stride = size.w * CURSOR_BPP as i32;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user