// SPDX-License-Identifier: AGPL-3.0-or-later pub mod frame_pool; #[cfg(feature = "wgpu")] pub mod gpu_loss; use parking_lot::RwLock; use std::collections::HashMap; use std::ffi::c_void; use std::sync::Arc; use std::sync::OnceLock; use crate::frame_pool::PooledFrame; #[cfg(target_os = "linux")] use std::os::fd::{IntoRawFd, OwnedFd}; #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct DmabufDesc { pub plane_count: u8, pub width: u32, pub height: u32, pub drm_format: u32, pub modifier: u64, pub strides: [u32; 4], pub offsets: [u32; 4], pub device_uuid: [u8; 16], pub timestamp_us: i64, } #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct SharedTextureDesc { pub handle: u64, pub width: u32, pub height: u32, pub dxgi_format: u32, pub timestamp_us: i64, } pub const FRAME_DATA_BYTES_CAP: usize = 1 << 30; pub trait SharedFrameBytes: Send + Sync { fn bytes(&self) -> &[u8]; } enum FrameDataRepr { Owned(Vec), Shared { source: Arc, len: usize, _capacity_token: Option, }, } pub struct FrameData { repr: FrameDataRepr, } impl FrameData { pub fn from_owned(data: Vec) -> Self { assert!(data.len() <= FRAME_DATA_BYTES_CAP); Self { repr: FrameDataRepr::Owned(data), } } pub fn from_shared( source: Arc, len: usize, capacity_token: Option, ) -> Self { assert!(len <= FRAME_DATA_BYTES_CAP); assert!(len <= source.bytes().len()); Self { repr: FrameDataRepr::Shared { source, len, _capacity_token: capacity_token, }, } } pub fn as_slice(&self) -> &[u8] { match &self.repr { FrameDataRepr::Owned(data) => data.as_slice(), FrameDataRepr::Shared { source, len, .. } => { let bytes = source.bytes(); assert!(*len <= bytes.len()); &bytes[..*len] } } } pub fn len(&self) -> usize { let len = match &self.repr { FrameDataRepr::Owned(data) => data.len(), FrameDataRepr::Shared { len, .. } => *len, }; assert!(len <= FRAME_DATA_BYTES_CAP); len } pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn is_shared(&self) -> bool { matches!(self.repr, FrameDataRepr::Shared { .. }) } pub fn into_vec(self) -> Vec { match self.repr { FrameDataRepr::Owned(data) => data, FrameDataRepr::Shared { source, len, .. } => { let bytes = source.bytes(); assert!(len <= bytes.len()); bytes[..len].to_vec() } } } } impl From> for FrameData { fn from(data: Vec) -> Self { Self::from_owned(data) } } impl std::ops::Deref for FrameData { type Target = [u8]; fn deref(&self) -> &[u8] { self.as_slice() } } pub struct Nv12Frame { pub data: FrameData, pub width: u32, pub height: u32, pub stride_y: u32, pub stride_uv: u32, pub timestamp_us: i64, } pub struct BgraFrame { pub data: Vec, pub width: u32, pub height: u32, pub stride: u32, pub timestamp_us: i64, } #[cfg(target_os = "macos")] pub struct MacCvPixelBufferFrame { pixel_buffer: *mut c_void, pub width: u32, pub height: u32, pub pixel_format: u32, pub timestamp_us: i64, } #[cfg(target_os = "macos")] unsafe impl Send for MacCvPixelBufferFrame {} #[cfg(target_os = "macos")] unsafe impl Sync for MacCvPixelBufferFrame {} #[cfg(target_os = "macos")] impl MacCvPixelBufferFrame { pub unsafe fn from_retained( pixel_buffer: *mut c_void, width: u32, height: u32, pixel_format: u32, timestamp_us: i64, ) -> Self { Self { pixel_buffer, width, height, pixel_format, timestamp_us, } } pub fn pixel_buffer_ptr(&self) -> *mut c_void { self.pixel_buffer } pub fn into_raw_pixel_buffer(mut self) -> *mut c_void { let pb = self.pixel_buffer; self.pixel_buffer = std::ptr::null_mut(); std::mem::forget(self); pb } } #[cfg(target_os = "macos")] impl Drop for MacCvPixelBufferFrame { fn drop(&mut self) { if !self.pixel_buffer.is_null() { unsafe { CFRelease(self.pixel_buffer as *const c_void) }; self.pixel_buffer = std::ptr::null_mut(); } } } #[cfg(target_os = "macos")] #[link(name = "CoreFoundation", kind = "framework")] unsafe extern "C" { fn CFRelease(cf: *const c_void); } #[cfg(target_os = "linux")] pub struct DmabufFrame { pub desc: DmabufDesc, pub fds: Vec, } pub enum ScreenFrame { Nv12(Nv12Frame), Bgra(BgraFrame), #[cfg(target_os = "macos")] MacCvPixelBuffer(MacCvPixelBufferFrame), #[cfg(target_os = "linux")] Dmabuf(DmabufFrame), #[cfg(target_os = "windows")] SharedTexture(SharedTextureDesc), } impl ScreenFrame { pub fn timestamp_us(&self) -> i64 { match self { Self::Nv12(f) => f.timestamp_us, Self::Bgra(f) => f.timestamp_us, #[cfg(target_os = "macos")] Self::MacCvPixelBuffer(f) => f.timestamp_us, #[cfg(target_os = "linux")] Self::Dmabuf(f) => f.desc.timestamp_us, #[cfg(target_os = "windows")] Self::SharedTexture(d) => d.timestamp_us, } } pub fn dimensions(&self) -> (u32, u32) { match self { Self::Nv12(f) => (f.width, f.height), Self::Bgra(f) => (f.width, f.height), #[cfg(target_os = "macos")] Self::MacCvPixelBuffer(f) => (f.width, f.height), #[cfg(target_os = "linux")] Self::Dmabuf(f) => (f.desc.width, f.desc.height), #[cfg(target_os = "windows")] Self::SharedTexture(d) => (d.width, d.height), } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum EnqueueOutcome { Accepted, Coalesced, Rejected, } pub trait ScreenFrameSink: Send + Sync { fn enqueue(&self, frame: ScreenFrame) -> EnqueueOutcome; } pub const NATIVE_SCREEN_FRAME_SINK_HANDLE_MAGIC: u32 = u32::from_be_bytes(*b"FXSH"); pub const NATIVE_SCREEN_FRAME_SINK_HANDLE_VERSION: u32 = 2; pub const NATIVE_SCREEN_FRAME_SINK_ACCEPTED: u32 = 1; pub const NATIVE_SCREEN_FRAME_SINK_COALESCED: u32 = 2; pub const NATIVE_SCREEN_FRAME_SINK_REJECTED: u32 = 3; pub type NativeScreenFrameSinkRetainFn = unsafe extern "C" fn(context: *const c_void); pub type NativeScreenFrameSinkReleaseFn = unsafe extern "C" fn(context: *const c_void); pub type NativeScreenFrameSinkEnqueueNv12Fn = unsafe extern "C" fn( context: *const c_void, data: *const u8, data_len: usize, width: u32, height: u32, stride_y: u32, stride_uv: u32, timestamp_us: i64, ) -> u32; pub type NativeScreenFrameSinkEnqueueBgraFn = unsafe extern "C" fn( context: *const c_void, data: *const u8, data_len: usize, width: u32, height: u32, stride: u32, timestamp_us: i64, ) -> u32; pub type NativeScreenFrameSinkEnqueueMacCvPixelBufferFn = unsafe extern "C" fn( context: *const c_void, pixel_buffer: *mut c_void, width: u32, height: u32, pixel_format: u32, timestamp_us: i64, ) -> u32; pub type NativeScreenFrameSinkEnqueueDmabufFn = unsafe extern "C" fn( context: *const c_void, desc: DmabufDesc, fds: *const i32, fd_count: usize, ) -> u32; pub type NativeScreenFrameSinkEnqueueSharedTextureFn = unsafe extern "C" fn(context: *const c_void, desc: SharedTextureDesc) -> u32; pub type NativeScreenFrameSinkEnqueueScreenAudioFn = unsafe extern "C" fn( context: *const c_void, samples: *const f32, num_frames: u32, channels: u32, sample_rate_hz: u32, timestamp_us: i64, ) -> u32; #[repr(C)] #[derive(Clone, Copy)] pub struct NativeScreenFrameSinkHandle { magic: u32, version: u32, context: *const c_void, retain: NativeScreenFrameSinkRetainFn, release: NativeScreenFrameSinkReleaseFn, enqueue_nv12: Option, enqueue_bgra: Option, enqueue_mac_cv_pixel_buffer: Option, enqueue_dmabuf: Option, enqueue_shared_texture: Option, enqueue_screen_audio: Option, } unsafe impl Send for NativeScreenFrameSinkHandle {} unsafe impl Sync for NativeScreenFrameSinkHandle {} impl NativeScreenFrameSinkHandle { fn is_valid(&self) -> bool { self.magic == NATIVE_SCREEN_FRAME_SINK_HANDLE_MAGIC && self.version == NATIVE_SCREEN_FRAME_SINK_HANDLE_VERSION && !self.context.is_null() } pub fn retain_ref(&self) -> Option { if !self.is_valid() { return None; } unsafe { (self.retain)(self.context) }; Some(NativeScreenFrameSinkHandleRef { handle: *self }) } pub fn native_outcome(outcome: EnqueueOutcome) -> u32 { match outcome { EnqueueOutcome::Accepted => NATIVE_SCREEN_FRAME_SINK_ACCEPTED, EnqueueOutcome::Coalesced => NATIVE_SCREEN_FRAME_SINK_COALESCED, EnqueueOutcome::Rejected => NATIVE_SCREEN_FRAME_SINK_REJECTED, } } pub fn outcome_from_native(value: u32) -> EnqueueOutcome { match value { NATIVE_SCREEN_FRAME_SINK_ACCEPTED => EnqueueOutcome::Accepted, NATIVE_SCREEN_FRAME_SINK_COALESCED => EnqueueOutcome::Coalesced, _ => EnqueueOutcome::Rejected, } } } pub struct NativeScreenFrameSinkHandleRef { handle: NativeScreenFrameSinkHandle, } unsafe impl Send for NativeScreenFrameSinkHandleRef {} unsafe impl Sync for NativeScreenFrameSinkHandleRef {} impl NativeScreenFrameSinkHandleRef { pub fn enqueue_nv12_copy( &self, data: &[u8], width: u32, height: u32, stride_y: u32, stride_uv: u32, timestamp_us: i64, ) -> EnqueueOutcome { let Some(enqueue) = self.handle.enqueue_nv12 else { return EnqueueOutcome::Rejected; }; NativeScreenFrameSinkHandle::outcome_from_native(unsafe { enqueue( self.handle.context, data.as_ptr(), data.len(), width, height, stride_y, stride_uv, timestamp_us, ) }) } pub fn enqueue_bgra_copy( &self, data: &[u8], width: u32, height: u32, stride: u32, timestamp_us: i64, ) -> EnqueueOutcome { let Some(enqueue) = self.handle.enqueue_bgra else { return EnqueueOutcome::Rejected; }; NativeScreenFrameSinkHandle::outcome_from_native(unsafe { enqueue( self.handle.context, data.as_ptr(), data.len(), width, height, stride, timestamp_us, ) }) } #[cfg(target_os = "macos")] pub fn supports_mac_cv_pixel_buffer(&self) -> bool { self.handle.enqueue_mac_cv_pixel_buffer.is_some() } #[cfg(target_os = "macos")] pub unsafe fn enqueue_mac_cv_pixel_buffer( &self, pixel_buffer: *mut c_void, width: u32, height: u32, pixel_format: u32, timestamp_us: i64, ) -> EnqueueOutcome { let Some(enqueue) = self.handle.enqueue_mac_cv_pixel_buffer else { return EnqueueOutcome::Rejected; }; NativeScreenFrameSinkHandle::outcome_from_native(unsafe { enqueue( self.handle.context, pixel_buffer, width, height, pixel_format, timestamp_us, ) }) } #[cfg(target_os = "linux")] pub fn supports_dmabuf(&self) -> bool { self.handle.enqueue_dmabuf.is_some() } #[cfg(target_os = "linux")] pub fn enqueue_dmabuf_take_fds(&self, desc: DmabufDesc, fds: Vec) -> EnqueueOutcome { let Some(enqueue) = self.handle.enqueue_dmabuf else { return EnqueueOutcome::Rejected; }; let fd_count = fds.len(); if fd_count == 0 || fd_count > 4 || usize::from(desc.plane_count) != fd_count { return EnqueueOutcome::Rejected; } let mut raw_fds = [-1; 4]; for (raw_fd, fd) in raw_fds.iter_mut().zip(fds) { *raw_fd = fd.into_raw_fd(); } NativeScreenFrameSinkHandle::outcome_from_native(unsafe { enqueue(self.handle.context, desc, raw_fds.as_ptr(), fd_count) }) } #[cfg(target_os = "windows")] pub fn enqueue_shared_texture(&self, desc: SharedTextureDesc) -> EnqueueOutcome { let Some(enqueue) = self.handle.enqueue_shared_texture else { return EnqueueOutcome::Rejected; }; NativeScreenFrameSinkHandle::outcome_from_native(unsafe { enqueue(self.handle.context, desc) }) } pub fn supports_screen_audio(&self) -> bool { self.handle.enqueue_screen_audio.is_some() } pub fn enqueue_screen_audio_f32( &self, samples: &[f32], num_frames: u32, channels: u32, sample_rate_hz: u32, timestamp_us: i64, ) -> EnqueueOutcome { let Some(enqueue) = self.handle.enqueue_screen_audio else { return EnqueueOutcome::Rejected; }; if channels == 0 || num_frames == 0 { return EnqueueOutcome::Rejected; } let expected_samples = (num_frames as usize).checked_mul(channels as usize); if expected_samples != Some(samples.len()) { return EnqueueOutcome::Rejected; } NativeScreenFrameSinkHandle::outcome_from_native(unsafe { enqueue( self.handle.context, samples.as_ptr(), num_frames, channels, sample_rate_hz, timestamp_us, ) }) } } impl Drop for NativeScreenFrameSinkHandleRef { fn drop(&mut self) { if self.handle.is_valid() { unsafe { (self.handle.release)(self.handle.context) }; } } } pub const STAGING_PAIR_LEN: usize = 2; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum StagingSlotState { Empty, Submitted, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum StagingError { WouldOverwrite, } pub trait StagingBackend { fn write(&mut self, fill: F); fn read R>(&self, read: F) -> R; fn is_ready(&self) -> bool; fn is_idle(&self) -> bool; } pub struct CpuStagingBackend { buffer: Vec, } impl CpuStagingBackend { pub fn new(byte_len: usize) -> Self { assert!(byte_len > 0, "staging backend length must be positive"); assert!( byte_len <= 1 << 30, "staging backend length exceeds sanity cap" ); Self { buffer: vec![0u8; byte_len], } } pub fn len(&self) -> usize { self.buffer.len() } pub fn is_empty(&self) -> bool { self.buffer.is_empty() } } impl StagingBackend for CpuStagingBackend { fn write(&mut self, fill: F) { fill(&mut self.buffer); } fn read R>(&self, read: F) -> R { read(&self.buffer) } fn is_ready(&self) -> bool { true } fn is_idle(&self) -> bool { true } } pub struct StagingSurfacePair { backends: [B; STAGING_PAIR_LEN], slot_states: [StagingSlotState; STAGING_PAIR_LEN], next_submit: u64, next_map: u64, skipped_count: u64, overwrite_dropped_count: u64, } impl StagingSurfacePair { pub fn new(backends: [B; STAGING_PAIR_LEN]) -> Self { assert_eq!( STAGING_PAIR_LEN, 2, "OBS staging pair is exactly two surfaces" ); assert!( backends[0].is_idle(), "backend zero must be idle at construction" ); assert!( backends[1].is_idle(), "backend one must be idle at construction" ); Self { backends, slot_states: [StagingSlotState::Empty; STAGING_PAIR_LEN], next_submit: 0, next_map: 0, skipped_count: 0, overwrite_dropped_count: 0, } } pub fn next_submit_sequence(&self) -> u64 { self.next_submit } pub fn next_map_sequence(&self) -> u64 { self.next_map } pub fn skipped_count(&self) -> u64 { self.skipped_count } pub fn overwrite_dropped_count(&self) -> u64 { self.overwrite_dropped_count } pub fn submit( &mut self, sequence: u64, fill: F, ) -> Result<(), StagingError> { assert_eq!(sequence, self.next_submit, "submit must use next sequence"); assert!( self.next_map <= self.next_submit, "map sequence must trail submit" ); let slot = (sequence & 1) as usize; assert!(slot < STAGING_PAIR_LEN, "slot index within pair bounds"); if self.slot_states[slot] != StagingSlotState::Empty { self.overwrite_dropped_count = self.overwrite_dropped_count.wrapping_add(1); return Err(StagingError::WouldOverwrite); } assert!( self.backends[slot].is_idle(), "backend reports busy on empty slot" ); self.backends[slot].write(fill); self.slot_states[slot] = StagingSlotState::Submitted; self.next_submit = self.next_submit.wrapping_add(1); Ok(()) } pub fn try_map R>(&mut self, sequence: u64, read: F) -> Option { assert!( self.next_map < self.next_submit, "cannot map an unsubmitted sequence" ); assert_eq!( sequence, self.next_map, "try_map must use next pending sequence" ); let slot = (sequence & 1) as usize; assert!(slot < STAGING_PAIR_LEN, "slot index within pair bounds"); assert_eq!( self.slot_states[slot], StagingSlotState::Submitted, "double map of unsubmitted slot" ); if !self.backends[slot].is_ready() { self.skipped_count = self.skipped_count.wrapping_add(1); self.slot_states[slot] = StagingSlotState::Empty; self.next_map = self.next_map.wrapping_add(1); return None; } let value = self.backends[slot].read(read); self.slot_states[slot] = StagingSlotState::Empty; self.next_map = self.next_map.wrapping_add(1); Some(value) } pub fn backend(&self, slot: usize) -> &B { assert!(slot < STAGING_PAIR_LEN, "slot index within pair bounds"); assert!( slot < self.backends.len(), "slot index within backend storage" ); &self.backends[slot] } } type SinkMap = HashMap>; static REGISTRY: OnceLock> = OnceLock::new(); fn registry() -> &'static RwLock { REGISTRY.get_or_init(|| RwLock::new(HashMap::new())) } pub fn register_sink(capture_id: impl Into, sink: Arc) { registry().write().insert(capture_id.into(), sink); } pub fn unregister_sink(capture_id: &str) -> Option> { registry().write().remove(capture_id) } pub fn get_sink(capture_id: &str) -> Option> { registry().read().get(capture_id).cloned() } pub fn clear_all_sinks() { registry().write().clear(); } #[cfg(test)] mod tests { use super::*; use std::sync::atomic::{AtomicU32, Ordering}; struct CountingSink { accepted: AtomicU32, } impl ScreenFrameSink for CountingSink { fn enqueue(&self, _frame: ScreenFrame) -> EnqueueOutcome { self.accepted.fetch_add(1, Ordering::Relaxed); EnqueueOutcome::Accepted } } fn fresh_id(label: &str) -> String { format!("test:{}:{}", label, std::process::id()) } fn solid_nv12_frame(width: u32, height: u32) -> ScreenFrame { let y = (width * height) as usize; let uv = (width * (height / 2)) as usize; let mut data = vec![0u8; y + uv]; data[..y].fill(16); data[y..].fill(128); ScreenFrame::Nv12(Nv12Frame { data: data.into(), width, height, stride_y: width, stride_uv: width, timestamp_us: 0, }) } #[test] fn sink_registration_round_trips_through_registry() { let id = fresh_id("round-trip"); let sink = Arc::new(CountingSink { accepted: AtomicU32::new(0), }); register_sink(id.clone(), sink.clone()); let resolved = get_sink(&id).expect("sink must be resolvable"); assert_eq!( resolved.enqueue(solid_nv12_frame(64, 64)), EnqueueOutcome::Accepted, ); assert_eq!(sink.accepted.load(Ordering::Relaxed), 1); let removed = unregister_sink(&id).expect("sink must be removable"); assert_eq!( removed.enqueue(solid_nv12_frame(64, 64)), EnqueueOutcome::Accepted, ); assert!(get_sink(&id).is_none()); assert!(unregister_sink(&id).is_none()); } #[test] fn screen_frame_exposes_timestamp_and_dimensions() { let frame = solid_nv12_frame(640, 480); assert_eq!(frame.dimensions(), (640, 480)); assert_eq!(frame.timestamp_us(), 0); } struct RejectingSink; impl ScreenFrameSink for RejectingSink { fn enqueue(&self, _frame: ScreenFrame) -> EnqueueOutcome { EnqueueOutcome::Rejected } } #[test] fn concurrent_producers_all_deliver_to_a_single_sink() { let id = fresh_id("concurrent-producers"); let sink = Arc::new(CountingSink { accepted: AtomicU32::new(0), }); register_sink(id.clone(), sink.clone()); let producers = 8; let frames_per_producer = 64; let handles: Vec<_> = (0..producers) .map(|p| { let id = id.clone(); std::thread::spawn(move || { for f in 0..frames_per_producer { let resolved = get_sink(&id).expect("sink missing during producer race"); assert_eq!( resolved.enqueue(solid_nv12_frame(32, 32)), EnqueueOutcome::Accepted, "producer {} frame {}", p, f ); } }) }) .collect(); for h in handles { h.join().expect("producer panicked"); } unregister_sink(&id); assert_eq!( sink.accepted.load(Ordering::Relaxed), producers * frames_per_producer ); } #[test] fn sinks_for_distinct_ids_do_not_cross_route() { let id_a = fresh_id("isolation-a"); let id_b = fresh_id("isolation-b"); let sink_a = Arc::new(CountingSink { accepted: AtomicU32::new(0), }); let sink_b: Arc = Arc::new(RejectingSink); register_sink(id_a.clone(), sink_a.clone()); register_sink(id_b.clone(), sink_b.clone()); assert_eq!( get_sink(&id_a).unwrap().enqueue(solid_nv12_frame(8, 8)), EnqueueOutcome::Accepted ); assert_eq!( get_sink(&id_b).unwrap().enqueue(solid_nv12_frame(8, 8)), EnqueueOutcome::Rejected ); assert_eq!(sink_a.accepted.load(Ordering::Relaxed), 1); unregister_sink(&id_a); unregister_sink(&id_b); } #[test] fn unregister_returns_the_active_arc_and_inflight_holders_keep_working() { let id = fresh_id("unregister-race"); let sink = Arc::new(CountingSink { accepted: AtomicU32::new(0), }); register_sink(id.clone(), sink.clone()); let inflight = get_sink(&id).expect("sink available"); let removed = unregister_sink(&id).expect("unregister returns sink"); assert!(Arc::ptr_eq(&inflight, &removed)); assert!(get_sink(&id).is_none()); assert_eq!( inflight.enqueue(solid_nv12_frame(8, 8)), EnqueueOutcome::Accepted ); assert_eq!(sink.accepted.load(Ordering::Relaxed), 1); } #[test] fn bgra_frame_exposes_timestamp_and_dimensions() { let frame = ScreenFrame::Bgra(BgraFrame { data: vec![0; 4 * 16 * 16], width: 16, height: 16, stride: 64, timestamp_us: 9_999, }); assert_eq!(frame.dimensions(), (16, 16)); assert_eq!(frame.timestamp_us(), 9_999); } struct StaticSharedBytes { bytes: Box<[u8]>, } impl SharedFrameBytes for StaticSharedBytes { fn bytes(&self) -> &[u8] { &self.bytes } } #[test] fn frame_data_owned_round_trips_without_sharing() { let data = FrameData::from(vec![1u8, 2, 3]); assert!(!data.is_shared()); assert!(!data.is_empty()); assert_eq!(data.len(), 3); assert_eq!(data.as_slice(), &[1, 2, 3]); assert_eq!(data.into_vec(), vec![1, 2, 3]); } #[test] fn frame_data_shared_round_trips_prefix_of_source_bytes() { let source = Arc::new(StaticSharedBytes { bytes: vec![7u8; 16].into_boxed_slice(), }); let data = FrameData::from_shared(source.clone(), 12, None); assert!(data.is_shared()); assert_eq!(data.len(), 12); assert_eq!(data.as_slice(), &[7u8; 12]); assert_eq!(Arc::strong_count(&source), 2); assert_eq!(data.into_vec(), vec![7u8; 12]); assert_eq!(Arc::strong_count(&source), 1); } #[test] #[should_panic] fn frame_data_shared_rejects_len_beyond_source_bytes() { let source = Arc::new(StaticSharedBytes { bytes: vec![0u8; 4].into_boxed_slice(), }); let _ = FrameData::from_shared(source, 5, None); } #[test] fn frame_data_shared_capacity_token_returns_slot_on_drop_with_counters_intact() { let pool = frame_pool::CpuFrameBuilder::build_pool_with_capacity(2, 4).expect("pool init"); let source = Arc::new(StaticSharedBytes { bytes: vec![9u8; 8].into_boxed_slice(), }); let token = pool.try_acquire().expect("first slot"); let data = FrameData::from_shared(source, 8, Some(token)); assert_eq!(pool.currently_in_flight(), 1); let second = pool.try_acquire().expect("second slot"); assert!(pool.try_acquire().is_none()); assert_eq!(pool.skipped_total(), 1); assert_eq!(pool.acquired_total(), 2); drop(data); assert_eq!(pool.currently_in_flight(), 1); let third = pool.try_acquire().expect("slot returned by frame drop"); assert_eq!(pool.acquired_total(), 3); assert_eq!(pool.skipped_total(), 1); drop(second); drop(third); assert_eq!(pool.currently_in_flight(), 0); } #[test] fn nv12_frame_with_shared_payload_exposes_timestamp_and_dimensions() { let source = Arc::new(StaticSharedBytes { bytes: vec![0u8; 64 * 96].into_boxed_slice(), }); let frame = ScreenFrame::Nv12(Nv12Frame { data: FrameData::from_shared(source, 64 * 96, None), width: 64, height: 64, stride_y: 64, stride_uv: 64, timestamp_us: 77, }); assert_eq!(frame.dimensions(), (64, 64)); assert_eq!(frame.timestamp_us(), 77); } #[cfg(target_os = "macos")] #[test] fn mac_cv_pixel_buffer_with_null_pointer_drops_without_calling_release() { let frame = unsafe { MacCvPixelBufferFrame::from_retained(std::ptr::null_mut(), 8, 8, 0, 0) }; assert_eq!(frame.pixel_buffer_ptr(), std::ptr::null_mut()); drop(frame); } const STAGING_TEST_LEN: usize = 64; fn fresh_staging_pair() -> StagingSurfacePair { let backends = [ CpuStagingBackend::new(STAGING_TEST_LEN), CpuStagingBackend::new(STAGING_TEST_LEN), ]; assert_eq!(backends[0].len(), STAGING_TEST_LEN); assert_eq!(backends[1].len(), STAGING_TEST_LEN); StagingSurfacePair::new(backends) } #[test] fn submit_then_map_returns_the_data_from_two_frames_ago() { let mut pair = fresh_staging_pair(); assert!(pair.submit(0, |buf| buf.fill(0xA1)).is_ok()); assert!(pair.submit(1, |buf| buf.fill(0xB2)).is_ok()); let first = pair .try_map(0, |buf| buf[0]) .expect("first map ready under CPU backend"); assert_eq!(first, 0xA1); assert!(pair.submit(2, |buf| buf.fill(0xC3)).is_ok()); let second = pair .try_map(1, |buf| buf[0]) .expect("second map ready under CPU backend"); assert_eq!(second, 0xB2); let third = pair .try_map(2, |buf| buf[0]) .expect("third map ready under CPU backend"); assert_eq!(third, 0xC3); } #[test] fn two_submits_in_a_row_preserve_distinct_slots() { let mut pair = fresh_staging_pair(); assert!(pair.submit(0, |buf| buf.fill(0x11)).is_ok()); assert!(pair.submit(1, |buf| buf.fill(0x22)).is_ok()); assert_eq!(pair.next_submit_sequence(), 2); let first = pair .try_map(0, |buf| buf[0]) .expect("slot zero still holds first submission"); assert_eq!(first, 0x11); let second = pair .try_map(1, |buf| buf[0]) .expect("slot one still holds second submission"); assert_eq!(second, 0x22); } #[test] fn third_unmapped_submit_returns_would_overwrite_and_drops_newest() { let mut pair = fresh_staging_pair(); assert!(pair.submit(0, |buf| buf.fill(0x33)).is_ok()); assert!(pair.submit(1, |buf| buf.fill(0x44)).is_ok()); assert_eq!(pair.overwrite_dropped_count(), 0); let outcome = pair.submit(2, |buf| buf.fill(0xFF)); assert_eq!(outcome, Err(StagingError::WouldOverwrite)); assert_eq!(pair.overwrite_dropped_count(), 1); let preserved = pair .try_map(0, |buf| buf[0]) .expect("slot zero kept its original submission"); assert_eq!(preserved, 0x33); } struct GatedBackend { buffer: Vec, ready: bool, } impl GatedBackend { fn new(len: usize) -> Self { assert!(len > 0, "gated backend len positive"); assert!(len <= 1 << 16, "gated backend len within sanity cap"); Self { buffer: vec![0u8; len], ready: true, } } } impl StagingBackend for GatedBackend { fn write(&mut self, fill: F) { fill(&mut self.buffer); } fn read R>(&self, read: F) -> R { read(&self.buffer) } fn is_ready(&self) -> bool { self.ready } fn is_idle(&self) -> bool { true } } #[test] fn try_map_returning_none_advances_and_bumps_skipped_counter() { let mut a = GatedBackend::new(STAGING_TEST_LEN); let mut b = GatedBackend::new(STAGING_TEST_LEN); a.ready = false; b.ready = true; let mut pair = StagingSurfacePair::new([a, b]); assert!(pair.submit(0, |buf| buf.fill(0x55)).is_ok()); assert!(pair.submit(1, |buf| buf.fill(0x66)).is_ok()); let skipped = pair.try_map(0, |buf| buf[0]); assert!(skipped.is_none()); assert_eq!(pair.skipped_count(), 1); assert_eq!(pair.next_map_sequence(), 1); let mapped = pair.try_map(1, |buf| buf[0]).expect("slot one ready"); assert_eq!(mapped, 0x66); assert_eq!(pair.next_map_sequence(), 2); assert!(pair.submit(2, |buf| buf.fill(0x77)).is_ok()); assert!(pair.submit(3, |buf| buf.fill(0x88)).is_ok()); let after_skip = pair.try_map(2, |buf| buf[0]); assert!(after_skip.is_none()); assert_eq!(pair.skipped_count(), 2); } #[test] #[should_panic(expected = "submit must use next sequence")] fn submit_with_out_of_order_sequence_panics() { let mut pair = fresh_staging_pair(); let _ = pair.submit(7, |buf| buf.fill(0)); } #[test] #[should_panic(expected = "try_map must use next pending sequence")] fn try_map_with_wrong_sequence_panics() { let mut pair = fresh_staging_pair(); assert!(pair.submit(0, |buf| buf.fill(0)).is_ok()); let _ = pair.try_map(5, |buf| buf[0]); } #[test] #[should_panic(expected = "cannot map an unsubmitted sequence")] fn try_map_without_any_submit_panics() { let mut pair = fresh_staging_pair(); let _ = pair.try_map(0, |buf| buf[0]); } #[test] #[should_panic(expected = "try_map must use next pending sequence")] fn try_map_twice_on_the_same_sequence_panics() { let mut pair = fresh_staging_pair(); assert!(pair.submit(0, |buf| buf.fill(0xAB)).is_ok()); assert!(pair.submit(1, |buf| buf.fill(0xCD)).is_ok()); let first = pair.try_map(0, |buf| buf[0]).expect("first map"); assert_eq!(first, 0xAB); let _ = pair.try_map(0, |buf| buf[0]); } static SCREEN_AUDIO_ENQUEUE_CALLS: AtomicU32 = AtomicU32::new(0); unsafe extern "C" fn screen_audio_test_retain(_context: *const c_void) {} unsafe extern "C" fn screen_audio_test_release(_context: *const c_void) {} unsafe extern "C" fn screen_audio_test_enqueue( _context: *const c_void, _samples: *const f32, _num_frames: u32, _channels: u32, _sample_rate_hz: u32, _timestamp_us: i64, ) -> u32 { SCREEN_AUDIO_ENQUEUE_CALLS.fetch_add(1, Ordering::Relaxed); NATIVE_SCREEN_FRAME_SINK_ACCEPTED } fn screen_audio_test_handle_ref() -> NativeScreenFrameSinkHandleRef { let handle = NativeScreenFrameSinkHandle { magic: NATIVE_SCREEN_FRAME_SINK_HANDLE_MAGIC, version: NATIVE_SCREEN_FRAME_SINK_HANDLE_VERSION, context: (&SCREEN_AUDIO_ENQUEUE_CALLS as *const AtomicU32).cast::(), retain: screen_audio_test_retain, release: screen_audio_test_release, enqueue_nv12: None, enqueue_bgra: None, enqueue_mac_cv_pixel_buffer: None, enqueue_dmabuf: None, enqueue_shared_texture: None, enqueue_screen_audio: Some(screen_audio_test_enqueue), }; NativeScreenFrameSinkHandleRef { handle } } #[test] fn enqueue_screen_audio_rejects_malformed_inputs_without_panicking() { SCREEN_AUDIO_ENQUEUE_CALLS.store(0, Ordering::Relaxed); let sink = screen_audio_test_handle_ref(); assert_eq!( sink.enqueue_screen_audio_f32(&[0.0, 0.0], 1, 0, 48_000, 0), EnqueueOutcome::Rejected ); assert_eq!( sink.enqueue_screen_audio_f32(&[], 0, 2, 48_000, 0), EnqueueOutcome::Rejected ); assert_eq!( sink.enqueue_screen_audio_f32(&[0.0, 0.0, 0.0], 1, 2, 48_000, 0), EnqueueOutcome::Rejected ); assert_eq!(SCREEN_AUDIO_ENQUEUE_CALLS.load(Ordering::Relaxed), 0); } #[test] fn enqueue_screen_audio_accepts_whole_stereo_frames() { SCREEN_AUDIO_ENQUEUE_CALLS.store(0, Ordering::Relaxed); let sink = screen_audio_test_handle_ref(); assert_eq!( sink.enqueue_screen_audio_f32(&[0.1, 0.2, 0.3, 0.4], 2, 2, 48_000, 7), EnqueueOutcome::Accepted ); assert_eq!(SCREEN_AUDIO_ENQUEUE_CALLS.load(Ordering::Relaxed), 1); } }