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 = 256;
50
51const SHM_FORMATS: [(u32, u32); 4] = [
54 (SPA_VIDEO_FORMAT_BGRX, FOURCC_XR24),
55 (SPA_VIDEO_FORMAT_BGRA, FOURCC_AR24),
56 (SPA_VIDEO_FORMAT_RGBX, FOURCC_XB24),
57 (SPA_VIDEO_FORMAT_RGBA, FOURCC_AB24),
58];
59
60fn spa_to_fourcc(spa: u32) -> Option<u32> {
61 SHM_FORMATS.iter().find(|(s, _)| *s == spa).map(|(_, f)| *f)
62}
63
64fn shm_code(fourcc: u32) -> u32 {
67 match fourcc {
68 FOURCC_AR24 => 0,
69 FOURCC_XR24 => 1,
70 other => other,
71 }
72}
73
74pub struct PwCore {
76 api: Api,
77 thread_loop: *mut c_void,
78 context: *mut c_void,
79 core: *mut c_void,
80}
81
82unsafe impl Send for PwCore {}
83unsafe impl Sync for PwCore {}
84
85impl PwCore {
86 pub fn connect(fd: OwnedFd) -> Result<Arc<Self>, String> {
88 let api = *api()?;
89 unsafe {
90 let thread_loop = (api.thread_loop_new)(c"pixelflux-portal-pw".as_ptr(), ptr::null());
91 if thread_loop.is_null() {
92 return Err("pw_thread_loop_new failed".into());
93 }
94 let mut this = PwCore {
95 api,
96 thread_loop,
97 context: ptr::null_mut(),
98 core: ptr::null_mut(),
99 };
100 this.context =
101 (api.context_new)((api.thread_loop_get_loop)(thread_loop), ptr::null_mut(), 0);
102 if this.context.is_null() {
103 return Err("pw_context_new failed".into());
104 }
105 if (api.thread_loop_start)(thread_loop) < 0 {
106 return Err("pw_thread_loop_start failed".into());
107 }
108 (api.thread_loop_lock)(thread_loop);
109 this.core = (api.context_connect_fd)(
110 this.context,
111 std::os::fd::IntoRawFd::into_raw_fd(fd),
112 ptr::null_mut(),
113 0,
114 );
115 (api.thread_loop_unlock)(thread_loop);
116 if this.core.is_null() {
117 return Err("pw_context_connect_fd failed".into());
118 }
119 Ok(Arc::new(this))
120 }
121 }
122
123 fn locked<R>(&self, f: impl FnOnce() -> R) -> R {
125 unsafe { (self.api.thread_loop_lock)(self.thread_loop) };
126 let r = f();
127 unsafe { (self.api.thread_loop_unlock)(self.thread_loop) };
128 r
129 }
130}
131
132impl Drop for PwCore {
133 fn drop(&mut self) {
134 unsafe {
135 (self.api.thread_loop_lock)(self.thread_loop);
136 if !self.core.is_null() {
137 (self.api.core_disconnect)(self.core);
138 }
139 (self.api.thread_loop_unlock)(self.thread_loop);
140 (self.api.thread_loop_stop)(self.thread_loop);
141 if !self.context.is_null() {
142 (self.api.context_destroy)(self.context);
143 }
144 (self.api.thread_loop_destroy)(self.thread_loop);
145 }
146 }
147}
148
149#[derive(Clone, Debug, PartialEq)]
151pub struct StreamConfig {
152 pub zero_copy: bool,
154 pub dma_formats: Vec<(u32, Vec<u64>)>,
156 pub fps_milli: u32,
158 pub expect: (i32, i32),
160}
161
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164pub struct Negotiated {
165 pub width: i32,
166 pub height: i32,
167 pub fourcc: u32,
168 pub modifier: Option<u64>,
169}
170
171#[derive(Default)]
174struct BufferSlot {
175 pw: *mut PwBuffer,
176 dmabuf: Option<Dmabuf>,
177 cpu: Option<Arc<memmap2::Mmap>>,
178 held: bool,
180}
181
182struct Shared {
184 api: Api,
185 stream: AtomicPtr<c_void>,
186 sink: FrameSink,
187 cursor_tx: std::sync::mpsc::Sender<CursorJob>,
188 cfg: Mutex<StreamConfig>,
189 negotiated: Mutex<Option<Negotiated>>,
190 buffers: Mutex<Vec<BufferSlot>>,
191 generation: AtomicU64,
193 state: AtomicI32,
194 errored: AtomicBool,
195 frames: AtomicU64,
196 stamps: Mutex<Stamps>,
197 cursor_pos: AtomicI64,
198 cursor_hash: AtomicU64,
199 params: Mutex<Vec<Vec<u8>>>,
201}
202
203unsafe impl Send for Shared {}
206unsafe impl Sync for Shared {}
207
208fn enum_format_pod(spa_format: u32, modifiers: Option<&[u64]>, cfg: &StreamConfig) -> Vec<u8> {
209 object(SPA_TYPE_OBJECT_FORMAT, SPA_PARAM_ENUM_FORMAT, |p| {
210 prop(p, SPA_FORMAT_MEDIA_TYPE, |v| {
211 pod_id(v, SPA_MEDIA_TYPE_VIDEO)
212 });
213 prop(p, SPA_FORMAT_MEDIA_SUBTYPE, |v| {
214 pod_id(v, SPA_MEDIA_SUBTYPE_RAW)
215 });
216 prop(p, SPA_FORMAT_VIDEO_FORMAT, |v| pod_id(v, spa_format));
217 if let Some(mods) = modifiers {
218 let mut values: Vec<i64> = Vec::with_capacity(mods.len() + 1);
219 values.push(mods[0] as i64);
220 values.extend(mods.iter().map(|m| *m as i64));
221 prop_flags(
222 p,
223 SPA_FORMAT_VIDEO_MODIFIER,
224 SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE,
225 |v| pod_choice_long(v, SPA_CHOICE_ENUM, &values),
226 );
227 }
228 let (w, h) = (cfg.expect.0.max(1) as u32, cfg.expect.1.max(1) as u32);
229 prop(p, SPA_FORMAT_VIDEO_SIZE, |v| {
230 pod_choice_rect(v, SPA_CHOICE_RANGE, &[(w, h), (1, 1), (16384, 16384)])
231 });
232 prop(p, SPA_FORMAT_VIDEO_FRAMERATE, |v| pod_frac(v, 0, 1));
233 let fps = (cfg.fps_milli.max(1000), 1000);
234 prop(p, SPA_FORMAT_VIDEO_MAX_FRAMERATE, |v| {
235 pod_choice_frac(v, SPA_CHOICE_RANGE, &[fps, (1, 1), fps])
236 });
237 })
238}
239
240fn enum_format_pods(cfg: &StreamConfig) -> Vec<Vec<u8>> {
243 let mut pods = Vec::new();
244 if cfg.zero_copy {
245 for (fourcc, mods) in &cfg.dma_formats {
246 let spa = match *fourcc {
247 FOURCC_XR24 => SPA_VIDEO_FORMAT_BGRX,
248 FOURCC_AR24 => SPA_VIDEO_FORMAT_BGRA,
249 _ => continue,
250 };
251 if !mods.is_empty() {
252 pods.push(enum_format_pod(spa, Some(mods), cfg));
253 }
254 }
255 }
256 for (spa, _) in SHM_FORMATS {
257 pods.push(enum_format_pod(spa, None, cfg));
258 }
259 pods
260}
261
262fn negotiated_params(n: &Negotiated, dmabuf: bool) -> Vec<Vec<u8>> {
266 let data_type = 1i32
267 << if dmabuf {
268 SPA_DATA_DMABUF
269 } else {
270 SPA_DATA_MEMFD
271 };
272 let buffers = object(SPA_TYPE_OBJECT_PARAM_BUFFERS, SPA_PARAM_BUFFERS, |p| {
273 prop(p, SPA_PARAM_BUFFERS_BUFFERS, |v| {
274 pod_choice_int(v, SPA_CHOICE_RANGE, &[4, 3, 8])
275 });
276 prop(p, SPA_PARAM_BUFFERS_BLOCKS, |v| pod_int(v, 1));
277 prop(p, SPA_PARAM_BUFFERS_SIZE, |v| {
278 pod_int(v, n.width * n.height * 4)
279 });
280 prop(p, SPA_PARAM_BUFFERS_STRIDE, |v| pod_int(v, n.width * 4));
281 prop(p, SPA_PARAM_BUFFERS_ALIGN, |v| pod_int(v, 16));
282 prop(p, SPA_PARAM_BUFFERS_DATATYPE, |v| pod_int(v, data_type));
283 });
284 let region = SPA_META_REGION_SIZE as i32;
285 vec![
286 buffers,
287 meta(SPA_META_HEADER, |v| pod_int(v, SPA_META_HEADER_SIZE as i32)),
288 meta(SPA_META_VIDEO_DAMAGE, |v| {
289 pod_choice_int(
290 v,
291 SPA_CHOICE_RANGE,
292 &[4 * region, region, MAX_DAMAGE_REGIONS as i32 * region],
293 )
294 }),
295 meta(SPA_META_VIDEO_CROP, |v| pod_int(v, region)),
296 meta(SPA_META_CURSOR, |v| {
297 pod_choice_int(
298 v,
299 SPA_CHOICE_RANGE,
300 &[
301 cursor_meta_size(64, 64) as i32,
302 cursor_meta_size(1, 1) as i32,
303 cursor_meta_size(MAX_CURSOR_EDGE, MAX_CURSOR_EDGE) as i32,
304 ],
305 )
306 }),
307 ]
308}
309
310fn meta(ty: u32, size: impl FnOnce(&mut Vec<u8>)) -> Vec<u8> {
312 object(SPA_TYPE_OBJECT_PARAM_META, SPA_PARAM_META, |p| {
313 prop(p, SPA_PARAM_META_TYPE, |v| pod_id(v, ty));
314 prop(p, SPA_PARAM_META_SIZE, size);
315 })
316}
317
318fn parse_format(pod: &[u8]) -> Option<Negotiated> {
320 let (ty, _, props) = object_props(pod)?;
321 if ty != SPA_TYPE_OBJECT_FORMAT {
322 return None;
323 }
324 let spa = find_prop(&props, SPA_FORMAT_VIDEO_FORMAT)?.as_u32()?;
325 let (w, h) = find_prop(&props, SPA_FORMAT_VIDEO_SIZE)?.as_pair()?;
326 let modifier = find_prop(&props, SPA_FORMAT_VIDEO_MODIFIER)
327 .and_then(|p| p.as_i64())
328 .map(|m| m as u64);
329 Some(Negotiated {
330 width: w as i32,
331 height: h as i32,
332 fourcc: spa_to_fourcc(spa)?,
333 modifier,
334 })
335}
336
337unsafe extern "C" fn on_state_changed(
338 data: *mut c_void,
339 _old: c_int,
340 new: c_int,
341 error: *const c_char,
342) {
343 let shared = unsafe { &*(data as *const Shared) };
344 shared.state.store(new, Ordering::Release);
345 let name = match new {
346 PW_STREAM_STATE_ERROR => "error",
347 PW_STREAM_STATE_UNCONNECTED => "unconnected",
348 PW_STREAM_STATE_PAUSED => "paused",
349 PW_STREAM_STATE_STREAMING => "streaming",
350 _ => "connecting",
351 };
352 let msg = if error.is_null() {
353 String::new()
354 } else {
355 unsafe { CStr::from_ptr(error) }
356 .to_string_lossy()
357 .into_owned()
358 };
359 eprintln!(
360 "[HostCapture] output {}: portal stream {name} {msg}",
361 shared.sink.index
362 );
363 if new == PW_STREAM_STATE_ERROR {
364 shared.errored.store(true, Ordering::Release);
365 }
366}
367
368unsafe extern "C" fn on_param_changed(data: *mut c_void, id: u32, param: *const c_void) {
369 if id != SPA_PARAM_FORMAT {
370 return;
371 }
372 let shared = unsafe { &*(data as *const Shared) };
373 shared.generation.fetch_add(1, Ordering::AcqRel);
374 if param.is_null() {
375 *shared.negotiated.lock().unwrap() = None;
376 return;
377 }
378 let Some(negotiated) = parse_format(unsafe { pod_bytes(param) }) else {
379 eprintln!(
380 "[HostCapture] output {}: unusable PipeWire format",
381 shared.sink.index
382 );
383 return;
384 };
385 eprintln!(
386 "[HostCapture] output {} portal stream: {}x{} {} ({})",
387 shared.sink.index,
388 negotiated.width,
389 negotiated.height,
390 String::from_utf8_lossy(&negotiated.fourcc.to_le_bytes()),
391 match negotiated.modifier {
392 Some(m) => format!("dmabuf, modifier {m:#x}"),
393 None => "memfd".to_string(),
394 }
395 );
396 let pods = negotiated_params(&negotiated, negotiated.modifier.is_some());
397 *shared.negotiated.lock().unwrap() = Some(negotiated);
398 let stream = shared.stream.load(Ordering::Acquire);
399 if stream.is_null() {
400 return;
401 }
402 let mut ptrs: Vec<*const c_void> = pods.iter().map(|p| p.as_ptr() as *const c_void).collect();
403 unsafe { (shared.api.stream_update_params)(stream, ptrs.as_mut_ptr(), ptrs.len() as u32) };
404 *shared.params.lock().unwrap() = pods;
405}
406
407unsafe extern "C" fn on_add_buffer(data: *mut c_void, b: *mut PwBuffer) {
408 let shared = unsafe { &*(data as *const Shared) };
409 let mut buffers = shared.buffers.lock().unwrap();
410 let idx = buffers
411 .iter()
412 .position(|s| s.pw.is_null())
413 .unwrap_or_else(|| {
414 buffers.push(BufferSlot::default());
415 buffers.len() - 1
416 });
417 buffers[idx] = BufferSlot {
418 pw: b,
419 ..BufferSlot::default()
420 };
421 unsafe { (*b).user_data = idx as *mut c_void };
422}
423
424unsafe extern "C" fn on_remove_buffer(data: *mut c_void, b: *mut PwBuffer) {
425 let shared = unsafe { &*(data as *const Shared) };
426 let idx = unsafe { (*b).user_data } as usize;
427 if let Some(slot) = shared.buffers.lock().unwrap().get_mut(idx) {
428 *slot = BufferSlot::default();
429 }
430}
431
432unsafe fn read_cursor(shared: &Shared, meta: &SpaMeta) {
435 if meta.size < std::mem::size_of::<SpaMetaCursor>() as u32 || meta.data.is_null() {
436 return;
437 }
438 let cursor = unsafe { &*(meta.data as *const SpaMetaCursor) };
439 if cursor.id == 0 {
440 return;
441 }
442 shared.cursor_pos.store(
443 ((cursor.x as i64) << 32) | (cursor.y as u32 as i64),
444 Ordering::Relaxed,
445 );
446 if cursor.bitmap_offset < std::mem::size_of::<SpaMetaCursor>() as u32
447 || cursor.bitmap_offset + std::mem::size_of::<SpaMetaBitmap>() as u32 > meta.size
448 {
449 return;
450 }
451 let bitmap = unsafe {
452 &*((meta.data as *const u8).add(cursor.bitmap_offset as usize) as *const SpaMetaBitmap)
453 };
454 if bitmap.format == 0 {
455 return;
456 }
457 let (w, h) = (bitmap.width, bitmap.height);
458 if w == 0 || h == 0 || bitmap.offset == 0 {
459 if shared.cursor_hash.swap(0, Ordering::Relaxed) != 0 {
460 let _ = shared.cursor_tx.send(CursorJob::Hide);
461 }
462 return;
463 }
464 let stride = bitmap.stride.max(0) as usize;
465 let end = cursor.bitmap_offset as usize
466 + bitmap.offset as usize
467 + stride * (h as usize - 1)
468 + w as usize * 4;
469 if end > meta.size as usize || w > MAX_CURSOR_EDGE || h > MAX_CURSOR_EDGE {
470 return;
471 }
472 let pixels = unsafe {
473 (meta.data as *const u8).add(cursor.bitmap_offset as usize + bitmap.offset as usize)
474 };
475 let mut rgba = Vec::with_capacity((w * h * 4) as usize);
476 for y in 0..h as usize {
477 let row = unsafe { std::slice::from_raw_parts(pixels.add(y * stride), w as usize * 4) };
478 for px in row.as_chunks::<4>().0 {
479 let (r, g, b, a) = match bitmap.format {
480 SPA_VIDEO_FORMAT_BGRA | SPA_VIDEO_FORMAT_BGRX => (
481 px[2],
482 px[1],
483 px[0],
484 if bitmap.format == SPA_VIDEO_FORMAT_BGRX {
485 0xff
486 } else {
487 px[3]
488 },
489 ),
490 SPA_VIDEO_FORMAT_ARGB => (px[1], px[2], px[3], px[0]),
491 SPA_VIDEO_FORMAT_ABGR => (px[3], px[2], px[1], px[0]),
492 _ => (
493 px[0],
494 px[1],
495 px[2],
496 if bitmap.format == SPA_VIDEO_FORMAT_RGBX {
497 0xff
498 } else {
499 px[3]
500 },
501 ),
502 };
503 rgba.extend_from_slice(&[r, g, b, a]);
504 }
505 }
506 let hash = xxhash_rust::xxh3::xxh3_64(&rgba) ^ ((w as u64) << 32 | h as u64);
507 let previous = shared.cursor_hash.swap(hash, Ordering::Relaxed);
508 if previous != hash {
509 if previous == 0 {
510 eprintln!(
511 "[HostCapture] output {}: portal cursor sprites arrive ({w}x{h}, hotspot {},{})",
512 shared.sink.index, cursor.hot_x, cursor.hot_y
513 );
514 }
515 let _ = shared.cursor_tx.send(CursorJob::Gles {
516 hash,
517 width: w as i32,
518 height: h as i32,
519 bytes: rgba,
520 hot_x: cursor.hot_x,
521 hot_y: cursor.hot_y,
522 });
523 }
524}
525
526unsafe fn read_damage(buf: &SpaBuffer, width: i32, height: i32) -> Vec<Rectangle<i32, Physical>> {
528 let mut damage = Vec::new();
529 for i in 0..buf.n_metas as usize {
530 let m = unsafe { &*buf.metas.add(i) };
531 if m.type_ != SPA_META_VIDEO_DAMAGE || m.data.is_null() {
532 continue;
533 }
534 let n = m.size as usize / std::mem::size_of::<SpaMetaRegion>();
535 for j in 0..n {
536 let r = unsafe { *(m.data as *const SpaMetaRegion).add(j) };
537 if r.width == 0 || r.height == 0 {
538 break;
539 }
540 damage.push(Rectangle::new(
541 (r.x, r.y).into(),
542 (r.width as i32, r.height as i32).into(),
543 ));
544 }
545 }
546 if damage.is_empty() {
547 damage.push(Rectangle::new((0, 0).into(), (width, height).into()));
548 }
549 damage
550}
551
552const STAMP_MAX_AGE_PERIODS: i64 = 4;
555
556#[derive(Clone, Copy, Debug, PartialEq, Eq)]
558enum StampRefusal {
559 Missing,
560 Ahead,
561 Stale,
562 Repeated,
563}
564
565impl StampRefusal {
566 fn says(self) -> &'static str {
567 match self {
568 StampRefusal::Missing => "missing",
569 StampRefusal::Ahead => "ahead of the frame's arrival",
570 StampRefusal::Stale => "older than four frame periods at arrival",
571 StampRefusal::Repeated => "no later than the last frame's",
572 }
573 }
574}
575
576#[derive(Default)]
584struct Stamps {
585 last: i64,
586 refusals_seen: u8,
587}
588
589impl Stamps {
590 fn instant(
591 &mut self,
592 pts: Option<i64>,
593 arrival: i64,
594 period_ns: i64,
595 ) -> (i64, Option<StampRefusal>) {
596 let refusal = match pts {
597 None => Some(StampRefusal::Missing),
598 Some(pts) if pts > arrival => Some(StampRefusal::Ahead),
599 Some(pts) if arrival - pts > STAMP_MAX_AGE_PERIODS * period_ns => {
600 Some(StampRefusal::Stale)
601 }
602 Some(pts) if pts <= self.last => Some(StampRefusal::Repeated),
603 Some(_) => None,
604 };
605 let at = match (pts, refusal) {
606 (Some(pts), None) => pts,
607 _ => arrival.max(self.last + 1),
608 };
609 self.last = at;
610 (at, refusal)
611 }
612
613 fn first(&mut self, refusal: StampRefusal) -> bool {
615 let bit = 1 << refusal as u8;
616 let first = self.refusals_seen & bit == 0;
617 self.refusals_seen |= bit;
618 first
619 }
620}
621
622fn header_pts(buf: &SpaBuffer) -> Option<i64> {
624 (0..buf.n_metas as usize)
625 .map(|i| unsafe { &*buf.metas.add(i) })
626 .find(|m| m.type_ == SPA_META_HEADER && m.size >= SPA_META_HEADER_SIZE && !m.data.is_null())
627 .map(|m| unsafe { (*(m.data as *const SpaMetaHeader)).pts })
628 .filter(|pts| *pts > 0)
629}
630
631unsafe fn import_slot(slot: &mut BufferSlot, buf: &SpaBuffer, n: &Negotiated) -> bool {
634 if slot.dmabuf.is_some() || slot.cpu.is_some() {
635 return true;
636 }
637 let d0 = unsafe { &*buf.datas };
638 if d0.type_ == SPA_DATA_DMABUF {
639 let mut builder = Dmabuf::builder(
640 (n.width, n.height),
641 Fourcc::try_from(n.fourcc).unwrap_or(Fourcc::Xrgb8888),
642 Modifier::from(n.modifier.unwrap_or(u64::from(Modifier::Invalid))),
643 DmabufFlags::empty(),
644 );
645 for i in 0..buf.n_datas as usize {
646 let d = unsafe { &*buf.datas.add(i) };
647 let dup = unsafe { libc::dup(d.fd as c_int) };
648 if dup < 0 {
649 return false;
650 }
651 let chunk = unsafe { &*d.chunk };
652 builder.add_plane(
653 unsafe { OwnedFd::from_raw_fd(dup) },
654 chunk.offset,
655 chunk.stride as u32,
656 );
657 }
658 slot.dmabuf = builder.build();
659 slot.dmabuf.is_some()
660 } else if d0.type_ == SPA_DATA_MEMFD {
661 let file = unsafe { std::fs::File::from_raw_fd(libc::dup(d0.fd as c_int)) };
662 let map = unsafe {
663 memmap2::MmapOptions::new()
664 .offset(d0.mapoffset as u64)
665 .len(d0.maxsize as usize)
666 .map(&file)
667 };
668 match map {
669 Ok(m) => {
670 slot.cpu = Some(Arc::new(m));
671 true
672 }
673 Err(e) => {
674 eprintln!("[HostCapture] memfd map: {e}");
675 false
676 }
677 }
678 } else {
679 false
680 }
681}
682
683unsafe extern "C" fn on_process(data: *mut c_void) {
684 let shared = unsafe { &*(data as *const Shared) };
685 let stream = shared.stream.load(Ordering::Acquire);
686 if stream.is_null() {
687 return;
688 }
689 let Some(n) = *shared.negotiated.lock().unwrap() else {
690 return;
691 };
692 let mut newest: Option<ptr::NonNull<PwBuffer>> = None;
695 loop {
696 let b = unsafe { (shared.api.stream_dequeue_buffer)(stream) };
697 if b.is_null() {
698 break;
699 }
700 let buf = unsafe { &*(*b).buffer };
701 for i in 0..buf.n_metas as usize {
702 let m = unsafe { &*buf.metas.add(i) };
703 if m.type_ == SPA_META_CURSOR {
704 unsafe { read_cursor(shared, m) };
705 }
706 }
707 let has_video = buf.n_datas > 0 && {
708 let d = unsafe { &*buf.datas };
709 let chunk = unsafe { &*d.chunk };
710 chunk.size > 0 && chunk.flags & SPA_CHUNK_FLAG_CORRUPTED == 0
711 };
712 if has_video {
713 if let Some(previous) = newest {
714 unsafe { (shared.api.stream_queue_buffer)(stream, previous.as_ptr()) };
715 }
716 newest = ptr::NonNull::new(b);
717 } else {
718 unsafe { (shared.api.stream_queue_buffer)(stream, b) };
719 }
720 }
721 let Some(newest) = newest.map(|p| p.as_ptr()) else {
722 return;
723 };
724 let buf = unsafe { &*(*newest).buffer };
725 let idx = unsafe { (*newest).user_data } as usize;
726 let generation = shared.generation.load(Ordering::Acquire);
727 let pts = header_pts(buf);
728 let arrival = now_ns();
729 let period_ns = 1_000_000_000_000 / shared.cfg.lock().unwrap().fps_milli.max(1000) as i64;
730 let (stamp_ns, refusal) = shared
731 .stamps
732 .lock()
733 .unwrap()
734 .instant(pts, arrival, period_ns);
735 let frame = {
736 let mut buffers = shared.buffers.lock().unwrap();
737 let Some(slot) = buffers.get_mut(idx).filter(|s| s.pw == newest) else {
738 unsafe { (shared.api.stream_queue_buffer)(stream, newest) };
739 return;
740 };
741 if !unsafe { import_slot(slot, buf, &n) } {
742 unsafe { (shared.api.stream_queue_buffer)(stream, newest) };
743 return;
744 }
745 let chunk = unsafe { &*(*buf.datas).chunk };
746 let cpu = slot.cpu.as_ref().map(|map| HostCpuFrame {
747 map: map.clone(),
748 offset: chunk.offset as usize,
749 stride: if chunk.stride > 0 {
750 chunk.stride as usize
751 } else {
752 n.width as usize * 4
753 },
754 format: shm_code(n.fourcc),
755 });
756 slot.held = true;
757 HostFrame {
758 generation,
759 slot: idx,
760 dmabuf: slot.dmabuf.clone(),
761 cpu,
762 width: n.width,
763 height: n.height,
764 damage: unsafe { read_damage(buf, n.width, n.height) },
765 stamp_ns,
766 }
767 };
768 let count = shared.frames.fetch_add(1, Ordering::Relaxed);
769 let age_ms = (arrival - stamp_ns) as f64 / 1e6;
770 if count == 0 {
771 eprintln!(
772 "[HostCapture] output {}: first portal frame {}x{}, {} damage rect(s), {}",
773 shared.sink.index,
774 n.width,
775 n.height,
776 frame.damage.len(),
777 match refusal {
778 None => format!("{age_ms:.1} ms after the compositor stamped it"),
779 Some(why) => format!(
780 "stamped at arrival: the compositor's stamp is {}",
781 why.says()
782 ),
783 }
784 );
785 }
786 if let Some(why) = refusal.filter(|why| count > 0 && shared.stamps.lock().unwrap().first(*why))
787 {
788 eprintln!(
789 "[HostCapture] output {}: frame {count} stamped at arrival: the compositor's stamp is {}",
790 shared.sink.index,
791 why.says()
792 );
793 }
794 if trace() {
795 eprintln!(
796 "[HostTrace] output {} frame {count} process +{age_ms:.2}ms pts {} arrival {arrival}{}",
797 shared.sink.index,
798 pts.map_or("none".to_string(), |pts| pts.to_string()),
799 refusal.map_or(String::new(), |why| format!(" refused: {}", why.says()))
800 );
801 }
802 if !shared.sink.send(frame) {
803 if let Some(slot) = shared.buffers.lock().unwrap().get_mut(idx) {
804 slot.held = false;
805 }
806 unsafe { (shared.api.stream_queue_buffer)(stream, newest) };
807 }
808}
809
810pub struct PwStream {
812 core: Arc<PwCore>,
813 stream: *mut c_void,
814 shared: Arc<Shared>,
815 _events: Box<PwStreamEvents>,
816 _hook: Box<[u64; 8]>,
817}
818
819unsafe impl Send for PwStream {}
820
821impl PwStream {
822 pub(crate) fn connect(
825 core: &Arc<PwCore>,
826 node_id: u32,
827 sink: FrameSink,
828 cursor_tx: std::sync::mpsc::Sender<CursorJob>,
829 cfg: StreamConfig,
830 ) -> Result<Self, String> {
831 let api = core.api;
832 let pods = enum_format_pods(&cfg);
833 let shared = Arc::new(Shared {
834 api,
835 stream: AtomicPtr::new(ptr::null_mut()),
836 sink,
837 cursor_tx,
838 cfg: Mutex::new(cfg),
839 negotiated: Mutex::new(None),
840 buffers: Mutex::new(Vec::new()),
841 generation: AtomicU64::new(0),
842 state: AtomicI32::new(0),
843 errored: AtomicBool::new(false),
844 frames: AtomicU64::new(0),
845 stamps: Mutex::new(Stamps::default()),
846 cursor_pos: AtomicI64::new(i64::MIN),
847 cursor_hash: AtomicU64::new(0),
848 params: Mutex::new(Vec::new()),
849 });
850 let events = Box::new(PwStreamEvents {
851 state_changed: Some(on_state_changed),
852 param_changed: Some(on_param_changed),
853 add_buffer: Some(on_add_buffer),
854 remove_buffer: Some(on_remove_buffer),
855 process: Some(on_process),
856 ..PwStreamEvents::empty()
857 });
858 let mut this = PwStream {
859 core: core.clone(),
860 stream: ptr::null_mut(),
861 shared,
862 _events: events,
863 _hook: Box::new([0u64; 8]),
864 };
865 let result = core.locked(|| unsafe {
866 let props = (api.properties_new)(ptr::null::<c_char>());
867 if props.is_null() {
868 return Err("pw_properties_new failed".to_string());
869 }
870 for (k, v) in [
871 (c"media.type", c"Video"),
872 (c"media.category", c"Capture"),
873 (c"media.role", c"Screen"),
874 ] {
875 (api.properties_set)(props, k.as_ptr(), v.as_ptr());
876 }
877 this.stream = (api.stream_new)(core.core, c"pixelflux host capture".as_ptr(), props);
878 if this.stream.is_null() {
879 return Err("pw_stream_new failed".to_string());
880 }
881 this.shared.stream.store(this.stream, Ordering::Release);
882 (api.stream_add_listener)(
883 this.stream,
884 this._hook.as_mut_ptr() as *mut c_void,
885 &*this._events,
886 Arc::as_ptr(&this.shared) as *mut c_void,
887 );
888 let mut ptrs: Vec<*const c_void> =
889 pods.iter().map(|p| p.as_ptr() as *const c_void).collect();
890 let rc = (api.stream_connect)(
891 this.stream,
892 PW_DIRECTION_INPUT,
893 node_id,
894 PW_STREAM_FLAG_AUTOCONNECT,
895 ptrs.as_mut_ptr(),
896 ptrs.len() as u32,
897 );
898 if rc < 0 {
899 return Err(format!("pw_stream_connect failed ({rc})"));
900 }
901 Ok(())
902 });
903 *this.shared.params.lock().unwrap() = pods;
904 result?;
905 Ok(this)
906 }
907
908 pub fn reconfigure(&self, cfg: StreamConfig) {
910 let pods = {
911 let mut cur = self.shared.cfg.lock().unwrap();
912 if *cur == cfg {
913 return;
914 }
915 *cur = cfg;
916 enum_format_pods(&cur)
917 };
918 self.core.locked(|| unsafe {
919 let mut ptrs: Vec<*const c_void> =
920 pods.iter().map(|p| p.as_ptr() as *const c_void).collect();
921 (self.shared.api.stream_update_params)(
922 self.stream,
923 ptrs.as_mut_ptr(),
924 ptrs.len() as u32,
925 );
926 });
927 *self.shared.params.lock().unwrap() = pods;
928 }
929
930 pub fn release(&self, generation: u64, slot: usize) {
932 if generation != self.shared.generation.load(Ordering::Acquire) {
933 return;
934 }
935 self.core.locked(|| {
936 let mut buffers = self.shared.buffers.lock().unwrap();
937 if let Some(s) = buffers.get_mut(slot).filter(|s| s.held && !s.pw.is_null()) {
938 s.held = false;
939 unsafe { (self.shared.api.stream_queue_buffer)(self.stream, s.pw) };
940 }
941 });
942 }
943
944 pub fn release_all(&self) {
946 self.core.locked(|| {
947 let mut buffers = self.shared.buffers.lock().unwrap();
948 for s in buffers.iter_mut().filter(|s| s.held && !s.pw.is_null()) {
949 s.held = false;
950 unsafe { (self.shared.api.stream_queue_buffer)(self.stream, s.pw) };
951 }
952 });
953 }
954
955 pub fn set_active(&self, active: bool) {
956 self.core
957 .locked(|| unsafe { (self.shared.api.stream_set_active)(self.stream, active) });
958 }
959
960 pub fn negotiated(&self) -> Option<Negotiated> {
961 *self.shared.negotiated.lock().unwrap()
962 }
963
964 pub fn errored(&self) -> bool {
965 self.shared.errored.load(Ordering::Acquire)
966 || self.shared.state.load(Ordering::Acquire) == PW_STREAM_STATE_ERROR
967 }
968
969 pub fn frames(&self) -> u64 {
970 self.shared.frames.load(Ordering::Relaxed)
971 }
972
973 pub fn cursor_position(&self) -> Option<(i32, i32)> {
975 let v = self.shared.cursor_pos.load(Ordering::Relaxed);
976 (v != i64::MIN).then_some(((v >> 32) as i32, v as i32))
977 }
978
979 pub fn wait_negotiated(&self, timeout: Duration) -> bool {
981 let deadline = Instant::now() + timeout;
982 while Instant::now() < deadline {
983 if self.negotiated().is_some() || self.errored() {
984 return self.negotiated().is_some();
985 }
986 std::thread::sleep(Duration::from_millis(5));
987 }
988 false
989 }
990}
991
992impl Drop for PwStream {
993 fn drop(&mut self) {
994 self.core.locked(|| unsafe {
995 self.shared.stream.store(ptr::null_mut(), Ordering::Release);
996 if !self.stream.is_null() {
997 (self.shared.api.stream_disconnect)(self.stream);
998 (self.shared.api.stream_destroy)(self.stream);
999 }
1000 });
1001 }
1002}
1003
1004#[cfg(test)]
1005mod tests {
1006 use super::*;
1007
1008 const PERIOD: i64 = 16_683_333;
1009 const NOW: i64 = 5_000_000_000_000;
1010
1011 #[test]
1014 fn a_compositor_stamp_is_taken_only_where_it_is_plausible() {
1015 let mut stamps = Stamps::default();
1016 assert_eq!(
1017 stamps.instant(Some(NOW - 3_000_000), NOW, PERIOD),
1018 (NOW - 3_000_000, None)
1019 );
1020 let next = NOW + PERIOD;
1021 assert_eq!(
1022 stamps.instant(Some(NOW - 3_000_000), next, PERIOD),
1023 (next, Some(StampRefusal::Repeated)),
1024 "a stamp reused from the previous frame"
1025 );
1026 let next = next + PERIOD;
1027 assert_eq!(
1028 stamps.instant(Some(next + 1), next, PERIOD),
1029 (next, Some(StampRefusal::Ahead)),
1030 "a stamp ahead of the arrival"
1031 );
1032 let next = next + PERIOD;
1033 assert_eq!(
1034 stamps.instant(Some(next - 5 * PERIOD), next, PERIOD),
1035 (next, Some(StampRefusal::Stale)),
1036 "a stamp five frame periods old"
1037 );
1038 let next = next + PERIOD;
1039 assert_eq!(
1040 stamps.instant(Some(1_759_000_000_000_000_000), next, PERIOD),
1041 (next, Some(StampRefusal::Ahead)),
1042 "a CLOCK_REALTIME stamp"
1043 );
1044 let next = next + PERIOD;
1045 assert_eq!(
1046 stamps.instant(Some(40_000_000), next, PERIOD),
1047 (next, Some(StampRefusal::Stale)),
1048 "a stamp counted from the stream's start"
1049 );
1050 let next = next + PERIOD;
1051 assert_eq!(
1052 stamps.instant(None, next, PERIOD),
1053 (next, Some(StampRefusal::Missing)),
1054 "no stamp (the header meta absent, or zero)"
1055 );
1056 let next = next + PERIOD;
1057 assert_eq!(
1058 stamps.instant(Some(next - 2_000_000), next, PERIOD),
1059 (next - 2_000_000, None)
1060 );
1061 }
1062
1063 #[test]
1066 fn the_instants_never_go_back() {
1067 let mut stamps = Stamps::default();
1068 let (first, why) = stamps.instant(None, NOW, PERIOD);
1069 assert_eq!((first, why), (NOW, Some(StampRefusal::Missing)));
1070 let (second, why) = stamps.instant(Some(NOW - 1_000_000), NOW + 1_000_000, PERIOD);
1071 assert_eq!(why, Some(StampRefusal::Repeated));
1072 assert!(second > first);
1073 let mut seen = Stamps::default();
1074 assert!(seen.first(StampRefusal::Repeated));
1075 assert!(!seen.first(StampRefusal::Repeated));
1076 assert!(seen.first(StampRefusal::Stale));
1077 }
1078
1079 fn cfg(zero_copy: bool) -> StreamConfig {
1080 StreamConfig {
1081 zero_copy,
1082 dma_formats: vec![
1083 (FOURCC_XR24, vec![0x0300_0000_0000_0001, 0]),
1084 (FOURCC_AR24, vec![0]),
1085 ],
1086 fps_milli: 59_940,
1087 expect: (1920, 1080),
1088 }
1089 }
1090
1091 #[test]
1095 fn enum_formats_lead_with_modifiers_when_zero_copy() {
1096 let pods = enum_format_pods(&cfg(true));
1097 assert_eq!(pods.len(), 2 + SHM_FORMATS.len());
1098 let (_, _, first) = object_props(&pods[0]).unwrap();
1099 let modifier = find_prop(&first, SPA_FORMAT_VIDEO_MODIFIER).unwrap();
1100 assert_eq!(
1101 modifier.flags,
1102 SPA_POD_PROP_FLAG_MANDATORY | SPA_POD_PROP_FLAG_DONT_FIXATE
1103 );
1104 assert_eq!(modifier.as_i64(), Some(0x0300_0000_0000_0001));
1105 assert_eq!(
1106 find_prop(&first, SPA_FORMAT_VIDEO_FORMAT).unwrap().as_u32(),
1107 Some(SPA_VIDEO_FORMAT_BGRX)
1108 );
1109 assert_eq!(
1110 find_prop(&first, SPA_FORMAT_VIDEO_SIZE).unwrap().as_pair(),
1111 Some((1920, 1080))
1112 );
1113 assert_eq!(
1114 find_prop(&first, SPA_FORMAT_VIDEO_MAX_FRAMERATE)
1115 .unwrap()
1116 .as_pair(),
1117 Some((59_940, 1000))
1118 );
1119 let (_, _, plain) = object_props(&pods[2]).unwrap();
1120 assert!(find_prop(&plain, SPA_FORMAT_VIDEO_MODIFIER).is_none());
1121 let cpu = enum_format_pods(&cfg(false));
1122 assert_eq!(cpu.len(), SHM_FORMATS.len());
1123 for pod in &cpu {
1124 let (_, _, props) = object_props(pod).unwrap();
1125 assert!(find_prop(&props, SPA_FORMAT_VIDEO_MODIFIER).is_none());
1126 }
1127 }
1128
1129 #[test]
1132 fn fixated_format_parses_and_selects_buffer_type() {
1133 let fixated = object(SPA_TYPE_OBJECT_FORMAT, SPA_PARAM_FORMAT, |p| {
1134 prop(p, SPA_FORMAT_MEDIA_TYPE, |v| {
1135 pod_id(v, SPA_MEDIA_TYPE_VIDEO)
1136 });
1137 prop(p, SPA_FORMAT_MEDIA_SUBTYPE, |v| {
1138 pod_id(v, SPA_MEDIA_SUBTYPE_RAW)
1139 });
1140 prop(p, SPA_FORMAT_VIDEO_FORMAT, |v| {
1141 pod_id(v, SPA_VIDEO_FORMAT_BGRA)
1142 });
1143 prop(p, SPA_FORMAT_VIDEO_MODIFIER, |v| {
1144 pod_choice_long(v, SPA_CHOICE_NONE, &[0x0300_0000_0000_0007])
1145 });
1146 prop(p, SPA_FORMAT_VIDEO_SIZE, |v| pod_rect(v, 1280, 720));
1147 prop(p, SPA_FORMAT_VIDEO_FRAMERATE, |v| pod_frac(v, 0, 1));
1148 });
1149 let n = parse_format(&fixated).unwrap();
1150 assert_eq!(
1151 n,
1152 Negotiated {
1153 width: 1280,
1154 height: 720,
1155 fourcc: FOURCC_AR24,
1156 modifier: Some(0x0300_0000_0000_0007)
1157 }
1158 );
1159 let params = negotiated_params(&n, true);
1160 let (_, _, buffers) = object_props(¶ms[0]).unwrap();
1161 assert_eq!(
1162 find_prop(&buffers, SPA_PARAM_BUFFERS_DATATYPE)
1163 .unwrap()
1164 .as_u32(),
1165 Some(1 << SPA_DATA_DMABUF)
1166 );
1167 assert_eq!(
1168 find_prop(&buffers, SPA_PARAM_BUFFERS_BUFFERS)
1169 .unwrap()
1170 .as_u32(),
1171 Some(4)
1172 );
1173 let memfd = object(SPA_TYPE_OBJECT_FORMAT, SPA_PARAM_FORMAT, |p| {
1174 prop(p, SPA_FORMAT_VIDEO_FORMAT, |v| {
1175 pod_id(v, SPA_VIDEO_FORMAT_RGBX)
1176 });
1177 prop(p, SPA_FORMAT_VIDEO_SIZE, |v| pod_rect(v, 640, 480));
1178 });
1179 let n = parse_format(&memfd).unwrap();
1180 assert_eq!((n.fourcc, n.modifier), (FOURCC_XB24, None));
1181 let params = negotiated_params(&n, false);
1182 let (_, _, buffers) = object_props(¶ms[0]).unwrap();
1183 assert_eq!(
1184 find_prop(&buffers, SPA_PARAM_BUFFERS_DATATYPE)
1185 .unwrap()
1186 .as_u32(),
1187 Some(1 << SPA_DATA_MEMFD)
1188 );
1189 assert_eq!(shm_code(FOURCC_XR24), 1);
1190 assert_eq!(shm_code(FOURCC_AR24), 0);
1191 assert_eq!(shm_code(FOURCC_XB24), FOURCC_XB24);
1192 }
1193}