Implement cursor metadata in window screencast

Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
This commit is contained in:
abmantis
2026-01-07 01:04:16 +00:00
committed by Ivan Molodetskikh
parent 0fb6c5706b
commit 05599ce2c4
3 changed files with 459 additions and 69 deletions
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}