Skip to main content

pixelflux/wayland/
pwcapture.rs

1/*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5 */
6
7//! PipeWire consumer for a portal monitor stream: one `Video/Input` stream per host output on
8//! the connection the portal opened, delivering the compositor's frames to the host-capture
9//! consumer as [`HostFrame`]s.
10//!
11//! The compositor allocates and fills the buffers, so a frame is never copied here: a dmabuf
12//! buffer becomes a `Dmabuf` the encoder imports in place (built once per buffer from
13//! duplicated fds, so the encoder's import cache keeps hitting), a memfd buffer is mapped once
14//! and read in place by the CPU path. A buffer stays dequeued from the moment its frame is
15//! published until the consumer releases it, which is the same slot discipline the Wayland
16//! capture threads follow, so the retained frame an IDR re-encodes is still the compositor's
17//! memory. Frames are handed on from the stream's own thread the instant `process` runs, and
18//! the format offered to the compositor asks it to send at most the capture's rate, so nothing
19//! is captured only to be dropped and nothing waits for a timer.
20//!
21//! Format negotiation follows the modifier convention consumers and compositors agree on: each
22//! importable `(fourcc, modifiers)` pair the encoder's EGL display reported is one format entry
23//! with a mandatory, non-fixating modifier choice, followed by plain entries the compositor
24//! answers with memfd buffers; the compositor picks, and the fixated format decides whether
25//! dmabuf or memfd buffers are requested. Cursor metadata is requested so a metadata-mode
26//! session delivers the host's real cursor sprite to the cursor callback, and damage metadata
27//! so unchanged stripes are recognized.
28
29use std::ffi::{CStr, c_char, c_int, c_void};
30use std::os::fd::{FromRawFd, OwnedFd};
31use std::ptr;
32use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, AtomicPtr, AtomicU64, Ordering};
33use std::sync::{Arc, Mutex};
34use std::time::{Duration, Instant};
35
36use smithay::backend::allocator::dmabuf::{Dmabuf, DmabufFlags};
37use smithay::backend::allocator::{Fourcc, Modifier};
38use smithay::utils::{Physical, Rectangle};
39
40use crate::pipewire::*;
41use crate::wayland::cursor::CursorJob;
42use crate::wayland::host::{FrameSink, HostCpuFrame, HostFrame, now_ns, trace};
43
44const FOURCC_XR24: u32 = 0x3432_5258;
45const FOURCC_AR24: u32 = 0x3432_5241;
46const FOURCC_XB24: u32 = 0x3432_4258;
47const FOURCC_AB24: u32 = 0x3432_4241;
48const MAX_DAMAGE_REGIONS: u32 = 16;
49/// The largest cursor sprite edge the cursor meta is asked to hold. A compositor offers that
50/// meta at one fixed size (Mutter 384x384, KWin 256x256), and PipeWire leaves it off the
51/// buffers, with every sprite, unless the size falls inside the range asked for.
52const MAX_CURSOR_EDGE: u32 = 1024;
53
54/// The SPA pixel formats a consumer of BGRA-order pixels takes, best first, with the DRM fourcc
55/// each corresponds to (SPA names byte order, DRM names the little-endian word).
56const SHM_FORMATS: [(u32, u32); 4] = [
57    (SPA_VIDEO_FORMAT_BGRX, FOURCC_XR24),
58    (SPA_VIDEO_FORMAT_BGRA, FOURCC_AR24),
59    (SPA_VIDEO_FORMAT_RGBX, FOURCC_XB24),
60    (SPA_VIDEO_FORMAT_RGBA, FOURCC_AB24),
61];
62
63fn spa_to_fourcc(spa: u32) -> Option<u32> {
64    SHM_FORMATS.iter().find(|(s, _)| *s == spa).map(|(_, f)| *f)
65}
66
67/// The wl_shm-style code `HostCpuFrame` converts by: 0/1 for the ARGB/XRGB words, DRM fourcc
68/// for the rest.
69fn shm_code(fourcc: u32) -> u32 {
70    match fourcc {
71        FOURCC_AR24 => 0,
72        FOURCC_XR24 => 1,
73        other => other,
74    }
75}
76
77/// One PipeWire connection, shared by every stream of a portal session.
78pub struct PwCore {
79    api: Api,
80    thread_loop: *mut c_void,
81    context: *mut c_void,
82    core: *mut c_void,
83}
84
85unsafe impl Send for PwCore {}
86unsafe impl Sync for PwCore {}
87
88impl PwCore {
89    /// Connect on the fd the portal handed out (PipeWire owns and closes it).
90    pub fn connect(fd: OwnedFd) -> Result<Arc<Self>, String> {
91        let api = *api()?;
92        unsafe {
93            let thread_loop = (api.thread_loop_new)(c"pixelflux-portal-pw".as_ptr(), ptr::null());
94            if thread_loop.is_null() {
95                return Err("pw_thread_loop_new failed".into());
96            }
97            let mut this = PwCore {
98                api,
99                thread_loop,
100                context: ptr::null_mut(),
101                core: ptr::null_mut(),
102            };
103            this.context =
104                (api.context_new)((api.thread_loop_get_loop)(thread_loop), ptr::null_mut(), 0);
105            if this.context.is_null() {
106                return Err("pw_context_new failed".into());
107            }
108            if (api.thread_loop_start)(thread_loop) < 0 {
109                return Err("pw_thread_loop_start failed".into());
110            }
111            (api.thread_loop_lock)(thread_loop);
112            this.core = (api.context_connect_fd)(
113                this.context,
114                std::os::fd::IntoRawFd::into_raw_fd(fd),
115                ptr::null_mut(),
116                0,
117            );
118            (api.thread_loop_unlock)(thread_loop);
119            if this.core.is_null() {
120                return Err("pw_context_connect_fd failed".into());
121            }
122            Ok(Arc::new(this))
123        }
124    }
125
126    /// Run `f` with the loop locked, which every stream call from outside the loop needs.
127    fn locked<R>(&self, f: impl FnOnce() -> R) -> R {
128        unsafe { (self.api.thread_loop_lock)(self.thread_loop) };
129        let r = f();
130        unsafe { (self.api.thread_loop_unlock)(self.thread_loop) };
131        r
132    }
133}
134
135impl Drop for PwCore {
136    fn drop(&mut self) {
137        unsafe {
138            (self.api.thread_loop_lock)(self.thread_loop);
139            if !self.core.is_null() {
140                (self.api.core_disconnect)(self.core);
141            }
142            (self.api.thread_loop_unlock)(self.thread_loop);
143            (self.api.thread_loop_stop)(self.thread_loop);
144            if !self.context.is_null() {
145                (self.api.context_destroy)(self.context);
146            }
147            (self.api.thread_loop_destroy)(self.thread_loop);
148        }
149    }
150}
151
152/// What a stream asks the compositor for.
153#[derive(Clone, Debug, PartialEq)]
154pub struct StreamConfig {
155    /// Offer dmabuf import (with `dma_formats`); off, only memfd frames are offered for.
156    pub zero_copy: bool,
157    /// `(fourcc, modifiers)` the encoder's display imports.
158    pub dma_formats: Vec<(u32, Vec<u64>)>,
159    /// Rate cap the compositor is asked to hold, in millihertz.
160    pub fps_milli: u32,
161    /// The size the consumer expects (the preferred value of the size range).
162    pub expect: (i32, i32),
163    /// Where the stream's cursor metas land in the union layout (`HOST_POINTER`). Not a
164    /// format matter: a change alone never renegotiates.
165    pub place: CursorPlace,
166}
167
168/// Where a stream's output sits for its cursor metas: the display's origin in the union layout
169/// and its capture size, as Selkies laid it out (`HostSession::set_layout`), and the output's
170/// logical size. A zero size is unknown.
171#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
172pub struct CursorPlace {
173    pub origin: (i32, i32),
174    pub size: (i32, i32),
175    pub logical: (i32, i32),
176}
177
178/// Host capture's pointer, for `WaylandBackend.pointer_location`: None without a host session;
179/// while one is attached, the newest cursor meta's position, or None before the first and
180/// where the host paints the cursor into its frames (no metas). The compositor's own
181/// `PointerShare` is not read meanwhile: it stops at its last pass before the host attached.
182/// Process-wide like the backend slot.
183pub static HOST_POINTER: Mutex<Option<Option<crate::wayland::frontend::PointerAt>>> =
184    Mutex::new(None);
185/// The motion numbers handed to the host session, each with when it was sent (CLOCK_MONOTONIC
186/// ns), newest last: a cursor meta claims the moves sent before its buffer was stamped
187/// (`motion_at`), so one from a frame captured ahead of the latest move does not claim it.
188static HOST_MOTIONS: Mutex<std::collections::VecDeque<(u64, i64)>> =
189    Mutex::new(std::collections::VecDeque::new());
190/// Sent moves remembered for `motion_at`: well over a frame's worth at any input rate.
191const HOST_MOTIONS_KEPT: usize = 256;
192
193/// Note motion number `motion` handed to the host session now.
194pub fn note_host_motion(motion: u64) {
195    let mut sent = HOST_MOTIONS.lock().unwrap();
196    if sent.back().is_some_and(|&(last, _)| last >= motion) {
197        return;
198    }
199    if sent.len() == HOST_MOTIONS_KEPT {
200        sent.pop_front();
201    }
202    sent.push_back((motion, now_ns()));
203}
204
205/// The newest motion number of `sent` handed over at or before `stamp`; 0 before any.
206fn motion_at(sent: &std::collections::VecDeque<(u64, i64)>, stamp: i64) -> u64 {
207    sent.iter()
208        .rev()
209        .find(|&&(_, at)| at <= stamp)
210        .map_or(0, |&(motion, _)| motion)
211}
212
213/// When a buffer was captured, for the moves its cursor meta claims: the compositor's stamp
214/// where it is plausible (no later than `arrival`, at most four periods older), else `arrival`.
215fn meta_stamp(pts: Option<i64>, arrival: i64, period_ns: i64) -> i64 {
216    pts.filter(|&pts| pts <= arrival && arrival - pts <= STAMP_MAX_AGE_PERIODS * period_ns)
217        .unwrap_or(arrival)
218}
219
220/// A cursor meta's position `(x, y)` in a stream negotiated at `stream` pixels as the pointer
221/// in the union layout: the display's origin plus the position scaled to its capture size,
222/// the inverse of the mapping absolute motion is injected through (`host::stream_point`).
223/// The scale is the output's capture pixels per logical pixel, what a relative pixel moves.
224pub(crate) fn host_pointer_at(
225    place: CursorPlace,
226    stream: (i32, i32),
227    x: i32,
228    y: i32,
229    motion: u64,
230) -> crate::wayland::frontend::PointerAt {
231    let ratio = |to: i32, from: i32| {
232        if to > 0 && from > 0 {
233            f64::from(to) / f64::from(from)
234        } else {
235            1.0
236        }
237    };
238    crate::wayland::frontend::PointerAt {
239        x: f64::from(place.origin.0) + f64::from(x) * ratio(place.size.0, stream.0),
240        y: f64::from(place.origin.1) + f64::from(y) * ratio(place.size.1, stream.1),
241        scale: ratio(place.size.0, place.logical.0),
242        motion,
243    }
244}
245
246/// The pointer `WaylandBackend.pointer_location` reports: under host capture (`Some`), host
247/// capture's alone, None included; otherwise the compositor's own.
248pub fn prefer_host_pointer<T>(
249    host: Option<Option<T>>,
250    share: impl FnOnce() -> Option<T>,
251) -> Option<T> {
252    match host {
253        Some(at) => at,
254        None => share(),
255    }
256}
257
258/// Publish a cursor meta's stream-pixel position to `HOST_POINTER` while a host session holds it,
259/// claiming the moves up to `motion`.
260fn publish_host_pointer(shared: &Shared, x: i32, y: i32, motion: u64) {
261    let place = shared.cfg.lock().unwrap().place;
262    let stream = shared
263        .negotiated
264        .lock()
265        .unwrap()
266        .map_or((0, 0), |n| (n.width, n.height));
267    let at = host_pointer_at(place, stream, x, y, motion);
268    if let Some(slot) = HOST_POINTER.lock().unwrap().as_mut() {
269        *slot = Some(at);
270    }
271}
272
273/// The fixated format of a stream.
274#[derive(Clone, Copy, Debug, PartialEq, Eq)]
275pub struct Negotiated {
276    pub width: i32,
277    pub height: i32,
278    pub fourcc: u32,
279    pub modifier: Option<u64>,
280}
281
282/// One buffer the compositor handed the stream: its PipeWire handle plus the import built
283/// on first use, kept for the buffer's life so the same fds reach the encoder each time.
284#[derive(Default)]
285struct BufferSlot {
286    pw: *mut PwBuffer,
287    dmabuf: Option<Dmabuf>,
288    cpu: Option<Arc<memmap2::Mmap>>,
289    /// Published to the consumer and not yet released: PipeWire does not own it meanwhile.
290    held: bool,
291}
292
293/// State shared with the stream callbacks (handed to PipeWire as the listener's user data).
294struct Shared {
295    api: Api,
296    stream: AtomicPtr<c_void>,
297    sink: FrameSink,
298    cursor_tx: std::sync::mpsc::Sender<CursorJob>,
299    cfg: Mutex<StreamConfig>,
300    negotiated: Mutex<Option<Negotiated>>,
301    buffers: Mutex<Vec<BufferSlot>>,
302    /// Bumped on every renegotiation; a release naming an older generation is ignored.
303    generation: AtomicU64,
304    state: AtomicI32,
305    errored: AtomicBool,
306    frames: AtomicU64,
307    stamps: Mutex<Stamps>,
308    cursor_pos: AtomicI64,
309    cursor_hash: AtomicU64,
310    /// The pods handed to `pw_stream_update_params`, alive for the stream's life.
311    params: Mutex<Vec<Vec<u8>>>,
312}
313
314// The raw PipeWire buffer pointers in the slots are only dereferenced on the stream's own loop
315// or under its lock; everything else in the state is atomics and mutexes.
316unsafe impl Send for Shared {}
317unsafe impl Sync for Shared {}
318
319fn enum_format_pod(spa_format: u32, modifiers: Option<&[u64]>, cfg: &StreamConfig) -> Vec<u8> {
320    object(SPA_TYPE_OBJECT_FORMAT, SPA_PARAM_ENUM_FORMAT, |p| {
321        prop(p, SPA_FORMAT_MEDIA_TYPE, |v| {
322            pod_id(v, SPA_MEDIA_TYPE_VIDEO)
323        });
324        prop(p, SPA_FORMAT_MEDIA_SUBTYPE, |v| {
325            pod_id(v, SPA_MEDIA_SUBTYPE_RAW)
326        });
327        prop(p, SPA_FORMAT_VIDEO_FORMAT, |v| pod_id(v, spa_format));
328        if let Some(mods) = modifiers {
329            let mut values: Vec<i64> = Vec::with_capacity(mods.len() + 1);
330            values.push(mods[0] as i64);
331            values.extend(mods.iter().map(|m| *m as i64));
332            prop_flags(
333                p,
334                SPA_FORMAT_VIDEO_MODIFIER,
335                SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE,
336                |v| pod_choice_long(v, SPA_CHOICE_ENUM, &values),
337            );
338        }
339        let (w, h) = (cfg.expect.0.max(1) as u32, cfg.expect.1.max(1) as u32);
340        prop(p, SPA_FORMAT_VIDEO_SIZE, |v| {
341            pod_choice_rect(v, SPA_CHOICE_RANGE, &[(w, h), (1, 1), (16384, 16384)])
342        });
343        prop(p, SPA_FORMAT_VIDEO_FRAMERATE, |v| pod_frac(v, 0, 1));
344        let fps = (cfg.fps_milli.max(1000), 1000);
345        prop(p, SPA_FORMAT_VIDEO_MAX_FRAMERATE, |v| {
346            pod_choice_frac(v, SPA_CHOICE_RANGE, &[fps, (1, 1), fps])
347        });
348    })
349}
350
351/// The `EnumFormat` list for `cfg`: modifier-bearing entries for every importable fourcc when
352/// dmabufs are wanted, then the plain entries every compositor answers with memfd buffers.
353fn enum_format_pods(cfg: &StreamConfig) -> Vec<Vec<u8>> {
354    let mut pods = Vec::new();
355    if cfg.zero_copy {
356        for (fourcc, mods) in &cfg.dma_formats {
357            let spa = match *fourcc {
358                FOURCC_XR24 => SPA_VIDEO_FORMAT_BGRX,
359                FOURCC_AR24 => SPA_VIDEO_FORMAT_BGRA,
360                _ => continue,
361            };
362            if !mods.is_empty() {
363                pods.push(enum_format_pod(spa, Some(mods), cfg));
364            }
365        }
366    }
367    for (spa, _) in SHM_FORMATS {
368        pods.push(enum_format_pod(spa, None, cfg));
369    }
370    pods
371}
372
373/// Buffers and metadata asked for once the format is fixated: dmabuf or memfd blocks per the
374/// modifier's presence, three to eight buffers so one can be held retained while another is
375/// in flight and the compositor still has one to fill, plus header, damage, and cursor metas.
376fn negotiated_params(n: &Negotiated, dmabuf: bool) -> Vec<Vec<u8>> {
377    let data_type = 1i32
378        << if dmabuf {
379            SPA_DATA_DMABUF
380        } else {
381            SPA_DATA_MEMFD
382        };
383    let buffers = object(SPA_TYPE_OBJECT_PARAM_BUFFERS, SPA_PARAM_BUFFERS, |p| {
384        prop(p, SPA_PARAM_BUFFERS_BUFFERS, |v| {
385            pod_choice_int(v, SPA_CHOICE_RANGE, &[4, 3, 8])
386        });
387        prop(p, SPA_PARAM_BUFFERS_BLOCKS, |v| pod_int(v, 1));
388        prop(p, SPA_PARAM_BUFFERS_SIZE, |v| {
389            pod_int(v, n.width * n.height * 4)
390        });
391        prop(p, SPA_PARAM_BUFFERS_STRIDE, |v| pod_int(v, n.width * 4));
392        prop(p, SPA_PARAM_BUFFERS_ALIGN, |v| pod_int(v, 16));
393        prop(p, SPA_PARAM_BUFFERS_DATATYPE, |v| pod_int(v, data_type));
394    });
395    let region = SPA_META_REGION_SIZE as i32;
396    vec![
397        buffers,
398        meta(SPA_META_HEADER, |v| pod_int(v, SPA_META_HEADER_SIZE as i32)),
399        meta(SPA_META_VIDEO_DAMAGE, |v| {
400            pod_choice_int(
401                v,
402                SPA_CHOICE_RANGE,
403                &[4 * region, region, MAX_DAMAGE_REGIONS as i32 * region],
404            )
405        }),
406        meta(SPA_META_VIDEO_CROP, |v| pod_int(v, region)),
407        meta(SPA_META_CURSOR, |v| {
408            pod_choice_int(
409                v,
410                SPA_CHOICE_RANGE,
411                &[
412                    cursor_meta_size(64, 64) as i32,
413                    cursor_meta_size(1, 1) as i32,
414                    cursor_meta_size(MAX_CURSOR_EDGE, MAX_CURSOR_EDGE) as i32,
415                ],
416            )
417        }),
418    ]
419}
420
421/// Whether moving a stream's ask from `cur` to `new` renegotiates (`reconfigure`): anything but
422/// the preferred size does, and that only before a format stands.
423fn renegotiates(cur: &StreamConfig, new: &StreamConfig, negotiated: bool) -> bool {
424    let size_only = StreamConfig {
425        expect: cur.expect,
426        place: cur.place,
427        ..new.clone()
428    } == *cur;
429    !size_only || !negotiated
430}
431
432/// A `Meta` param asking for one metadata type of the given size (a plain or ranged Int).
433fn meta(ty: u32, size: impl FnOnce(&mut Vec<u8>)) -> Vec<u8> {
434    object(SPA_TYPE_OBJECT_PARAM_META, SPA_PARAM_META, |p| {
435        prop(p, SPA_PARAM_META_TYPE, |v| pod_id(v, ty));
436        prop(p, SPA_PARAM_META_SIZE, size);
437    })
438}
439
440/// The fixated `Format` param: pixel format, size, and whether a modifier (dmabuf) was agreed.
441fn parse_format(pod: &[u8]) -> Option<Negotiated> {
442    let (ty, _, props) = object_props(pod)?;
443    if ty != SPA_TYPE_OBJECT_FORMAT {
444        return None;
445    }
446    let spa = find_prop(&props, SPA_FORMAT_VIDEO_FORMAT)?.as_u32()?;
447    let (w, h) = find_prop(&props, SPA_FORMAT_VIDEO_SIZE)?.as_pair()?;
448    let modifier = find_prop(&props, SPA_FORMAT_VIDEO_MODIFIER)
449        .and_then(|p| p.as_i64())
450        .map(|m| m as u64);
451    Some(Negotiated {
452        width: w as i32,
453        height: h as i32,
454        fourcc: spa_to_fourcc(spa)?,
455        modifier,
456    })
457}
458
459unsafe extern "C" fn on_state_changed(
460    data: *mut c_void,
461    _old: c_int,
462    new: c_int,
463    error: *const c_char,
464) {
465    let shared = unsafe { &*(data as *const Shared) };
466    shared.state.store(new, Ordering::Release);
467    let name = match new {
468        PW_STREAM_STATE_ERROR => "error",
469        PW_STREAM_STATE_UNCONNECTED => "unconnected",
470        PW_STREAM_STATE_PAUSED => "paused",
471        PW_STREAM_STATE_STREAMING => "streaming",
472        _ => "connecting",
473    };
474    let msg = if error.is_null() {
475        String::new()
476    } else {
477        unsafe { CStr::from_ptr(error) }
478            .to_string_lossy()
479            .into_owned()
480    };
481    eprintln!(
482        "[HostCapture] output {}: portal stream {name} {msg}",
483        shared.sink.index
484    );
485    if new == PW_STREAM_STATE_ERROR {
486        shared.errored.store(true, Ordering::Release);
487    }
488}
489
490unsafe extern "C" fn on_param_changed(data: *mut c_void, id: u32, param: *const c_void) {
491    if id != SPA_PARAM_FORMAT {
492        return;
493    }
494    let shared = unsafe { &*(data as *const Shared) };
495    shared.generation.fetch_add(1, Ordering::AcqRel);
496    if param.is_null() {
497        *shared.negotiated.lock().unwrap() = None;
498        return;
499    }
500    let Some(negotiated) = parse_format(unsafe { pod_bytes(param) }) else {
501        eprintln!(
502            "[HostCapture] output {}: unusable PipeWire format",
503            shared.sink.index
504        );
505        return;
506    };
507    eprintln!(
508        "[HostCapture] output {} portal stream: {}x{} {} ({})",
509        shared.sink.index,
510        negotiated.width,
511        negotiated.height,
512        String::from_utf8_lossy(&negotiated.fourcc.to_le_bytes()),
513        match negotiated.modifier {
514            Some(m) => format!("dmabuf, modifier {m:#x}"),
515            None => "memfd".to_string(),
516        }
517    );
518    let pods = negotiated_params(&negotiated, negotiated.modifier.is_some());
519    *shared.negotiated.lock().unwrap() = Some(negotiated);
520    let stream = shared.stream.load(Ordering::Acquire);
521    if stream.is_null() {
522        return;
523    }
524    let mut ptrs: Vec<*const c_void> = pods.iter().map(|p| p.as_ptr() as *const c_void).collect();
525    unsafe { (shared.api.stream_update_params)(stream, ptrs.as_mut_ptr(), ptrs.len() as u32) };
526    *shared.params.lock().unwrap() = pods;
527}
528
529unsafe extern "C" fn on_add_buffer(data: *mut c_void, b: *mut PwBuffer) {
530    let shared = unsafe { &*(data as *const Shared) };
531    let mut buffers = shared.buffers.lock().unwrap();
532    let idx = buffers
533        .iter()
534        .position(|s| s.pw.is_null())
535        .unwrap_or_else(|| {
536            buffers.push(BufferSlot::default());
537            buffers.len() - 1
538        });
539    buffers[idx] = BufferSlot {
540        pw: b,
541        ..BufferSlot::default()
542    };
543    unsafe { (*b).user_data = idx as *mut c_void };
544}
545
546unsafe extern "C" fn on_remove_buffer(data: *mut c_void, b: *mut PwBuffer) {
547    let shared = unsafe { &*(data as *const Shared) };
548    let idx = unsafe { (*b).user_data } as usize;
549    if let Some(slot) = shared.buffers.lock().unwrap().get_mut(idx) {
550        *slot = BufferSlot::default();
551    }
552}
553
554/// Read the cursor meta of a buffer: position, and a changed sprite forwarded to the cursor
555/// worker (an empty sprite hides it).
556unsafe fn read_cursor(shared: &Shared, meta: &SpaMeta, motion: u64) {
557    if meta.size < std::mem::size_of::<SpaMetaCursor>() as u32 || meta.data.is_null() {
558        return;
559    }
560    let cursor = unsafe { &*(meta.data as *const SpaMetaCursor) };
561    if cursor.id == 0 {
562        return;
563    }
564    shared.cursor_pos.store(
565        (i64::from(cursor.x) << 32) | i64::from(cursor.y as u32),
566        Ordering::Relaxed,
567    );
568    publish_host_pointer(shared, cursor.x, cursor.y, motion);
569    if cursor.bitmap_offset < std::mem::size_of::<SpaMetaCursor>() as u32
570        || cursor.bitmap_offset + std::mem::size_of::<SpaMetaBitmap>() as u32 > meta.size
571    {
572        return;
573    }
574    let bitmap = unsafe {
575        &*((meta.data as *const u8).add(cursor.bitmap_offset as usize) as *const SpaMetaBitmap)
576    };
577    if bitmap.format == 0 {
578        return;
579    }
580    let (w, h) = (bitmap.width, bitmap.height);
581    if w == 0 || h == 0 || bitmap.offset == 0 {
582        if shared.cursor_hash.swap(0, Ordering::Relaxed) != 0 {
583            let _ = shared.cursor_tx.send(CursorJob::Hide);
584        }
585        return;
586    }
587    let stride = bitmap.stride.max(0) as usize;
588    let end = cursor.bitmap_offset as usize
589        + bitmap.offset as usize
590        + stride * (h as usize - 1)
591        + w as usize * 4;
592    if end > meta.size as usize || w > MAX_CURSOR_EDGE || h > MAX_CURSOR_EDGE {
593        return;
594    }
595    let pixels = unsafe {
596        (meta.data as *const u8).add(cursor.bitmap_offset as usize + bitmap.offset as usize)
597    };
598    let mut rgba = Vec::with_capacity((w * h * 4) as usize);
599    for y in 0..h as usize {
600        let row = unsafe { std::slice::from_raw_parts(pixels.add(y * stride), w as usize * 4) };
601        for px in row.as_chunks::<4>().0 {
602            let (r, g, b, a) = match bitmap.format {
603                SPA_VIDEO_FORMAT_BGRA | SPA_VIDEO_FORMAT_BGRX => (
604                    px[2],
605                    px[1],
606                    px[0],
607                    if bitmap.format == SPA_VIDEO_FORMAT_BGRX {
608                        0xff
609                    } else {
610                        px[3]
611                    },
612                ),
613                SPA_VIDEO_FORMAT_ARGB => (px[1], px[2], px[3], px[0]),
614                SPA_VIDEO_FORMAT_ABGR => (px[3], px[2], px[1], px[0]),
615                _ => (
616                    px[0],
617                    px[1],
618                    px[2],
619                    if bitmap.format == SPA_VIDEO_FORMAT_RGBX {
620                        0xff
621                    } else {
622                        px[3]
623                    },
624                ),
625            };
626            rgba.extend_from_slice(&[r, g, b, a]);
627        }
628    }
629    let hash = xxhash_rust::xxh3::xxh3_64(&rgba) ^ (u64::from(w) << 32 | u64::from(h));
630    let previous = shared.cursor_hash.swap(hash, Ordering::Relaxed);
631    if previous != hash {
632        if previous == 0 {
633            eprintln!(
634                "[HostCapture] output {}: portal cursor sprites arrive ({w}x{h}, hotspot {},{})",
635                shared.sink.index, cursor.hot_x, cursor.hot_y
636            );
637        }
638        let _ = shared.cursor_tx.send(CursorJob::Gles {
639            hash,
640            width: w as i32,
641            height: h as i32,
642            bytes: rgba,
643            hot_x: cursor.hot_x,
644            hot_y: cursor.hot_y,
645        });
646    }
647}
648
649/// The damage regions of a buffer, or the whole frame when the compositor sent none.
650unsafe fn read_damage(buf: &SpaBuffer, width: i32, height: i32) -> Vec<Rectangle<i32, Physical>> {
651    let mut damage = Vec::new();
652    for i in 0..buf.n_metas as usize {
653        let m = unsafe { &*buf.metas.add(i) };
654        if m.type_ != SPA_META_VIDEO_DAMAGE || m.data.is_null() {
655            continue;
656        }
657        let n = m.size as usize / std::mem::size_of::<SpaMetaRegion>();
658        for j in 0..n {
659            let r = unsafe { *(m.data as *const SpaMetaRegion).add(j) };
660            if r.width == 0 || r.height == 0 {
661                break;
662            }
663            damage.push(Rectangle::new(
664                (r.x, r.y).into(),
665                (r.width as i32, r.height as i32).into(),
666            ));
667        }
668    }
669    if damage.is_empty() {
670        damage.push(Rectangle::new((0, 0).into(), (width, height).into()));
671    }
672    damage
673}
674
675/// How many frame periods before its arrival a compositor's stamp may lie and still be taken
676/// as the frame's capture instant (`Stamps`).
677const STAMP_MAX_AGE_PERIODS: i64 = 4;
678
679/// Why a portal frame's capture instant is its arrival rather than the compositor's stamp.
680#[derive(Clone, Copy, Debug, PartialEq, Eq)]
681enum StampRefusal {
682    Missing,
683    Ahead,
684    Stale,
685    Repeated,
686}
687
688impl StampRefusal {
689    fn says(self) -> &'static str {
690        match self {
691            StampRefusal::Missing => "missing",
692            StampRefusal::Ahead => "ahead of the frame's arrival",
693            StampRefusal::Stale => "older than four frame periods at arrival",
694            StampRefusal::Repeated => "no later than the last frame's",
695        }
696    }
697}
698
699/// The capture instant of each portal frame, which the encoded frame carries as `capture_ns`
700/// into the RTP timestamp, abs-capture-time and pipeline_ms: the compositor's header stamp
701/// where it reads as a `CLOCK_MONOTONIC` instant of this frame, else the frame's arrival. A
702/// stamp is taken when it is no later than the arrival, at most `STAMP_MAX_AGE_PERIODS`
703/// frame periods before it, and later than the last instant given, so a stamp reused from an
704/// earlier frame, one on another clock's scale, or one ahead of now moves neither. Which source
705/// each refusal falls back to is logged the first time it happens on a stream.
706#[derive(Default)]
707struct Stamps {
708    last: i64,
709    refusals_seen: u8,
710}
711
712impl Stamps {
713    fn instant(
714        &mut self,
715        pts: Option<i64>,
716        arrival: i64,
717        period_ns: i64,
718    ) -> (i64, Option<StampRefusal>) {
719        let refusal = match pts {
720            None => Some(StampRefusal::Missing),
721            Some(pts) if pts > arrival => Some(StampRefusal::Ahead),
722            Some(pts) if arrival - pts > STAMP_MAX_AGE_PERIODS * period_ns => {
723                Some(StampRefusal::Stale)
724            }
725            Some(pts) if pts <= self.last => Some(StampRefusal::Repeated),
726            Some(_) => None,
727        };
728        let at = match (pts, refusal) {
729            (Some(pts), None) => pts,
730            _ => arrival.max(self.last + 1),
731        };
732        self.last = at;
733        (at, refusal)
734    }
735
736    /// Whether this refusal is the first of its kind on the stream.
737    fn first(&mut self, refusal: StampRefusal) -> bool {
738        let bit = 1 << refusal as u8;
739        let first = self.refusals_seen & bit == 0;
740        self.refusals_seen |= bit;
741        first
742    }
743}
744
745/// The compositor's stamp from the buffer's header meta, when it carries one.
746fn header_pts(buf: &SpaBuffer) -> Option<i64> {
747    (0..buf.n_metas as usize)
748        .map(|i| unsafe { &*buf.metas.add(i) })
749        .find(|m| m.type_ == SPA_META_HEADER && m.size >= SPA_META_HEADER_SIZE && !m.data.is_null())
750        .map(|m| unsafe { (*(m.data as *const SpaMetaHeader)).pts })
751        .filter(|pts| *pts > 0)
752}
753
754/// Build the slot's import on its first frame: the dmabuf planes as a `Dmabuf` of duplicated
755/// fds, or the memfd mapped whole.
756unsafe fn import_slot(slot: &mut BufferSlot, buf: &SpaBuffer, n: &Negotiated) -> bool {
757    if slot.dmabuf.is_some() || slot.cpu.is_some() {
758        return true;
759    }
760    let d0 = unsafe { &*buf.datas };
761    if d0.type_ == SPA_DATA_DMABUF {
762        let mut builder = Dmabuf::builder(
763            (n.width, n.height),
764            Fourcc::try_from(n.fourcc).unwrap_or(Fourcc::Xrgb8888),
765            Modifier::from(n.modifier.unwrap_or(u64::from(Modifier::Invalid))),
766            DmabufFlags::empty(),
767        );
768        for i in 0..buf.n_datas as usize {
769            let d = unsafe { &*buf.datas.add(i) };
770            let dup = unsafe { libc::dup(d.fd as c_int) };
771            if dup < 0 {
772                return false;
773            }
774            let chunk = unsafe { &*d.chunk };
775            builder.add_plane(
776                unsafe { OwnedFd::from_raw_fd(dup) },
777                chunk.offset,
778                chunk.stride as u32,
779            );
780        }
781        slot.dmabuf = builder.build();
782        slot.dmabuf.is_some()
783    } else if d0.type_ == SPA_DATA_MEMFD {
784        let file = unsafe { std::fs::File::from_raw_fd(libc::dup(d0.fd as c_int)) };
785        let map = unsafe {
786            memmap2::MmapOptions::new()
787                .offset(u64::from(d0.mapoffset))
788                .len(d0.maxsize as usize)
789                .map(&file)
790        };
791        match map {
792            Ok(m) => {
793                slot.cpu = Some(Arc::new(m));
794                true
795            }
796            Err(e) => {
797                eprintln!("[HostCapture] memfd map: {e}");
798                false
799            }
800        }
801    } else {
802        false
803    }
804}
805
806unsafe extern "C" fn on_process(data: *mut c_void) {
807    let shared = unsafe { &*(data as *const Shared) };
808    let stream = shared.stream.load(Ordering::Acquire);
809    if stream.is_null() {
810        return;
811    }
812    let Some(n) = *shared.negotiated.lock().unwrap() else {
813        return;
814    };
815    // Take everything queued: cursor-only buffers go straight back, and of several frames
816    // only the newest is worth publishing.
817    let arrival = now_ns();
818    let period_ns = 1_000_000_000_000 / i64::from(shared.cfg.lock().unwrap().fps_milli.max(1000));
819    let mut newest: Option<ptr::NonNull<PwBuffer>> = None;
820    loop {
821        let b = unsafe { (shared.api.stream_dequeue_buffer)(stream) };
822        if b.is_null() {
823            break;
824        }
825        let buf = unsafe { &*(*b).buffer };
826        for i in 0..buf.n_metas as usize {
827            let m = unsafe { &*buf.metas.add(i) };
828            if m.type_ == SPA_META_CURSOR {
829                let stamp = meta_stamp(header_pts(buf), arrival, period_ns);
830                let motion = motion_at(&HOST_MOTIONS.lock().unwrap(), stamp);
831                unsafe { read_cursor(shared, m, motion) };
832            }
833        }
834        let has_video = buf.n_datas > 0 && {
835            let d = unsafe { &*buf.datas };
836            let chunk = unsafe { &*d.chunk };
837            chunk.size > 0 && chunk.flags & SPA_CHUNK_FLAG_CORRUPTED == 0
838        };
839        if has_video {
840            if let Some(previous) = newest {
841                unsafe { (shared.api.stream_queue_buffer)(stream, previous.as_ptr()) };
842            }
843            newest = ptr::NonNull::new(b);
844        } else {
845            unsafe { (shared.api.stream_queue_buffer)(stream, b) };
846        }
847    }
848    let Some(newest) = newest.map(|p| p.as_ptr()) else {
849        return;
850    };
851    let buf = unsafe { &*(*newest).buffer };
852    let idx = unsafe { (*newest).user_data } as usize;
853    let generation = shared.generation.load(Ordering::Acquire);
854    let pts = header_pts(buf);
855    let arrival = now_ns();
856    let (stamp_ns, refusal) = shared
857        .stamps
858        .lock()
859        .unwrap()
860        .instant(pts, arrival, period_ns);
861    let frame = {
862        let mut buffers = shared.buffers.lock().unwrap();
863        let Some(slot) = buffers.get_mut(idx).filter(|s| s.pw == newest) else {
864            unsafe { (shared.api.stream_queue_buffer)(stream, newest) };
865            return;
866        };
867        if !unsafe { import_slot(slot, buf, &n) } {
868            unsafe { (shared.api.stream_queue_buffer)(stream, newest) };
869            return;
870        }
871        let chunk = unsafe { &*(*buf.datas).chunk };
872        let cpu = slot.cpu.as_ref().map(|map| HostCpuFrame {
873            map: map.clone(),
874            offset: chunk.offset as usize,
875            stride: if chunk.stride > 0 {
876                chunk.stride as usize
877            } else {
878                n.width as usize * 4
879            },
880            format: shm_code(n.fourcc),
881        });
882        slot.held = true;
883        HostFrame {
884            generation,
885            slot: idx,
886            dmabuf: slot.dmabuf.clone(),
887            cpu,
888            width: n.width,
889            height: n.height,
890            damage: unsafe { read_damage(buf, n.width, n.height) },
891            stamp_ns,
892        }
893    };
894    let count = shared.frames.fetch_add(1, Ordering::Relaxed);
895    let age_ms = (arrival - stamp_ns) as f64 / 1e6;
896    if count == 0 {
897        eprintln!(
898            "[HostCapture] output {}: first portal frame {}x{}, {} damage rect(s), {}",
899            shared.sink.index,
900            n.width,
901            n.height,
902            frame.damage.len(),
903            match refusal {
904                None => format!("{age_ms:.1} ms after the compositor stamped it"),
905                Some(why) => format!(
906                    "stamped at arrival: the compositor's stamp is {}",
907                    why.says()
908                ),
909            }
910        );
911    }
912    if let Some(why) = refusal.filter(|why| count > 0 && shared.stamps.lock().unwrap().first(*why))
913    {
914        eprintln!(
915            "[HostCapture] output {}: frame {count} stamped at arrival: the compositor's stamp is {}",
916            shared.sink.index,
917            why.says()
918        );
919    }
920    if trace() {
921        eprintln!(
922            "[HostTrace] output {} frame {count} process +{age_ms:.2}ms pts {} arrival {arrival}{}",
923            shared.sink.index,
924            pts.map_or("none".to_string(), |pts| pts.to_string()),
925            refusal.map_or(String::new(), |why| format!(" refused: {}", why.says()))
926        );
927    }
928    if !shared.sink.send(frame) {
929        if let Some(slot) = shared.buffers.lock().unwrap().get_mut(idx) {
930            slot.held = false;
931        }
932        unsafe { (shared.api.stream_queue_buffer)(stream, newest) };
933    }
934}
935
936/// One output's stream: the consumer end of a portal monitor stream.
937pub struct PwStream {
938    core: Arc<PwCore>,
939    stream: *mut c_void,
940    shared: Arc<Shared>,
941    _events: Box<PwStreamEvents>,
942    _hook: Box<[u64; 8]>,
943}
944
945unsafe impl Send for PwStream {}
946
947impl PwStream {
948    /// Connect to the stream node `node_id` and start negotiating `cfg`; frames reach `sink`,
949    /// cursor sprites `cursor_tx`.
950    pub(crate) fn connect(
951        core: &Arc<PwCore>,
952        node_id: u32,
953        sink: FrameSink,
954        cursor_tx: std::sync::mpsc::Sender<CursorJob>,
955        cfg: StreamConfig,
956    ) -> Result<Self, String> {
957        let api = core.api;
958        let pods = enum_format_pods(&cfg);
959        let shared = Arc::new(Shared {
960            api,
961            stream: AtomicPtr::new(ptr::null_mut()),
962            sink,
963            cursor_tx,
964            cfg: Mutex::new(cfg),
965            negotiated: Mutex::new(None),
966            buffers: Mutex::new(Vec::new()),
967            generation: AtomicU64::new(0),
968            state: AtomicI32::new(0),
969            errored: AtomicBool::new(false),
970            frames: AtomicU64::new(0),
971            stamps: Mutex::new(Stamps::default()),
972            cursor_pos: AtomicI64::new(i64::MIN),
973            cursor_hash: AtomicU64::new(0),
974            params: Mutex::new(Vec::new()),
975        });
976        let events = Box::new(PwStreamEvents {
977            state_changed: Some(on_state_changed),
978            param_changed: Some(on_param_changed),
979            add_buffer: Some(on_add_buffer),
980            remove_buffer: Some(on_remove_buffer),
981            process: Some(on_process),
982            ..PwStreamEvents::empty()
983        });
984        let mut this = PwStream {
985            core: core.clone(),
986            stream: ptr::null_mut(),
987            shared,
988            _events: events,
989            _hook: Box::new([0u64; 8]),
990        };
991        let result = core.locked(|| unsafe {
992            let props = (api.properties_new)(ptr::null::<c_char>());
993            if props.is_null() {
994                return Err("pw_properties_new failed".to_string());
995            }
996            for (k, v) in [
997                (c"media.type", c"Video"),
998                (c"media.category", c"Capture"),
999                (c"media.role", c"Screen"),
1000            ] {
1001                (api.properties_set)(props, k.as_ptr(), v.as_ptr());
1002            }
1003            this.stream = (api.stream_new)(core.core, c"pixelflux host capture".as_ptr(), props);
1004            if this.stream.is_null() {
1005                return Err("pw_stream_new failed".to_string());
1006            }
1007            this.shared.stream.store(this.stream, Ordering::Release);
1008            (api.stream_add_listener)(
1009                this.stream,
1010                this._hook.as_mut_ptr() as *mut c_void,
1011                &*this._events,
1012                Arc::as_ptr(&this.shared) as *mut c_void,
1013            );
1014            let mut ptrs: Vec<*const c_void> =
1015                pods.iter().map(|p| p.as_ptr() as *const c_void).collect();
1016            let rc = (api.stream_connect)(
1017                this.stream,
1018                PW_DIRECTION_INPUT,
1019                node_id,
1020                PW_STREAM_FLAG_AUTOCONNECT,
1021                ptrs.as_mut_ptr(),
1022                ptrs.len() as u32,
1023            );
1024            if rc < 0 {
1025                return Err(format!("pw_stream_connect failed ({rc})"));
1026            }
1027            Ok(())
1028        });
1029        *this.shared.params.lock().unwrap() = pods;
1030        result?;
1031        Ok(this)
1032    }
1033
1034    /// Offer a new configuration; unchanged asks cost nothing, a change renegotiates. A monitor
1035    /// stream's size is its monitor's whatever the consumer prefers, so once a format stands, a
1036    /// new preferred size alone is kept for later offers without renegotiating: a renegotiation
1037    /// drops the buffers in flight, and with them the cursor bitmap KWin sends once per change.
1038    pub fn reconfigure(&self, cfg: StreamConfig) {
1039        let pods = {
1040            let mut cur = self.shared.cfg.lock().unwrap();
1041            if *cur == cfg {
1042                return;
1043            }
1044            let renegotiate =
1045                renegotiates(&cur, &cfg, self.shared.negotiated.lock().unwrap().is_some());
1046            *cur = cfg;
1047            if !renegotiate {
1048                return;
1049            }
1050            enum_format_pods(&cur)
1051        };
1052        self.core.locked(|| unsafe {
1053            let mut ptrs: Vec<*const c_void> =
1054                pods.iter().map(|p| p.as_ptr() as *const c_void).collect();
1055            (self.shared.api.stream_update_params)(
1056                self.stream,
1057                ptrs.as_mut_ptr(),
1058                ptrs.len() as u32,
1059            );
1060        });
1061        *self.shared.params.lock().unwrap() = pods;
1062    }
1063
1064    /// Return a published frame's buffer to the compositor.
1065    pub fn release(&self, generation: u64, slot: usize) {
1066        if generation != self.shared.generation.load(Ordering::Acquire) {
1067            return;
1068        }
1069        self.core.locked(|| {
1070            let mut buffers = self.shared.buffers.lock().unwrap();
1071            if let Some(s) = buffers.get_mut(slot).filter(|s| s.held && !s.pw.is_null()) {
1072                s.held = false;
1073                unsafe { (self.shared.api.stream_queue_buffer)(self.stream, s.pw) };
1074            }
1075        });
1076    }
1077
1078    /// Return every held buffer (an idled output has no consumer for them).
1079    pub fn release_all(&self) {
1080        self.core.locked(|| {
1081            let mut buffers = self.shared.buffers.lock().unwrap();
1082            for s in buffers.iter_mut().filter(|s| s.held && !s.pw.is_null()) {
1083                s.held = false;
1084                unsafe { (self.shared.api.stream_queue_buffer)(self.stream, s.pw) };
1085            }
1086        });
1087    }
1088
1089    pub fn set_active(&self, active: bool) {
1090        self.core
1091            .locked(|| unsafe { (self.shared.api.stream_set_active)(self.stream, active) });
1092    }
1093
1094    pub fn negotiated(&self) -> Option<Negotiated> {
1095        *self.shared.negotiated.lock().unwrap()
1096    }
1097
1098    pub fn errored(&self) -> bool {
1099        self.shared.errored.load(Ordering::Acquire)
1100            || self.shared.state.load(Ordering::Acquire) == PW_STREAM_STATE_ERROR
1101    }
1102
1103    pub fn frames(&self) -> u64 {
1104        self.shared.frames.load(Ordering::Relaxed)
1105    }
1106
1107    /// The host cursor position the last cursor meta carried, in stream pixels.
1108    pub fn cursor_position(&self) -> Option<(i32, i32)> {
1109        let v = self.shared.cursor_pos.load(Ordering::Relaxed);
1110        (v != i64::MIN).then_some(((v >> 32) as i32, v as i32))
1111    }
1112
1113    /// Block up to `timeout` for the stream to reach a state where frames may flow.
1114    pub fn wait_negotiated(&self, timeout: Duration) -> bool {
1115        let deadline = Instant::now() + timeout;
1116        while Instant::now() < deadline {
1117            if self.negotiated().is_some() || self.errored() {
1118                return self.negotiated().is_some();
1119            }
1120            std::thread::sleep(Duration::from_millis(5));
1121        }
1122        false
1123    }
1124}
1125
1126impl Drop for PwStream {
1127    fn drop(&mut self) {
1128        self.core.locked(|| unsafe {
1129            self.shared.stream.store(ptr::null_mut(), Ordering::Release);
1130            if !self.stream.is_null() {
1131                (self.shared.api.stream_disconnect)(self.stream);
1132                (self.shared.api.stream_destroy)(self.stream);
1133            }
1134        });
1135    }
1136}
1137
1138#[cfg(test)]
1139mod tests {
1140    use super::*;
1141
1142    const PERIOD: i64 = 16_683_333;
1143    const NOW: i64 = 5_000_000_000_000;
1144
1145    /// A cursor meta lands at the display's union-layout origin plus its stream position
1146    /// scaled to the capture size, whatever the host's own layout; without known sizes it is
1147    /// taken at scale one from the origin.
1148    #[test]
1149    fn a_cursor_meta_is_placed_in_the_union_layout() {
1150        let place = CursorPlace {
1151            origin: (1920, 0),
1152            size: (4080, 2304),
1153            logical: (2040, 1152),
1154        };
1155        let at = host_pointer_at(place, (4080, 2304), 3000, 1000, 7);
1156        assert_eq!(
1157            (at.x, at.y, at.scale, at.motion),
1158            (1920.0 + 3000.0, 1000.0, 2.0, 7)
1159        );
1160        // A stream negotiated at the logical size.
1161        let at = host_pointer_at(place, (2040, 1152), 1500, 500, 7);
1162        assert_eq!((at.x, at.y), (1920.0 + 3000.0, 1000.0));
1163        let at = host_pointer_at(
1164            CursorPlace {
1165                origin: (100, 200),
1166                ..CursorPlace::default()
1167            },
1168            (0, 0),
1169            10,
1170            20,
1171            0,
1172        );
1173        assert_eq!((at.x, at.y, at.scale), (110.0, 220.0, 1.0));
1174    }
1175
1176    /// Under host capture the host pointer answers alone, None before its first cursor meta
1177    /// included; the compositor's share is read only without a host session.
1178    #[test]
1179    fn the_host_pointer_answers_alone_under_host_capture() {
1180        assert_eq!(
1181            prefer_host_pointer(Some(Some(1)), || panic!("share read")),
1182            Some(1)
1183        );
1184        assert_eq!(
1185            prefer_host_pointer::<u8>(Some(None), || panic!("share read")),
1186            None
1187        );
1188        assert_eq!(prefer_host_pointer(None, || Some(2)), Some(2));
1189        assert_eq!(prefer_host_pointer::<u8>(None, || None), None);
1190    }
1191
1192    /// A cursor meta claims the moves handed over by its buffer's stamp: none before the first,
1193    /// the newest at or before the stamp, never one sent after it.
1194    #[test]
1195    fn a_cursor_meta_claims_the_moves_sent_before_its_stamp() {
1196        let sent: std::collections::VecDeque<(u64, i64)> = [(3, 100), (4, 200), (7, 300)].into();
1197        assert_eq!(motion_at(&sent, 99), 0);
1198        assert_eq!(motion_at(&sent, 100), 3);
1199        assert_eq!(motion_at(&sent, 250), 4);
1200        assert_eq!(motion_at(&sent, 1000), 7);
1201    }
1202
1203    /// The compositor's stamp stands where it is plausible; a stamp ahead of the arrival, or
1204    /// older than four periods, gives way to the arrival.
1205    #[test]
1206    fn a_meta_is_stamped_by_its_buffer_where_plausible() {
1207        let period = 4_000_000;
1208        assert_eq!(meta_stamp(Some(9_000_000), 10_000_000, period), 9_000_000);
1209        assert_eq!(meta_stamp(Some(11_000_000), 10_000_000, period), 10_000_000);
1210        assert_eq!(meta_stamp(Some(1_000), 20_000_000, period), 20_000_000);
1211        assert_eq!(meta_stamp(None, 10_000_000, period), 10_000_000);
1212    }
1213
1214    /// Moving a display in the union layout re-places its cursor metas without renegotiating
1215    /// the stream, which would drop the buffers in flight.
1216    #[test]
1217    fn a_new_place_alone_does_not_renegotiate() {
1218        let cur = cfg(true);
1219        let moved = StreamConfig {
1220            place: CursorPlace {
1221                origin: (1920, 0),
1222                ..cur.place
1223            },
1224            ..cur.clone()
1225        };
1226        assert!(!renegotiates(&cur, &moved, true));
1227    }
1228
1229    /// A stamp a little before its frame's arrival, later than the last, is the capture
1230    /// instant; each refusal falls back to the arrival, which still advances past the last.
1231    #[test]
1232    fn a_compositor_stamp_is_taken_only_where_it_is_plausible() {
1233        let mut stamps = Stamps::default();
1234        assert_eq!(
1235            stamps.instant(Some(NOW - 3_000_000), NOW, PERIOD),
1236            (NOW - 3_000_000, None)
1237        );
1238        let next = NOW + PERIOD;
1239        assert_eq!(
1240            stamps.instant(Some(NOW - 3_000_000), next, PERIOD),
1241            (next, Some(StampRefusal::Repeated)),
1242            "a stamp reused from the previous frame"
1243        );
1244        let next = next + PERIOD;
1245        assert_eq!(
1246            stamps.instant(Some(next + 1), next, PERIOD),
1247            (next, Some(StampRefusal::Ahead)),
1248            "a stamp ahead of the arrival"
1249        );
1250        let next = next + PERIOD;
1251        assert_eq!(
1252            stamps.instant(Some(next - 5 * PERIOD), next, PERIOD),
1253            (next, Some(StampRefusal::Stale)),
1254            "a stamp five frame periods old"
1255        );
1256        let next = next + PERIOD;
1257        assert_eq!(
1258            stamps.instant(Some(1_759_000_000_000_000_000), next, PERIOD),
1259            (next, Some(StampRefusal::Ahead)),
1260            "a CLOCK_REALTIME stamp"
1261        );
1262        let next = next + PERIOD;
1263        assert_eq!(
1264            stamps.instant(Some(40_000_000), next, PERIOD),
1265            (next, Some(StampRefusal::Stale)),
1266            "a stamp counted from the stream's start"
1267        );
1268        let next = next + PERIOD;
1269        assert_eq!(
1270            stamps.instant(None, next, PERIOD),
1271            (next, Some(StampRefusal::Missing)),
1272            "no stamp (the header meta absent, or zero)"
1273        );
1274        let next = next + PERIOD;
1275        assert_eq!(
1276            stamps.instant(Some(next - 2_000_000), next, PERIOD),
1277            (next - 2_000_000, None)
1278        );
1279    }
1280
1281    /// A stamp that is plausible on its own but earlier than an arrival given to the frame
1282    /// before is refused too, so the instants never go back.
1283    #[test]
1284    fn the_instants_never_go_back() {
1285        let mut stamps = Stamps::default();
1286        let (first, why) = stamps.instant(None, NOW, PERIOD);
1287        assert_eq!((first, why), (NOW, Some(StampRefusal::Missing)));
1288        let (second, why) = stamps.instant(Some(NOW - 1_000_000), NOW + 1_000_000, PERIOD);
1289        assert_eq!(why, Some(StampRefusal::Repeated));
1290        assert!(second > first);
1291        let mut seen = Stamps::default();
1292        assert!(seen.first(StampRefusal::Repeated));
1293        assert!(!seen.first(StampRefusal::Repeated));
1294        assert!(seen.first(StampRefusal::Stale));
1295    }
1296
1297    fn cfg(zero_copy: bool) -> StreamConfig {
1298        StreamConfig {
1299            zero_copy,
1300            dma_formats: vec![
1301                (FOURCC_XR24, vec![0x0300_0000_0000_0001, 0]),
1302                (FOURCC_AR24, vec![0]),
1303            ],
1304            fps_milli: 59_940,
1305            expect: (1920, 1080),
1306            place: CursorPlace {
1307                origin: (0, 0),
1308                size: (1920, 1080),
1309                logical: (1920, 1080),
1310            },
1311        }
1312    }
1313
1314    /// A zero-copy ask lists one modifier-bearing entry per importable fourcc ahead of the
1315    /// plain entries, each holding the rate cap and the expected size; a CPU ask lists only
1316    /// the plain entries.
1317    #[test]
1318    fn enum_formats_lead_with_modifiers_when_zero_copy() {
1319        let pods = enum_format_pods(&cfg(true));
1320        assert_eq!(pods.len(), 2 + SHM_FORMATS.len());
1321        let (_, _, first) = object_props(&pods[0]).unwrap();
1322        let modifier = find_prop(&first, SPA_FORMAT_VIDEO_MODIFIER).unwrap();
1323        assert_eq!(
1324            modifier.flags,
1325            SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE
1326        );
1327        assert_eq!(modifier.as_i64(), Some(0x0300_0000_0000_0001));
1328        assert_eq!(
1329            find_prop(&first, SPA_FORMAT_VIDEO_FORMAT).unwrap().as_u32(),
1330            Some(SPA_VIDEO_FORMAT_BGRX)
1331        );
1332        assert_eq!(
1333            find_prop(&first, SPA_FORMAT_VIDEO_SIZE).unwrap().as_pair(),
1334            Some((1920, 1080))
1335        );
1336        assert_eq!(
1337            find_prop(&first, SPA_FORMAT_VIDEO_MAX_FRAMERATE)
1338                .unwrap()
1339                .as_pair(),
1340            Some((59_940, 1000))
1341        );
1342        let (_, _, plain) = object_props(&pods[2]).unwrap();
1343        assert!(find_prop(&plain, SPA_FORMAT_VIDEO_MODIFIER).is_none());
1344        let cpu = enum_format_pods(&cfg(false));
1345        assert_eq!(cpu.len(), SHM_FORMATS.len());
1346        for pod in &cpu {
1347            let (_, _, props) = object_props(pod).unwrap();
1348            assert!(find_prop(&props, SPA_FORMAT_VIDEO_MODIFIER).is_none());
1349        }
1350    }
1351
1352    /// A fixated format the compositor sends back parses into geometry, fourcc, and the
1353    /// dmabuf decision, and the buffer params answer with the matching data type.
1354    #[test]
1355    fn fixated_format_parses_and_selects_buffer_type() {
1356        let fixated = object(SPA_TYPE_OBJECT_FORMAT, SPA_PARAM_FORMAT, |p| {
1357            prop(p, SPA_FORMAT_MEDIA_TYPE, |v| {
1358                pod_id(v, SPA_MEDIA_TYPE_VIDEO)
1359            });
1360            prop(p, SPA_FORMAT_MEDIA_SUBTYPE, |v| {
1361                pod_id(v, SPA_MEDIA_SUBTYPE_RAW)
1362            });
1363            prop(p, SPA_FORMAT_VIDEO_FORMAT, |v| {
1364                pod_id(v, SPA_VIDEO_FORMAT_BGRA)
1365            });
1366            prop(p, SPA_FORMAT_VIDEO_MODIFIER, |v| {
1367                pod_choice_long(v, SPA_CHOICE_NONE, &[0x0300_0000_0000_0007])
1368            });
1369            prop(p, SPA_FORMAT_VIDEO_SIZE, |v| pod_rect(v, 1280, 720));
1370            prop(p, SPA_FORMAT_VIDEO_FRAMERATE, |v| pod_frac(v, 0, 1));
1371        });
1372        let n = parse_format(&fixated).unwrap();
1373        assert_eq!(
1374            n,
1375            Negotiated {
1376                width: 1280,
1377                height: 720,
1378                fourcc: FOURCC_AR24,
1379                modifier: Some(0x0300_0000_0000_0007)
1380            }
1381        );
1382        let params = negotiated_params(&n, true);
1383        let (_, _, buffers) = object_props(&params[0]).unwrap();
1384        assert_eq!(
1385            find_prop(&buffers, SPA_PARAM_BUFFERS_DATATYPE)
1386                .unwrap()
1387                .as_u32(),
1388            Some(1 << SPA_DATA_DMABUF)
1389        );
1390        assert_eq!(
1391            find_prop(&buffers, SPA_PARAM_BUFFERS_BUFFERS)
1392                .unwrap()
1393                .as_u32(),
1394            Some(4)
1395        );
1396        let memfd = object(SPA_TYPE_OBJECT_FORMAT, SPA_PARAM_FORMAT, |p| {
1397            prop(p, SPA_FORMAT_VIDEO_FORMAT, |v| {
1398                pod_id(v, SPA_VIDEO_FORMAT_RGBX)
1399            });
1400            prop(p, SPA_FORMAT_VIDEO_SIZE, |v| pod_rect(v, 640, 480));
1401        });
1402        let n = parse_format(&memfd).unwrap();
1403        assert_eq!((n.fourcc, n.modifier), (FOURCC_XB24, None));
1404        let params = negotiated_params(&n, false);
1405        let (_, _, buffers) = object_props(&params[0]).unwrap();
1406        assert_eq!(
1407            find_prop(&buffers, SPA_PARAM_BUFFERS_DATATYPE)
1408                .unwrap()
1409                .as_u32(),
1410            Some(1 << SPA_DATA_MEMFD)
1411        );
1412        assert_eq!(shm_code(FOURCC_XR24), 1);
1413        assert_eq!(shm_code(FOURCC_AR24), 0);
1414        assert_eq!(shm_code(FOURCC_XB24), FOURCC_XB24);
1415    }
1416
1417    /// A new preferred size alone keeps a negotiated format, but renegotiates before one stands;
1418    /// a new rate cap or buffer type always renegotiates.
1419    #[test]
1420    fn a_preferred_size_alone_keeps_a_negotiated_format() {
1421        let cur = cfg(false);
1422        let resized = StreamConfig {
1423            expect: (1280, 720),
1424            ..cur.clone()
1425        };
1426        assert!(!renegotiates(&cur, &resized, true));
1427        assert!(renegotiates(&cur, &resized, false));
1428        let capped = StreamConfig {
1429            fps_milli: 30_000,
1430            ..resized
1431        };
1432        assert!(renegotiates(&cur, &capped, true));
1433        assert!(renegotiates(&cur, &cfg(true), true));
1434    }
1435
1436    /// The cursor meta's size range takes the fixed sizes compositors offer it at, Mutter's
1437    /// 384x384 and KWin's 256x256; PipeWire leaves off a meta whose offered size falls outside.
1438    #[test]
1439    fn cursor_meta_range_takes_the_compositors_sizes() {
1440        let n = Negotiated {
1441            width: 1280,
1442            height: 720,
1443            fourcc: FOURCC_XR24,
1444            modifier: None,
1445        };
1446        let range = negotiated_params(&n, false)
1447            .iter()
1448            .find_map(|pod| {
1449                let (_, _, props) = object_props(pod)?;
1450                if find_prop(&props, SPA_PARAM_META_TYPE)?.as_u32()? != SPA_META_CURSOR {
1451                    return None;
1452                }
1453                let size = find_prop(&props, SPA_PARAM_META_SIZE)?;
1454                let at =
1455                    |off: usize| u32::from_ne_bytes(size.payload[off..off + 4].try_into().unwrap());
1456                assert_eq!((size.ty, at(0)), (SPA_TYPE_CHOICE, SPA_CHOICE_RANGE));
1457                // Default, minimum, maximum, after the choice and child headers.
1458                Some((at(20), at(24)))
1459            })
1460            .unwrap();
1461        for edge in [256, 384] {
1462            let size = cursor_meta_size(edge, edge);
1463            assert!(
1464                range.0 <= size && size <= range.1,
1465                "{edge}x{edge} outside {range:?}"
1466            );
1467        }
1468    }
1469}