1use 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;
49const MAX_CURSOR_EDGE: u32 = 1024;
53
54const 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
67fn shm_code(fourcc: u32) -> u32 {
70 match fourcc {
71 FOURCC_AR24 => 0,
72 FOURCC_XR24 => 1,
73 other => other,
74 }
75}
76
77pub 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 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 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#[derive(Clone, Debug, PartialEq)]
154pub struct StreamConfig {
155 pub zero_copy: bool,
157 pub dma_formats: Vec<(u32, Vec<u64>)>,
159 pub fps_milli: u32,
161 pub expect: (i32, i32),
163 pub place: CursorPlace,
166}
167
168#[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
178pub static HOST_POINTER: Mutex<Option<Option<crate::wayland::frontend::PointerAt>>> =
184 Mutex::new(None);
185static HOST_MOTIONS: Mutex<std::collections::VecDeque<(u64, i64)>> =
189 Mutex::new(std::collections::VecDeque::new());
190const HOST_MOTIONS_KEPT: usize = 256;
192
193pub 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
205fn 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
213fn 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
220pub(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
246pub 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
258fn 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#[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#[derive(Default)]
285struct BufferSlot {
286 pw: *mut PwBuffer,
287 dmabuf: Option<Dmabuf>,
288 cpu: Option<Arc<memmap2::Mmap>>,
289 held: bool,
291}
292
293struct 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 generation: AtomicU64,
304 state: AtomicI32,
305 errored: AtomicBool,
306 frames: AtomicU64,
307 stamps: Mutex<Stamps>,
308 cursor_pos: AtomicI64,
309 cursor_hash: AtomicU64,
310 params: Mutex<Vec<Vec<u8>>>,
312}
313
314unsafe 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
351fn 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
373fn 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
421fn 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
432fn 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
440fn 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
554unsafe 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
649unsafe 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
675const STAMP_MAX_AGE_PERIODS: i64 = 4;
678
679#[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#[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 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
745fn 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
754unsafe 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 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
936pub 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 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 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 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 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 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 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 #[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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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(¶ms[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(¶ms[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 #[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 #[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 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}