chore(repo): prune dead code and duplicate abstractions (#3413)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-12 13:09:42 +02:00
1 parent c17091768c
commit c9fdf8fead
2937 files changed
+42233 -176793

No files matched your search

@@ -1,596 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use parking_lot::Mutex;
use std::cell::UnsafeCell;
use std::fmt;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
pub const MAC_AUDIO_POOL_CAP: usize = 16;
pub const MAX_FRAME_BYTES_PER_SLOT: usize = 8192;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacAudioError {
ZeroCapacity,
ZeroBytesPerSlot,
BytesPerSlotTooLarge(usize),
PayloadTooLarge { offered: usize, capacity: usize },
}
impl fmt::Display for MacAudioError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ZeroCapacity => write!(f, "MacAudioFramePool capacity must be > 0"),
Self::ZeroBytesPerSlot => write!(f, "MacAudioFramePool bytes_per_slot must be > 0"),
Self::BytesPerSlotTooLarge(n) => write!(
f,
"MacAudioFramePool bytes_per_slot {n} exceeds MAX_FRAME_BYTES_PER_SLOT={MAX_FRAME_BYTES_PER_SLOT}"
),
Self::PayloadTooLarge { offered, capacity } => write!(
f,
"MacAudioFramePool payload {offered} bytes exceeds slot capacity {capacity}"
),
}
}
}
impl std::error::Error for MacAudioError {}
struct PoolSlotCell {
inner: UnsafeCell<Box<[u8]>>,
}
unsafe impl Send for PoolSlotCell {}
unsafe impl Sync for PoolSlotCell {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MacAudioPoolStats {
pub acquired: u64,
pub released: u64,
pub dropped: u64,
pub in_flight: u32,
}
pub(crate) struct MacAudioFramePoolInner {
slots: Vec<PoolSlotCell>,
free: Mutex<Vec<usize>>,
capacity: u32,
bytes_per_slot: u32,
acquired_total: AtomicU64,
released_total: AtomicU64,
dropped_total: AtomicU64,
in_flight: AtomicU32,
}
pub struct MacAudioFramePool {
inner: Arc<MacAudioFramePoolInner>,
}
impl fmt::Debug for MacAudioFramePool {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let stats = self.stats();
f.debug_struct("MacAudioFramePool")
.field("capacity", &self.inner.capacity)
.field("bytes_per_slot", &self.inner.bytes_per_slot)
.field("stats", &stats)
.finish()
}
}
impl MacAudioFramePool {
pub fn new(capacity: usize, bytes_per_slot: usize) -> Result<Self, MacAudioError> {
if capacity == 0 {
return Err(MacAudioError::ZeroCapacity);
}
if bytes_per_slot == 0 {
return Err(MacAudioError::ZeroBytesPerSlot);
}
if bytes_per_slot > MAX_FRAME_BYTES_PER_SLOT {
return Err(MacAudioError::BytesPerSlotTooLarge(bytes_per_slot));
}
assert!(capacity > 0);
assert!(bytes_per_slot > 0);
assert!(bytes_per_slot <= MAX_FRAME_BYTES_PER_SLOT);
let mut slots: Vec<PoolSlotCell> = Vec::with_capacity(capacity);
for _ in 0..capacity {
let buf: Box<[u8]> = vec![0u8; bytes_per_slot].into_boxed_slice();
assert_eq!(buf.len(), bytes_per_slot);
slots.push(PoolSlotCell {
inner: UnsafeCell::new(buf),
});
}
assert_eq!(slots.len(), capacity);
let mut free: Vec<usize> = Vec::with_capacity(capacity);
for index in 0..capacity {
free.push(index);
}
assert_eq!(free.len(), capacity);
let cap_u32 = u32::try_from(capacity).map_err(|_| MacAudioError::ZeroCapacity)?;
let bps_u32 = u32::try_from(bytes_per_slot).map_err(|_| MacAudioError::ZeroBytesPerSlot)?;
let inner = MacAudioFramePoolInner {
slots,
free: Mutex::new(free),
capacity: cap_u32,
bytes_per_slot: bps_u32,
acquired_total: AtomicU64::new(0),
released_total: AtomicU64::new(0),
dropped_total: AtomicU64::new(0),
in_flight: AtomicU32::new(0),
};
Ok(Self {
inner: Arc::new(inner),
})
}
pub fn try_acquire(&self) -> Option<PooledMacAudioFrame> {
assert!(self.inner.capacity > 0);
assert!(self.inner.bytes_per_slot > 0);
let mut free = self.inner.free.lock();
assert!(free.len() <= self.inner.capacity as usize);
let index = match free.pop() {
Some(idx) => idx,
None => {
drop(free);
self.inner.dropped_total.fetch_add(1, Ordering::Relaxed);
return None;
}
};
assert!(index < self.inner.capacity as usize);
self.inner.acquired_total.fetch_add(1, Ordering::Relaxed);
let after = self.inner.in_flight.fetch_add(1, Ordering::AcqRel) + 1;
assert!(after <= self.inner.capacity);
drop(free);
Some(PooledMacAudioFrame {
slot_index: index,
filled_len: 0,
pool: Arc::clone(&self.inner),
})
}
pub fn capacity(&self) -> u32 {
let cap = self.inner.capacity;
assert!(cap > 0);
assert!(cap as usize == self.inner.slots.len());
cap
}
pub fn bytes_per_slot(&self) -> u32 {
let bps = self.inner.bytes_per_slot;
assert!(bps > 0);
assert!(bps as usize <= MAX_FRAME_BYTES_PER_SLOT);
bps
}
pub fn stats(&self) -> MacAudioPoolStats {
assert!(self.inner.capacity > 0);
let in_flight = self.inner.in_flight.load(Ordering::Acquire);
assert!(in_flight <= self.inner.capacity);
let acquired = self.inner.acquired_total.load(Ordering::Relaxed);
let released = self.inner.released_total.load(Ordering::Relaxed);
let dropped = self.inner.dropped_total.load(Ordering::Relaxed);
assert!(released <= acquired);
MacAudioPoolStats {
acquired,
released,
dropped,
in_flight,
}
}
}
impl Clone for MacAudioFramePool {
fn clone(&self) -> Self {
Self {
inner: Arc::clone(&self.inner),
}
}
}
pub struct PooledMacAudioFrame {
slot_index: usize,
filled_len: usize,
pool: Arc<MacAudioFramePoolInner>,
}
impl PooledMacAudioFrame {
pub fn write(&mut self, samples: &[u8]) -> Result<(), MacAudioError> {
assert!(self.slot_index < self.pool.capacity as usize);
let cap = self.pool.bytes_per_slot as usize;
if samples.len() > cap {
return Err(MacAudioError::PayloadTooLarge {
offered: samples.len(),
capacity: cap,
});
}
assert!(samples.len() <= cap);
let cell = &self.pool.slots[self.slot_index];
let buf: &mut [u8] = unsafe { &mut *cell.inner.get() };
assert_eq!(buf.len(), cap);
if !samples.is_empty() {
buf[..samples.len()].copy_from_slice(samples);
}
self.filled_len = samples.len();
assert!(self.filled_len <= cap);
Ok(())
}
pub fn append(&mut self, samples: &[u8]) -> Result<(), MacAudioError> {
assert!(self.slot_index < self.pool.capacity as usize);
let cap = self.pool.bytes_per_slot as usize;
let new_len =
self.filled_len
.checked_add(samples.len())
.ok_or(MacAudioError::PayloadTooLarge {
offered: usize::MAX,
capacity: cap,
})?;
if new_len > cap {
return Err(MacAudioError::PayloadTooLarge {
offered: new_len,
capacity: cap,
});
}
assert!(new_len <= cap);
let cell = &self.pool.slots[self.slot_index];
let buf: &mut [u8] = unsafe { &mut *cell.inner.get() };
assert_eq!(buf.len(), cap);
if !samples.is_empty() {
buf[self.filled_len..new_len].copy_from_slice(samples);
}
self.filled_len = new_len;
Ok(())
}
pub fn data_slice(&self) -> &[u8] {
assert!(self.slot_index < self.pool.capacity as usize);
assert!(self.filled_len <= self.pool.bytes_per_slot as usize);
let cell = &self.pool.slots[self.slot_index];
let buf: &[u8] = unsafe { &*cell.inner.get() };
&buf[..self.filled_len]
}
pub fn filled_len(&self) -> usize {
assert!(self.slot_index < self.pool.capacity as usize);
assert!(self.filled_len <= self.pool.bytes_per_slot as usize);
self.filled_len
}
pub fn capacity(&self) -> usize {
let cap = self.pool.bytes_per_slot as usize;
assert!(cap > 0);
assert!(cap <= MAX_FRAME_BYTES_PER_SLOT);
cap
}
pub fn slot_index(&self) -> usize {
assert!(self.slot_index < self.pool.capacity as usize);
self.slot_index
}
pub fn as_mut_ptr(&mut self) -> *mut u8 {
assert!(self.slot_index < self.pool.capacity as usize);
assert!(self.filled_len <= self.pool.bytes_per_slot as usize);
let cell = &self.pool.slots[self.slot_index];
let buf: &mut [u8] = unsafe { &mut *cell.inner.get() };
assert_eq!(buf.len(), self.pool.bytes_per_slot as usize);
buf.as_mut_ptr()
}
pub fn into_external_parts(mut self) -> (*mut u8, usize, Self) {
assert!(self.slot_index < self.pool.capacity as usize);
assert!(self.filled_len <= self.pool.bytes_per_slot as usize);
let len = self.filled_len;
let ptr = self.as_mut_ptr();
assert!(!ptr.is_null());
(ptr, len, self)
}
}
impl Drop for PooledMacAudioFrame {
fn drop(&mut self) {
assert!(self.slot_index < self.pool.capacity as usize);
let mut free = self.pool.free.lock();
assert!(free.len() < self.pool.capacity as usize);
free.push(self.slot_index);
let before = self.pool.in_flight.fetch_sub(1, Ordering::AcqRel);
assert!(before >= 1);
self.pool.released_total.fetch_add(1, Ordering::Relaxed);
drop(free);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Barrier;
use std::sync::atomic::AtomicUsize;
use std::thread;
use std::time::{Duration, Instant};
const SLOT_BYTES: usize = 3840;
fn default_pool() -> MacAudioFramePool {
MacAudioFramePool::new(MAC_AUDIO_POOL_CAP, SLOT_BYTES).expect("default pool builds")
}
#[test]
fn new_rejects_zero_capacity() {
let err = MacAudioFramePool::new(0, SLOT_BYTES).unwrap_err();
assert_eq!(err, MacAudioError::ZeroCapacity);
}
#[test]
fn new_rejects_zero_bytes_per_slot() {
let err = MacAudioFramePool::new(4, 0).unwrap_err();
assert_eq!(err, MacAudioError::ZeroBytesPerSlot);
}
#[test]
fn new_rejects_bytes_per_slot_above_max() {
let err = MacAudioFramePool::new(4, MAX_FRAME_BYTES_PER_SLOT + 1).unwrap_err();
assert_eq!(
err,
MacAudioError::BytesPerSlotTooLarge(MAX_FRAME_BYTES_PER_SLOT + 1)
);
}
#[test]
fn acquire_release_cycle_increments_counters() {
let pool = MacAudioFramePool::new(4, SLOT_BYTES).expect("pool");
let stats_before = pool.stats();
assert_eq!(stats_before.acquired, 0);
assert_eq!(stats_before.released, 0);
assert_eq!(stats_before.in_flight, 0);
{
let _slot = pool.try_acquire().expect("slot");
let stats_held = pool.stats();
assert_eq!(stats_held.acquired, 1);
assert_eq!(stats_held.in_flight, 1);
}
let stats_after = pool.stats();
assert_eq!(stats_after.acquired, 1);
assert_eq!(stats_after.released, 1);
assert_eq!(stats_after.in_flight, 0);
}
#[test]
fn pool_exhausts_at_cap_seventeenth_acquire_returns_none() {
let pool = default_pool();
let mut held = Vec::with_capacity(MAC_AUDIO_POOL_CAP);
for _ in 0..MAC_AUDIO_POOL_CAP {
held.push(pool.try_acquire().expect("slot in capacity"));
}
assert!(pool.try_acquire().is_none());
let stats = pool.stats();
assert_eq!(stats.dropped, 1);
assert_eq!(stats.acquired as usize, MAC_AUDIO_POOL_CAP);
assert_eq!(stats.in_flight as usize, MAC_AUDIO_POOL_CAP);
}
#[test]
fn drop_returns_slot_to_free_list() {
let pool = MacAudioFramePool::new(2, SLOT_BYTES).expect("pool");
let first = pool.try_acquire().expect("first");
let second = pool.try_acquire().expect("second");
assert!(pool.try_acquire().is_none());
drop(first);
let revived = pool.try_acquire().expect("revived");
assert_eq!(pool.stats().in_flight, 2);
drop(second);
drop(revived);
assert_eq!(pool.stats().in_flight, 0);
}
#[test]
fn write_then_data_slice_matches_payload() {
let pool = MacAudioFramePool::new(2, 64).expect("pool");
let mut slot = pool.try_acquire().expect("slot");
let payload = [0xAB_u8; 32];
slot.write(&payload).expect("payload fits");
assert_eq!(slot.data_slice(), &payload[..]);
assert_eq!(slot.filled_len(), 32);
}
#[test]
fn write_rejects_payload_larger_than_slot() {
let pool = MacAudioFramePool::new(2, 64).expect("pool");
let mut slot = pool.try_acquire().expect("slot");
let big = [0u8; 128];
let err = slot.write(&big).unwrap_err();
assert!(matches!(err, MacAudioError::PayloadTooLarge { .. }));
}
#[test]
fn append_concatenates_planar_to_interleaved_like_payload() {
let pool = MacAudioFramePool::new(1, 32).expect("pool");
let mut slot = pool.try_acquire().expect("slot");
let left = [1_u8, 2, 3, 4];
let right = [5_u8, 6, 7, 8];
slot.append(&left).expect("left fits");
slot.append(&right).expect("right fits");
assert_eq!(slot.data_slice(), &[1, 2, 3, 4, 5, 6, 7, 8]);
assert_eq!(slot.filled_len(), 8);
}
#[test]
fn append_rejects_overflow() {
let pool = MacAudioFramePool::new(1, 4).expect("pool");
let mut slot = pool.try_acquire().expect("slot");
slot.append(&[1, 2, 3]).expect("3 of 4 fits");
let err = slot.append(&[4, 5]).unwrap_err();
assert!(matches!(err, MacAudioError::PayloadTooLarge { .. }));
}
#[test]
fn recycled_slot_persists_buffer_storage_until_overwritten() {
let pool = MacAudioFramePool::new(1, 16).expect("pool");
let mut slot = pool.try_acquire().expect("first");
let payload = [0x42_u8; 8];
slot.write(&payload).expect("write");
let observed_first = slot.data_slice().to_vec();
drop(slot);
let slot2 = pool.try_acquire().expect("recycled");
assert_eq!(slot2.filled_len(), 0);
assert_eq!(observed_first, payload.to_vec());
}
#[test]
fn multi_thread_acquire_release_stress_8_threads_1000_ops_no_deadlock() {
const THREADS: usize = 8;
const OPS_PER_THREAD: usize = 1000;
const POOL_CAP: usize = 4;
let pool = Arc::new(MacAudioFramePool::new(POOL_CAP, 64).expect("pool"));
let barrier = Arc::new(Barrier::new(THREADS));
let acquired_obs = Arc::new(AtomicUsize::new(0));
let dropped_obs = Arc::new(AtomicUsize::new(0));
let mut handles = Vec::with_capacity(THREADS);
for _ in 0..THREADS {
let pool = Arc::clone(&pool);
let barrier = Arc::clone(&barrier);
let acquired_obs = Arc::clone(&acquired_obs);
let dropped_obs = Arc::clone(&dropped_obs);
handles.push(thread::spawn(move || {
barrier.wait();
let mut local_acq: usize = 0;
let mut local_drop: usize = 0;
for _ in 0..OPS_PER_THREAD {
match pool.try_acquire() {
Some(mut slot) => {
local_acq += 1;
let _ = slot.write(&[0xAA; 16]);
drop(slot);
}
None => {
local_drop += 1;
}
}
}
acquired_obs.fetch_add(local_acq, Ordering::Relaxed);
dropped_obs.fetch_add(local_drop, Ordering::Relaxed);
}));
}
let deadline = Instant::now() + Duration::from_secs(30);
for h in handles {
assert!(Instant::now() < deadline, "stress test exceeded 30s budget");
h.join().expect("worker panicked");
}
let stats = pool.stats();
let total = stats.acquired + stats.dropped;
assert_eq!(total as usize, THREADS * OPS_PER_THREAD);
assert_eq!(
stats.acquired as usize,
acquired_obs.load(Ordering::Relaxed)
);
assert_eq!(stats.dropped as usize, dropped_obs.load(Ordering::Relaxed));
assert_eq!(stats.in_flight, 0);
assert_eq!(stats.acquired, stats.released);
}
#[test]
fn default_pool_dimensions_match_constants() {
let pool = default_pool();
assert_eq!(pool.capacity() as usize, MAC_AUDIO_POOL_CAP);
assert_eq!(pool.bytes_per_slot() as usize, SLOT_BYTES);
}
#[test]
fn stats_in_flight_matches_simultaneous_holders() {
let pool = MacAudioFramePool::new(8, 64).expect("pool");
let a = pool.try_acquire().expect("a");
let b = pool.try_acquire().expect("b");
let c = pool.try_acquire().expect("c");
assert_eq!(pool.stats().in_flight, 3);
drop(b);
assert_eq!(pool.stats().in_flight, 2);
drop(a);
drop(c);
assert_eq!(pool.stats().in_flight, 0);
}
#[test]
fn into_external_parts_exposes_filled_pointer_and_length() {
let pool = MacAudioFramePool::new(2, 64).expect("pool");
let mut slot = pool.try_acquire().expect("slot");
let payload = [0x5A_u8; 16];
slot.write(&payload).expect("write fits");
let (ptr, len, owned) = slot.into_external_parts();
assert!(!ptr.is_null());
assert_eq!(len, 16);
let observed = unsafe { core::slice::from_raw_parts(ptr, len) };
assert_eq!(observed, &payload[..]);
assert_eq!(pool.stats().in_flight, 1);
drop(owned);
assert_eq!(pool.stats().in_flight, 0);
}
#[test]
fn into_external_parts_drop_returns_slot_to_pool() {
let pool = MacAudioFramePool::new(1, 32).expect("pool");
let mut slot = pool.try_acquire().expect("slot");
slot.write(&[0xCC_u8; 8]).expect("write");
let (_ptr, _len, owned) = slot.into_external_parts();
assert!(pool.try_acquire().is_none());
drop(owned);
let revived = pool.try_acquire().expect("revived");
assert_eq!(revived.filled_len(), 0);
drop(revived);
assert_eq!(pool.stats().in_flight, 0);
}
#[test]
fn into_external_parts_holds_slot_across_send() {
let pool = MacAudioFramePool::new(2, 64).expect("pool");
let mut slot = pool.try_acquire().expect("slot");
slot.write(&[0x77_u8; 32]).expect("write");
let (ptr, len, owned) = slot.into_external_parts();
let ptr_addr = ptr as usize;
let handle = thread::spawn(move || {
let owned = owned;
assert_eq!(owned.filled_len(), 32);
let observed = unsafe { core::slice::from_raw_parts(ptr_addr as *const u8, len) };
assert_eq!(observed[0], 0x77);
drop(owned);
});
handle.join().expect("worker");
assert_eq!(pool.stats().in_flight, 0);
}
#[test]
fn as_mut_ptr_returns_slot_base_address() {
let pool = MacAudioFramePool::new(1, 64).expect("pool");
let mut slot = pool.try_acquire().expect("slot");
let payload = [0x11_u8; 8];
slot.write(&payload).expect("write");
let ptr = slot.as_mut_ptr();
assert!(!ptr.is_null());
unsafe { ptr.add(0).write(0x22) };
unsafe { ptr.add(1).write(0x33) };
assert_eq!(&slot.data_slice()[..2], &[0x22, 0x33]);
}
#[test]
fn capacity_one_pool_round_trips() {
let pool = MacAudioFramePool::new(1, 32).expect("pool");
for _ in 0..5 {
let mut slot = pool.try_acquire().expect("slot");
slot.write(&[1, 2, 3]).expect("write");
assert_eq!(slot.data_slice(), &[1, 2, 3]);
drop(slot);
}
let stats = pool.stats();
assert_eq!(stats.acquired, 5);
assert_eq!(stats.released, 5);
assert_eq!(stats.in_flight, 0);
}
}
@@ -1,467 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::fmt;
pub const FPS_MIN: u32 = 1;
pub const FPS_MAX: u32 = 120;
pub const MAX_OUTPUT_WIDTH_DEFAULT: u32 = 3840;
pub const MAX_OUTPUT_HEIGHT_DEFAULT: u32 = 2160;
pub const OUTPUT_DIMENSION_MIN: u32 = 2;
pub const QUEUE_DEPTH_MIN: u32 = 1;
pub const QUEUE_DEPTH_MAX: u32 = 16;
pub const QUEUE_DEPTH_DEFAULT: u32 = 8;
pub const FPS_DEFAULT: u32 = 30;
pub const FRAME_INTERVAL_FACTOR_NUM: u64 = 9;
pub const FRAME_INTERVAL_FACTOR_DEN: u64 = 10;
pub const PIXEL_FORMAT_BGRA_FOURCC: u32 = u32::from_be_bytes(*b"BGRA");
pub const PIXEL_FORMAT_L10R_FOURCC: u32 = u32::from_be_bytes(*b"l10r");
pub const PIXEL_FORMAT_420V_FOURCC: u32 = u32::from_be_bytes(*b"420v");
pub const PIXEL_FORMAT_420F_FOURCC: u32 = u32::from_be_bytes(*b"420f");
pub const AUDIO_SAMPLE_RATE_DEFAULT_HZ: u32 = 48_000;
pub const AUDIO_CHANNEL_COUNT_DEFAULT: u32 = 2;
pub const AUDIO_SAMPLE_RATE_MIN_HZ: u32 = 8_000;
pub const AUDIO_SAMPLE_RATE_MAX_HZ: u32 = 192_000;
pub const AUDIO_CHANNEL_COUNT_MIN: u32 = 1;
pub const AUDIO_CHANNEL_COUNT_MAX: u32 = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SckPixelFormat {
Bgra8,
L10rHdr,
Nv12VideoRange,
Nv12FullRange,
}
impl SckPixelFormat {
pub fn as_fourcc(self) -> u32 {
let value = match self {
SckPixelFormat::Bgra8 => PIXEL_FORMAT_BGRA_FOURCC,
SckPixelFormat::L10rHdr => PIXEL_FORMAT_L10R_FOURCC,
SckPixelFormat::Nv12VideoRange => PIXEL_FORMAT_420V_FOURCC,
SckPixelFormat::Nv12FullRange => PIXEL_FORMAT_420F_FOURCC,
};
assert!(value != 0);
value
}
pub fn is_hdr(self) -> bool {
matches!(self, SckPixelFormat::L10rHdr)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SckColorSpace {
DisplayP3,
SrgbBt709,
}
impl SckColorSpace {
pub fn as_cf_name(self) -> &'static str {
let name = match self {
SckColorSpace::DisplayP3 => "kCGColorSpaceDisplayP3",
SckColorSpace::SrgbBt709 => "kCGColorSpaceSRGB",
};
assert!(!name.is_empty());
name
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SckError {
InvalidFps(u32),
InvalidQueueDepth(u32),
HdrRequiresWideColorSpace,
InvalidAudioSampleRate(u32),
InvalidAudioChannelCount(u32),
}
impl fmt::Display for SckError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SckError::InvalidFps(v) => write!(
f,
"SckCaptureConfig: target_fps={v} out of range [{FPS_MIN}..={FPS_MAX}]"
),
SckError::InvalidQueueDepth(v) => write!(
f,
"SckCaptureConfig: queue_depth={v} out of range [{QUEUE_DEPTH_MIN}..={QUEUE_DEPTH_MAX}]"
),
SckError::HdrRequiresWideColorSpace => write!(
f,
"SckCaptureConfig: l10r HDR pixel format requires DisplayP3 color space"
),
SckError::InvalidAudioSampleRate(v) => write!(
f,
"SckCaptureConfig: audio_sample_rate_hz={v} out of range [{AUDIO_SAMPLE_RATE_MIN_HZ}..={AUDIO_SAMPLE_RATE_MAX_HZ}]"
),
SckError::InvalidAudioChannelCount(v) => write!(
f,
"SckCaptureConfig: audio_channels={v} out of range [{AUDIO_CHANNEL_COUNT_MIN}..={AUDIO_CHANNEL_COUNT_MAX}]"
),
}
}
}
impl std::error::Error for SckError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SckCaptureConfig {
target_fps: u32,
queue_depth: u32,
pixel_format: SckPixelFormat,
color_space: SckColorSpace,
captures_audio: bool,
audio_sample_rate_hz: u32,
audio_channels: u32,
}
impl SckCaptureConfig {
pub fn new(
target_fps: u32,
queue_depth: u32,
pixel_format: SckPixelFormat,
color_space: SckColorSpace,
) -> Result<Self, SckError> {
Self::new_with_audio(
target_fps,
queue_depth,
pixel_format,
color_space,
false,
AUDIO_SAMPLE_RATE_DEFAULT_HZ,
AUDIO_CHANNEL_COUNT_DEFAULT,
)
}
pub fn new_with_audio(
target_fps: u32,
queue_depth: u32,
pixel_format: SckPixelFormat,
color_space: SckColorSpace,
captures_audio: bool,
audio_sample_rate_hz: u32,
audio_channels: u32,
) -> Result<Self, SckError> {
if !(FPS_MIN..=FPS_MAX).contains(&target_fps) {
return Err(SckError::InvalidFps(target_fps));
}
if !(QUEUE_DEPTH_MIN..=QUEUE_DEPTH_MAX).contains(&queue_depth) {
return Err(SckError::InvalidQueueDepth(queue_depth));
}
if pixel_format.is_hdr() && color_space != SckColorSpace::DisplayP3 {
return Err(SckError::HdrRequiresWideColorSpace);
}
if !(AUDIO_SAMPLE_RATE_MIN_HZ..=AUDIO_SAMPLE_RATE_MAX_HZ).contains(&audio_sample_rate_hz) {
return Err(SckError::InvalidAudioSampleRate(audio_sample_rate_hz));
}
if !(AUDIO_CHANNEL_COUNT_MIN..=AUDIO_CHANNEL_COUNT_MAX).contains(&audio_channels) {
return Err(SckError::InvalidAudioChannelCount(audio_channels));
}
let cfg = Self {
target_fps,
queue_depth,
pixel_format,
color_space,
captures_audio,
audio_sample_rate_hz,
audio_channels,
};
assert!(cfg.target_fps >= FPS_MIN);
assert!(cfg.target_fps <= FPS_MAX);
assert!(cfg.queue_depth >= QUEUE_DEPTH_MIN);
assert!(cfg.queue_depth <= QUEUE_DEPTH_MAX);
assert!(cfg.audio_sample_rate_hz >= AUDIO_SAMPLE_RATE_MIN_HZ);
assert!(cfg.audio_channels >= AUDIO_CHANNEL_COUNT_MIN);
Ok(cfg)
}
pub fn builder() -> SckCaptureConfigBuilder {
SckCaptureConfigBuilder::default()
}
pub fn target_fps(&self) -> u32 {
assert!(self.target_fps >= FPS_MIN);
assert!(self.target_fps <= FPS_MAX);
self.target_fps
}
pub fn queue_depth(&self) -> u32 {
assert!(self.queue_depth >= QUEUE_DEPTH_MIN);
assert!(self.queue_depth <= QUEUE_DEPTH_MAX);
self.queue_depth
}
pub fn pixel_format(&self) -> SckPixelFormat {
let pf = self.pixel_format;
assert!(pf.as_fourcc() != 0);
pf
}
pub fn color_space(&self) -> SckColorSpace {
let cs = self.color_space;
assert!(!cs.as_cf_name().is_empty());
cs
}
pub fn minimum_frame_interval_ns(&self) -> u64 {
assert!(self.target_fps >= FPS_MIN);
assert!(self.target_fps <= FPS_MAX);
let base_ns: u64 = 1_000_000_000 / (self.target_fps as u64);
let scaled = base_ns * FRAME_INTERVAL_FACTOR_NUM / FRAME_INTERVAL_FACTOR_DEN;
assert!(scaled > 0);
assert!(scaled <= 1_000_000_000);
scaled
}
pub fn captures_audio(&self) -> bool {
self.captures_audio
}
pub fn audio_sample_rate_hz(&self) -> u32 {
assert!(self.audio_sample_rate_hz >= AUDIO_SAMPLE_RATE_MIN_HZ);
assert!(self.audio_sample_rate_hz <= AUDIO_SAMPLE_RATE_MAX_HZ);
self.audio_sample_rate_hz
}
pub fn audio_channels(&self) -> u32 {
assert!(self.audio_channels >= AUDIO_CHANNEL_COUNT_MIN);
assert!(self.audio_channels <= AUDIO_CHANNEL_COUNT_MAX);
self.audio_channels
}
}
impl Default for SckCaptureConfig {
fn default() -> Self {
let cfg = Self::new_with_audio(
FPS_DEFAULT,
QUEUE_DEPTH_DEFAULT,
SckPixelFormat::Nv12VideoRange,
SckColorSpace::SrgbBt709,
false,
AUDIO_SAMPLE_RATE_DEFAULT_HZ,
AUDIO_CHANNEL_COUNT_DEFAULT,
);
match cfg {
Ok(c) => c,
Err(_) => unreachable!("default SckCaptureConfig must validate"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct SckCaptureConfigBuilder {
target_fps: u32,
queue_depth: u32,
pixel_format: SckPixelFormat,
color_space: SckColorSpace,
captures_audio: bool,
audio_sample_rate_hz: u32,
audio_channels: u32,
}
impl Default for SckCaptureConfigBuilder {
fn default() -> Self {
Self {
target_fps: FPS_DEFAULT,
queue_depth: QUEUE_DEPTH_DEFAULT,
pixel_format: SckPixelFormat::Nv12VideoRange,
color_space: SckColorSpace::SrgbBt709,
captures_audio: false,
audio_sample_rate_hz: AUDIO_SAMPLE_RATE_DEFAULT_HZ,
audio_channels: AUDIO_CHANNEL_COUNT_DEFAULT,
}
}
}
impl SckCaptureConfigBuilder {
pub fn target_fps(mut self, target_fps: u32) -> Self {
self.target_fps = target_fps;
self
}
pub fn queue_depth(mut self, queue_depth: u32) -> Self {
self.queue_depth = queue_depth;
self
}
pub fn pixel_format(mut self, pixel_format: SckPixelFormat) -> Self {
self.pixel_format = pixel_format;
self
}
pub fn color_space(mut self, color_space: SckColorSpace) -> Self {
self.color_space = color_space;
self
}
pub fn captures_audio(mut self, captures_audio: bool) -> Self {
self.captures_audio = captures_audio;
self
}
pub fn audio_sample_rate_hz(mut self, audio_sample_rate_hz: u32) -> Self {
self.audio_sample_rate_hz = audio_sample_rate_hz;
self
}
pub fn audio_channels(mut self, audio_channels: u32) -> Self {
self.audio_channels = audio_channels;
self
}
pub fn build(self) -> Result<SckCaptureConfig, SckError> {
SckCaptureConfig::new_with_audio(
self.target_fps,
self.queue_depth,
self.pixel_format,
self.color_space,
self.captures_audio,
self.audio_sample_rate_hz,
self.audio_channels,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SckCaptureFailure {
StreamStoppedWithError(String),
StreamStartFailed(String),
SystemDeniedAccess,
DisplayDisconnected,
Unknown(String),
}
impl SckCaptureFailure {
pub fn reason(&self) -> &str {
match self {
SckCaptureFailure::StreamStoppedWithError(m) => m.as_str(),
SckCaptureFailure::StreamStartFailed(m) => m.as_str(),
SckCaptureFailure::SystemDeniedAccess => "screen recording permission denied",
SckCaptureFailure::DisplayDisconnected => "captured display was disconnected",
SckCaptureFailure::Unknown(m) => m.as_str(),
}
}
}
pub trait CaptureFailureSurface: Send + Sync {
fn on_failure(&self, reason: SckCaptureFailure);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u32)]
pub enum AudioSampleFormat {
F32Planar = 0,
F32Interleaved = 1,
I16Interleaved = 2,
Unknown = 3,
}
pub const AUDIO_SAMPLE_FORMAT_CODE_MAX: u32 = 3;
impl AudioSampleFormat {
pub fn code(self) -> u32 {
let code = match self {
AudioSampleFormat::F32Planar => 0,
AudioSampleFormat::F32Interleaved => 1,
AudioSampleFormat::I16Interleaved => 2,
AudioSampleFormat::Unknown => 3,
};
assert!(code <= AUDIO_SAMPLE_FORMAT_CODE_MAX);
assert_eq!(code, self as u32);
code
}
pub fn as_str(self) -> &'static str {
let s = match self {
AudioSampleFormat::F32Planar => "f32_planar",
AudioSampleFormat::F32Interleaved => "f32_interleaved",
AudioSampleFormat::I16Interleaved => "i16_interleaved",
AudioSampleFormat::Unknown => "unknown",
};
assert!(!s.is_empty());
s
}
pub fn bytes_per_sample(self) -> u32 {
let bytes = match self {
AudioSampleFormat::F32Planar => 4,
AudioSampleFormat::F32Interleaved => 4,
AudioSampleFormat::I16Interleaved => 2,
AudioSampleFormat::Unknown => 0,
};
assert!(bytes <= 4);
bytes
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MacScreenShareAudioFrame {
pub sample_rate_hz: u32,
pub channels: u32,
pub num_samples_per_channel: u32,
pub pts_us: i64,
}
impl MacScreenShareAudioFrame {
pub fn new(
sample_rate_hz: u32,
channels: u32,
num_samples_per_channel: u32,
pts_us: i64,
) -> Result<Self, SckError> {
if !(AUDIO_SAMPLE_RATE_MIN_HZ..=AUDIO_SAMPLE_RATE_MAX_HZ).contains(&sample_rate_hz) {
return Err(SckError::InvalidAudioSampleRate(sample_rate_hz));
}
if !(AUDIO_CHANNEL_COUNT_MIN..=AUDIO_CHANNEL_COUNT_MAX).contains(&channels) {
return Err(SckError::InvalidAudioChannelCount(channels));
}
assert!(sample_rate_hz >= AUDIO_SAMPLE_RATE_MIN_HZ);
assert!(channels >= AUDIO_CHANNEL_COUNT_MIN);
Ok(Self {
sample_rate_hz,
channels,
num_samples_per_channel,
pts_us,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MacScreenShareAudioFrameWithBytes {
pub sample_rate_hz: u32,
pub channels: u32,
pub num_samples_per_channel: u32,
pub pts_us: i64,
pub format: AudioSampleFormat,
pub samples: Vec<u8>,
}
impl MacScreenShareAudioFrameWithBytes {
pub fn new(
sample_rate_hz: u32,
channels: u32,
num_samples_per_channel: u32,
pts_us: i64,
format: AudioSampleFormat,
samples: Vec<u8>,
) -> Result<Self, SckError> {
if !(AUDIO_SAMPLE_RATE_MIN_HZ..=AUDIO_SAMPLE_RATE_MAX_HZ).contains(&sample_rate_hz) {
return Err(SckError::InvalidAudioSampleRate(sample_rate_hz));
}
if !(AUDIO_CHANNEL_COUNT_MIN..=AUDIO_CHANNEL_COUNT_MAX).contains(&channels) {
return Err(SckError::InvalidAudioChannelCount(channels));
}
assert!(sample_rate_hz >= AUDIO_SAMPLE_RATE_MIN_HZ);
assert!(channels >= AUDIO_CHANNEL_COUNT_MIN);
Ok(Self {
sample_rate_hz,
channels,
num_samples_per_channel,
pts_us,
format,
samples,
})
}
}
@@ -1,846 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration;
use parking_lot::{Condvar, Mutex};
pub const CAPTURE_PTS_MAP_CAP: usize = RING_SIZE;
pub const EXTERNAL_SURFACE_QUEUE_CAP: usize = RING_SIZE;
pub const READY_WAIT_TIMEOUT_US_MAX: u64 = 16_667;
use fluxer_encoder_ring::{
EncoderFrameRate, EncoderInputRing, EncoderReady, IoSurfaceSlotHandle,
MetalSharedTextureBackend, RING_SIZE, RingError, TextureFormat,
};
#[cfg(target_os = "macos")]
use fluxer_encoder_ring::FillReservation;
#[cfg(target_os = "macos")]
use fluxer_encoder_ring::VtPixelTransfer;
#[cfg(target_os = "macos")]
use crate::iosurface_pair::{IoSurfaceRaw, iosurface_decrement_use_count};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncoderAttachError {
AlreadyAttached,
InvalidDimensions { width: u32, height: u32 },
RingInitFailed,
NotAttached,
}
impl std::fmt::Display for EncoderAttachError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::AlreadyAttached => write!(f, "encoder ring already attached"),
Self::InvalidDimensions { width, height } => {
write!(f, "encoder ring invalid dimensions {width}x{height}")
}
Self::RingInitFailed => write!(f, "encoder ring initialise failed"),
Self::NotAttached => write!(f, "encoder ring not attached"),
}
}
}
impl std::error::Error for EncoderAttachError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct EncoderAttachStats {
pub frames_submitted: u64,
pub frames_dropped: u64,
pub ring_full_events: u64,
pub failed_blits: u64,
}
#[cfg(target_os = "macos")]
pub struct ExternalSurfaceFrame {
surface: IoSurfaceRaw,
sequence: u64,
capture_pts_us: i64,
}
#[cfg(target_os = "macos")]
unsafe impl Send for ExternalSurfaceFrame {}
#[cfg(target_os = "macos")]
impl ExternalSurfaceFrame {
pub fn surface(&self) -> IoSurfaceRaw {
assert!(self.sequence > 0, "external frame sequence starts at one");
assert!(self.surface.as_ptr() as usize != 0, "surface ptr non-null");
self.surface
}
pub fn sequence(&self) -> u64 {
assert!(self.sequence > 0, "external frame sequence starts at one");
assert!(self.surface.as_ptr() as usize != 0, "surface ptr non-null");
self.sequence
}
pub fn capture_pts_us(&self) -> i64 {
assert!(self.sequence > 0, "external frame sequence starts at one");
assert!(self.surface.as_ptr() as usize != 0, "surface ptr non-null");
self.capture_pts_us
}
}
#[cfg(target_os = "macos")]
impl Drop for ExternalSurfaceFrame {
fn drop(&mut self) {
unsafe { iosurface_decrement_use_count(self.surface) };
unsafe { CFRelease(self.surface.as_ptr()) };
}
}
#[cfg(target_os = "macos")]
struct BlitDestination {
pixel_buffer: *mut core::ffi::c_void,
}
#[cfg(target_os = "macos")]
unsafe impl Send for BlitDestination {}
#[cfg(target_os = "macos")]
unsafe impl Sync for BlitDestination {}
#[cfg(target_os = "macos")]
impl Drop for BlitDestination {
fn drop(&mut self) {
unsafe { release_pixel_buffer(self.pixel_buffer) };
}
}
pub struct EncoderAttachment {
ring: Mutex<EncoderInputRing<MetalSharedTextureBackend>>,
ready_condvar: Condvar,
attached: AtomicBool,
frames_submitted: AtomicU64,
frames_dropped: AtomicU64,
ring_full_events: AtomicU64,
failed_blits: AtomicU64,
width: u32,
height: u32,
frame_rate: EncoderFrameRate,
capture_pts_by_sequence: Mutex<VecDeque<(u64, i64)>>,
#[cfg(target_os = "macos")]
pixel_transfer: Mutex<Option<VtPixelTransfer>>,
#[cfg(target_os = "macos")]
blit_destinations: Vec<BlitDestination>,
#[cfg(target_os = "macos")]
external_surfaces: Mutex<VecDeque<ExternalSurfaceFrame>>,
#[cfg(target_os = "macos")]
external_sequence: AtomicU64,
}
impl EncoderAttachment {
pub fn try_new(width: u32, height: u32) -> Result<Arc<Self>, EncoderAttachError> {
Self::try_new_with_frame_rate(width, height, EncoderFrameRate::default())
}
pub fn try_new_with_frame_rate(
width: u32,
height: u32,
frame_rate: EncoderFrameRate,
) -> Result<Arc<Self>, EncoderAttachError> {
if width == 0 || height == 0 {
return Err(EncoderAttachError::InvalidDimensions { width, height });
}
assert!(frame_rate.numerator > 0, "frame rate numerator positive");
assert!(
frame_rate.denominator > 0,
"frame rate denominator positive"
);
let mut ring = EncoderInputRing::new(MetalSharedTextureBackend::new());
ring.initialise(width, height, TextureFormat::Nv12)
.map_err(|_| EncoderAttachError::RingInitFailed)?;
#[cfg(target_os = "macos")]
let blit_destinations = build_blit_destinations(&mut ring, width, height)?;
#[cfg(target_os = "macos")]
let pixel_transfer = VtPixelTransfer::new().ok();
let attachment = Self {
ring: Mutex::new(ring),
ready_condvar: Condvar::new(),
attached: AtomicBool::new(true),
frames_submitted: AtomicU64::new(0),
frames_dropped: AtomicU64::new(0),
ring_full_events: AtomicU64::new(0),
failed_blits: AtomicU64::new(0),
width,
height,
frame_rate,
capture_pts_by_sequence: Mutex::new(VecDeque::with_capacity(CAPTURE_PTS_MAP_CAP)),
#[cfg(target_os = "macos")]
pixel_transfer: Mutex::new(pixel_transfer),
#[cfg(target_os = "macos")]
blit_destinations,
#[cfg(target_os = "macos")]
external_surfaces: Mutex::new(VecDeque::with_capacity(EXTERNAL_SURFACE_QUEUE_CAP)),
#[cfg(target_os = "macos")]
external_sequence: AtomicU64::new(0),
};
assert!(
attachment.attached.load(Ordering::Acquire),
"attachment is attached"
);
assert!(attachment.width > 0, "attachment width positive");
assert!(
attachment.frame_rate.numerator > 0,
"attachment fps positive"
);
Ok(Arc::new(attachment))
}
pub fn width(&self) -> u32 {
let w = self.width;
assert!(w > 0, "attachment width positive");
assert!(self.height > 0, "attachment height positive");
w
}
pub fn height(&self) -> u32 {
let h = self.height;
assert!(h > 0, "attachment height positive");
assert!(self.width > 0, "attachment width positive");
h
}
pub fn frame_rate(&self) -> EncoderFrameRate {
let rate = self.frame_rate;
assert!(rate.numerator > 0, "attachment fps numerator positive");
assert!(rate.denominator > 0, "attachment fps denominator positive");
rate
}
pub fn is_attached(&self) -> bool {
let a = self.attached.load(Ordering::Acquire);
assert!(self.width > 0, "width intact while reading attached");
assert!(self.height > 0, "height intact while reading attached");
a
}
pub fn detach(&self) {
self.attached.store(false, Ordering::Release);
#[cfg(target_os = "macos")]
{
let drained = core::mem::take(&mut *self.external_surfaces.lock());
assert!(
drained.len() <= EXTERNAL_SURFACE_QUEUE_CAP,
"external queue bounded at detach"
);
drop(drained);
}
self.ready_condvar.notify_all();
}
pub fn capacity(&self) -> usize {
let cap = RING_SIZE;
assert!(cap > 0, "ring capacity positive");
assert_eq!(cap, 8, "ring capacity matches RING_SIZE");
cap
}
pub fn submit_iosurface_frame(&self, _iosurface_handle: u64) -> Result<(), EncoderAttachError> {
if !self.attached.load(Ordering::Acquire) {
return Err(EncoderAttachError::NotAttached);
}
let mut ring = self.ring.lock();
let result: Result<(), RingError> = ring.submit_skip_oldest(|_handle| {});
drop(ring);
match result {
Ok(()) => {
self.frames_submitted.fetch_add(1, Ordering::Relaxed);
self.ready_condvar.notify_one();
Ok(())
}
Err(RingError::FullDropped { .. }) => {
self.frames_dropped.fetch_add(1, Ordering::Relaxed);
self.ring_full_events.fetch_add(1, Ordering::Relaxed);
Ok(())
}
Err(_) => Err(EncoderAttachError::RingInitFailed),
}
}
#[cfg(target_os = "macos")]
pub fn submit_with_blit(
&self,
source_pixel_buffer: *mut core::ffi::c_void,
capture_pts_us: i64,
) -> Result<(), EncoderAttachError> {
assert!(
!source_pixel_buffer.is_null(),
"blit source must be non-null"
);
assert!(self.width > 0, "attachment width positive");
if !self.attached.load(Ordering::Acquire) {
return Err(EncoderAttachError::NotAttached);
}
let transfer_guard = self.pixel_transfer.lock();
let Some(transfer) = transfer_guard.as_ref() else {
self.failed_blits.fetch_add(1, Ordering::Relaxed);
return Err(EncoderAttachError::RingInitFailed);
};
let Some(reservation) = self.reserve_blit_slot()? else {
return Ok(());
};
let slot_index = reservation.slot_index() as usize;
assert!(
slot_index < self.blit_destinations.len(),
"slot index within cached destinations"
);
let dest_pb = self.blit_destinations[slot_index].pixel_buffer;
assert!(
!dest_pb.is_null(),
"cached destination pixel buffer non-null"
);
let blit_ok = unsafe { transfer.transfer(source_pixel_buffer, dest_pb) }.is_ok();
if blit_ok {
self.commit_blit_slot(reservation, capture_pts_us)
} else {
let cancelled = self.ring.lock().cancel(reservation);
assert!(cancelled.is_ok(), "cancel of filling reservation succeeds");
self.failed_blits.fetch_add(1, Ordering::Relaxed);
Err(EncoderAttachError::RingInitFailed)
}
}
#[cfg(target_os = "macos")]
fn reserve_blit_slot(
&self,
) -> Result<Option<FillReservation<IoSurfaceSlotHandle>>, EncoderAttachError> {
assert!(self.width > 0, "attachment width positive");
assert!(self.height > 0, "attachment height positive");
let mut ring = self.ring.lock();
match ring.reserve_skip_oldest() {
Ok(reservation) => {
assert!(
(reservation.slot_index() as usize) < RING_SIZE,
"reserved slot within ring"
);
Ok(Some(reservation))
}
Err(RingError::FullDropped { .. }) => {
self.frames_dropped.fetch_add(1, Ordering::Relaxed);
self.ring_full_events.fetch_add(1, Ordering::Relaxed);
Ok(None)
}
Err(_) => Err(EncoderAttachError::RingInitFailed),
}
}
#[cfg(target_os = "macos")]
fn commit_blit_slot(
&self,
reservation: FillReservation<IoSurfaceSlotHandle>,
capture_pts_us: i64,
) -> Result<(), EncoderAttachError> {
assert!(
(reservation.slot_index() as usize) < RING_SIZE,
"slot within ring"
);
let sequence = {
let mut ring = self.ring.lock();
let sequence = match ring.commit(reservation) {
Ok(sequence) => sequence,
Err(_) => return Err(EncoderAttachError::RingInitFailed),
};
self.record_capture_pts(sequence, capture_pts_us);
sequence
};
assert!(sequence > 0, "committed sequence positive");
self.frames_submitted.fetch_add(1, Ordering::Relaxed);
self.ready_condvar.notify_one();
Ok(())
}
pub fn wait_next_ready(&self, timeout: Duration) -> Option<EncoderReady<IoSurfaceSlotHandle>> {
assert!(self.width > 0, "attachment width positive");
let timeout_bound = Duration::from_micros(READY_WAIT_TIMEOUT_US_MAX);
let bounded_timeout = timeout.min(timeout_bound);
assert!(
bounded_timeout <= timeout_bound,
"wait bounded to one frame interval"
);
let mut ring = self.ring.lock();
if let Some(ready) = ring.poll_next_ready() {
return Some(ready);
}
if !self.attached.load(Ordering::Acquire) {
return None;
}
let _ = self.ready_condvar.wait_for(&mut ring, bounded_timeout);
ring.poll_next_ready()
}
#[cfg(target_os = "macos")]
pub unsafe fn submit_external_surface(
&self,
surface: IoSurfaceRaw,
capture_pts_us: i64,
) -> Result<(), EncoderAttachError> {
assert!(surface.as_ptr() as usize != 0, "external surface non-null");
assert!(self.width > 0, "attachment width positive");
if !self.attached.load(Ordering::Acquire) {
unsafe { CFRelease(surface.as_ptr()) };
return Err(EncoderAttachError::NotAttached);
}
unsafe { crate::iosurface_pair::iosurface_increment_use_count(surface) };
let sequence = self.external_sequence.fetch_add(1, Ordering::AcqRel) + 1;
assert!(sequence > 0, "external sequence starts at one");
let frame = ExternalSurfaceFrame {
surface,
sequence,
capture_pts_us,
};
let evicted = {
let mut queue = self.external_surfaces.lock();
let evicted = if queue.len() >= EXTERNAL_SURFACE_QUEUE_CAP {
queue.pop_front()
} else {
None
};
queue.push_back(frame);
assert!(
queue.len() <= EXTERNAL_SURFACE_QUEUE_CAP,
"external queue bounded"
);
evicted
};
if let Some(oldest) = evicted {
drop(oldest);
self.frames_dropped.fetch_add(1, Ordering::Relaxed);
self.ring_full_events.fetch_add(1, Ordering::Relaxed);
}
self.frames_submitted.fetch_add(1, Ordering::Relaxed);
Ok(())
}
#[cfg(target_os = "macos")]
pub fn take_external_surface(&self) -> Option<ExternalSurfaceFrame> {
let frame = {
let mut queue = self.external_surfaces.lock();
assert!(
queue.len() <= EXTERNAL_SURFACE_QUEUE_CAP,
"external queue bounded"
);
queue.pop_front()
};
if let Some(ref taken) = frame {
assert!(taken.sequence > 0, "popped frame has real sequence");
}
frame
}
#[cfg(target_os = "macos")]
pub fn external_surface_queue_len(&self) -> usize {
let len = self.external_surfaces.lock().len();
assert!(len <= EXTERNAL_SURFACE_QUEUE_CAP, "external queue bounded");
assert!(self.width > 0, "attachment width intact");
len
}
fn record_capture_pts(&self, sequence: u64, capture_pts_us: i64) {
assert!(sequence > 0, "ring sequences start at one");
let mut map = self.capture_pts_by_sequence.lock();
while map.len() >= CAPTURE_PTS_MAP_CAP {
map.pop_front();
}
map.push_back((sequence, capture_pts_us));
assert!(map.len() <= CAPTURE_PTS_MAP_CAP, "pts map bounded");
}
pub fn capture_pts_us_for_sequence(&self, sequence: u64) -> Option<i64> {
let map = self.capture_pts_by_sequence.lock();
assert!(map.len() <= CAPTURE_PTS_MAP_CAP, "pts map bounded");
map.iter()
.find(|(seq, _)| *seq == sequence)
.map(|(_, pts)| *pts)
}
pub fn note_ring_full(&self) {
self.ring_full_events.fetch_add(1, Ordering::Relaxed);
}
pub fn stats(&self) -> EncoderAttachStats {
let stats = EncoderAttachStats {
frames_submitted: self.frames_submitted.load(Ordering::Relaxed),
frames_dropped: self.frames_dropped.load(Ordering::Relaxed),
ring_full_events: self.ring_full_events.load(Ordering::Relaxed),
failed_blits: self.failed_blits.load(Ordering::Relaxed),
};
assert!(
stats.frames_dropped <= stats.ring_full_events + stats.frames_dropped,
"drop counter consistent"
);
assert!(
stats.frames_submitted <= u64::MAX / 2,
"submitted within plausible bound"
);
stats
}
}
#[cfg(target_os = "macos")]
fn build_blit_destinations(
ring: &mut EncoderInputRing<MetalSharedTextureBackend>,
width: u32,
height: u32,
) -> Result<Vec<BlitDestination>, EncoderAttachError> {
assert!(width > 0, "blit destination width positive");
assert!(height > 0, "blit destination height positive");
let mut destinations: Vec<BlitDestination> = Vec::with_capacity(RING_SIZE);
for slot_index in 0..RING_SIZE {
let surface_ptr = ring
.backend_mut()
.slot_iosurface_ptr(slot_index as u32)
.ok_or(EncoderAttachError::RingInitFailed)?;
assert!(!surface_ptr.is_null(), "slot iosurface ptr non-null");
let pixel_buffer = unsafe { VtPixelTransfer::wrap_iosurface(surface_ptr, width, height) }
.map_err(|_| EncoderAttachError::RingInitFailed)?;
destinations.push(BlitDestination { pixel_buffer });
}
assert_eq!(destinations.len(), RING_SIZE, "one destination per slot");
Ok(destinations)
}
#[cfg(target_os = "macos")]
#[link(name = "CoreVideo", kind = "framework")]
unsafe extern "C" {
fn CVPixelBufferRelease(buffer: *mut core::ffi::c_void);
}
#[cfg(target_os = "macos")]
#[link(name = "CoreFoundation", kind = "framework")]
unsafe extern "C" {
fn CFRelease(cf: *const core::ffi::c_void);
fn CFRetain(cf: *const core::ffi::c_void) -> *const core::ffi::c_void;
}
#[cfg(target_os = "macos")]
unsafe fn release_pixel_buffer(pb: *mut core::ffi::c_void) {
if !pb.is_null() {
unsafe { CVPixelBufferRelease(pb) };
}
}
impl Drop for EncoderAttachment {
fn drop(&mut self) {
self.detach();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_zero_dimensions() {
let err = EncoderAttachment::try_new(0, 720).err();
assert!(matches!(
err,
Some(EncoderAttachError::InvalidDimensions { .. })
));
}
#[cfg(target_os = "macos")]
#[test]
fn macos_attach_then_detach() {
let attach = EncoderAttachment::try_new(640, 480).expect("attach ok");
assert!(attach.is_attached());
assert_eq!(attach.width(), 640);
assert_eq!(attach.height(), 480);
assert_eq!(attach.capacity(), 8);
attach.detach();
assert!(!attach.is_attached());
}
#[cfg(target_os = "macos")]
#[test]
fn macos_submit_records_stats() {
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
for _ in 0..3 {
attach
.submit_iosurface_frame(0xdead_beef)
.expect("submit ok");
}
let stats = attach.stats();
assert_eq!(stats.frames_submitted, 3);
assert_eq!(stats.frames_dropped, 0);
assert_eq!(stats.ring_full_events, 0);
}
#[cfg(target_os = "macos")]
#[test]
fn wait_next_ready_returns_submitted_frame() {
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
attach
.submit_iosurface_frame(0xdead_beef)
.expect("submit ok");
let ready = attach
.wait_next_ready(Duration::from_millis(5))
.expect("frame ready");
assert_eq!(ready.sequence, 1);
assert_eq!(ready.duplicate_count, 0);
assert!(attach.wait_next_ready(Duration::from_millis(1)).is_none());
}
#[cfg(target_os = "macos")]
#[test]
fn wait_next_ready_timeout_is_capped_at_frame_interval() {
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
let start = std::time::Instant::now();
let ready = attach.wait_next_ready(Duration::from_secs(60));
assert!(ready.is_none());
assert!(
start.elapsed() < Duration::from_millis(500),
"wait returned within the named frame-interval bound"
);
}
#[cfg(target_os = "macos")]
#[test]
fn wait_next_ready_wakes_for_concurrent_submit() {
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
let waiter = {
let attach = Arc::clone(&attach);
std::thread::spawn(move || attach.wait_next_ready(Duration::from_millis(15)))
};
std::thread::sleep(Duration::from_millis(3));
attach
.submit_iosurface_frame(0xdead_beef)
.expect("submit ok");
let ready = waiter.join().expect("waiter joins");
let sequence = ready.map(|r| r.sequence);
assert!(sequence == Some(1) || sequence.is_none());
if sequence.is_none() {
let retry = attach.wait_next_ready(Duration::from_millis(5));
assert_eq!(retry.map(|r| r.sequence), Some(1));
}
}
#[cfg(target_os = "macos")]
#[test]
fn wait_next_ready_returns_none_after_detach() {
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
attach.detach();
assert!(!attach.is_attached());
assert!(attach.wait_next_ready(Duration::from_millis(5)).is_none());
}
#[cfg(target_os = "macos")]
#[test]
fn macos_submit_rejected_when_detached() {
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
attach.detach();
let err = attach.submit_iosurface_frame(0xdead_beef).err();
assert!(matches!(err, Some(EncoderAttachError::NotAttached)));
}
#[cfg(target_os = "macos")]
#[test]
fn submit_encoder_ring_frame_blits_iosurface_to_slot() {
use core::ptr::NonNull;
use objc2_core_foundation::{CFDictionary, CFRetained};
use objc2_core_video::{
CVPixelBuffer, CVPixelBufferGetBaseAddressOfPlane, CVPixelBufferGetBytesPerRowOfPlane,
CVPixelBufferGetHeightOfPlane, CVPixelBufferLockBaseAddress, CVPixelBufferLockFlags,
CVPixelBufferUnlockBaseAddress, kCVPixelBufferIOSurfacePropertiesKey,
};
let attach = EncoderAttachment::try_new(256, 256).expect("attach ok");
let mut empty_keys: [*const core::ffi::c_void; 0] = [];
let mut empty_vals: [*const core::ffi::c_void; 0] = [];
let iosurf_dict: CFRetained<CFDictionary> = unsafe {
CFDictionary::new(
None,
empty_keys.as_mut_ptr(),
empty_vals.as_mut_ptr(),
0,
&objc2_core_foundation::kCFTypeDictionaryKeyCallBacks,
&objc2_core_foundation::kCFTypeDictionaryValueCallBacks,
)
.expect("iosurf empty dict")
};
let key_ref: &objc2_core_foundation::CFString =
unsafe { kCVPixelBufferIOSurfacePropertiesKey };
let key_ptr: *const core::ffi::c_void = key_ref as *const _ as *const core::ffi::c_void;
let val_ptr: *const core::ffi::c_void =
&*iosurf_dict as *const _ as *const core::ffi::c_void;
let mut keys = [key_ptr];
let mut vals = [val_ptr];
let attrs: CFRetained<CFDictionary> = unsafe {
CFDictionary::new(
None,
keys.as_mut_ptr(),
vals.as_mut_ptr(),
1,
&objc2_core_foundation::kCFTypeDictionaryKeyCallBacks,
&objc2_core_foundation::kCFTypeDictionaryValueCallBacks,
)
.expect("attrs dict")
};
let nv12: u32 = u32::from_be_bytes(*b"420v");
let mut pb_out: *mut CVPixelBuffer = core::ptr::null_mut();
let status = unsafe {
objc2_core_video::CVPixelBufferCreate(
None,
256,
256,
nv12,
Some(&attrs),
NonNull::new(&mut pb_out).expect("pb_out non-null"),
)
};
assert_eq!(status, 0, "CVPixelBufferCreate ok");
let source_pb: CFRetained<CVPixelBuffer> =
unsafe { CFRetained::from_raw(NonNull::new(pb_out).expect("pb non-null")) };
let lock_flags = CVPixelBufferLockFlags(0);
let lock_st = unsafe { CVPixelBufferLockBaseAddress(&source_pb, lock_flags) };
assert_eq!(lock_st, 0, "lock ok");
let y_ptr = CVPixelBufferGetBaseAddressOfPlane(&source_pb, 0);
let y_stride = CVPixelBufferGetBytesPerRowOfPlane(&source_pb, 0);
let y_h = CVPixelBufferGetHeightOfPlane(&source_pb, 0);
assert!(!y_ptr.is_null(), "Y plane base non-null");
assert!(y_stride > 0, "Y stride positive");
for row in 0..y_h {
for col in 0..y_stride {
unsafe {
(y_ptr as *mut u8).add(row * y_stride + col).write(0x55);
}
}
}
let _ = unsafe { CVPixelBufferUnlockBaseAddress(&source_pb, lock_flags) };
let src_ptr = &*source_pb as *const CVPixelBuffer as *mut core::ffi::c_void;
attach
.submit_with_blit(src_ptr, 41_500)
.expect("submit_with_blit ok");
let stats = attach.stats();
assert_eq!(stats.frames_submitted, 1, "one frame submitted");
assert_eq!(stats.failed_blits, 0, "no failed blits");
assert_eq!(
attach.capture_pts_us_for_sequence(1),
Some(41_500),
"capture pts recorded for ring sequence"
);
assert_eq!(attach.capture_pts_us_for_sequence(2), None);
}
#[cfg(target_os = "macos")]
#[test]
fn external_surface_submit_take_round_trip() {
use core::ptr::NonNull;
use fluxer_encoder_ring::metal_iosurface_macos::OwnedIoSurface;
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
let owned = OwnedIoSurface::create_nv12(64, 64).expect("surface");
let raw = NonNull::new(owned.as_ptr()).expect("non-null surface");
let use_before = unsafe { crate::iosurface_pair::iosurface_use_count(raw) };
unsafe { CFRetain(raw.as_ptr()) };
unsafe { attach.submit_external_surface(raw, 41_500) }.expect("submit ok");
assert_eq!(attach.external_surface_queue_len(), 1);
let use_during = unsafe { crate::iosurface_pair::iosurface_use_count(raw) };
assert_eq!(use_during, use_before + 1);
let stats = attach.stats();
assert_eq!(stats.frames_submitted, 1);
assert_eq!(stats.frames_dropped, 0);
assert_eq!(stats.ring_full_events, 0);
let frame = attach.take_external_surface().expect("frame queued");
assert_eq!(frame.surface(), raw);
assert_eq!(frame.sequence(), 1);
assert_eq!(frame.capture_pts_us(), 41_500);
drop(frame);
let use_after = unsafe { crate::iosurface_pair::iosurface_use_count(raw) };
assert_eq!(use_after, use_before);
assert_eq!(attach.external_surface_queue_len(), 0);
}
#[cfg(target_os = "macos")]
#[test]
fn external_surface_queue_evicts_oldest_when_full() {
use core::ptr::NonNull;
use fluxer_encoder_ring::metal_iosurface_macos::OwnedIoSurface;
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
let total = EXTERNAL_SURFACE_QUEUE_CAP + 2;
let mut owned: Vec<OwnedIoSurface> = Vec::with_capacity(total);
for i in 0..total {
let surface = OwnedIoSurface::create_nv12(64, 64).expect("surface");
let raw = NonNull::new(surface.as_ptr()).expect("non-null surface");
unsafe { CFRetain(raw.as_ptr()) };
unsafe { attach.submit_external_surface(raw, (i as i64) * 1_000) }.expect("submit ok");
owned.push(surface);
}
assert_eq!(
attach.external_surface_queue_len(),
EXTERNAL_SURFACE_QUEUE_CAP
);
let stats = attach.stats();
assert_eq!(stats.frames_submitted, total as u64);
assert_eq!(stats.frames_dropped, 2);
assert_eq!(stats.ring_full_events, 2);
let oldest_remaining = attach.take_external_surface().expect("frame queued");
assert_eq!(oldest_remaining.sequence(), 3);
assert_eq!(oldest_remaining.capture_pts_us(), 2_000);
}
#[cfg(target_os = "macos")]
#[test]
fn detach_drains_external_surface_queue_and_rejects_submit() {
use core::ptr::NonNull;
use fluxer_encoder_ring::metal_iosurface_macos::OwnedIoSurface;
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
let owned = OwnedIoSurface::create_nv12(64, 64).expect("surface");
let raw = NonNull::new(owned.as_ptr()).expect("non-null surface");
let use_before = unsafe { crate::iosurface_pair::iosurface_use_count(raw) };
unsafe { CFRetain(raw.as_ptr()) };
unsafe { attach.submit_external_surface(raw, 7) }.expect("submit ok");
assert_eq!(attach.external_surface_queue_len(), 1);
attach.detach();
assert_eq!(attach.external_surface_queue_len(), 0);
assert_eq!(
unsafe { crate::iosurface_pair::iosurface_use_count(raw) },
use_before
);
unsafe { CFRetain(raw.as_ptr()) };
let err = unsafe { attach.submit_external_surface(raw, 8) }.err();
assert!(matches!(err, Some(EncoderAttachError::NotAttached)));
assert_eq!(attach.external_surface_queue_len(), 0);
assert_eq!(
unsafe { crate::iosurface_pair::iosurface_use_count(raw) },
use_before
);
}
#[cfg(target_os = "macos")]
#[test]
fn capture_pts_map_stays_bounded_and_evicts_oldest() {
let attach = EncoderAttachment::try_new(64, 64).expect("attach ok");
let total = (CAPTURE_PTS_MAP_CAP as u64) + 4;
for sequence in 1..=total {
attach.record_capture_pts(sequence, (sequence as i64) * 1_000);
}
assert_eq!(
attach.capture_pts_us_for_sequence(1),
None,
"oldest evicted"
);
assert_eq!(
attach.capture_pts_us_for_sequence(4),
None,
"oldest evicted"
);
assert_eq!(
attach.capture_pts_us_for_sequence(5),
Some(5_000),
"newest retained"
);
assert_eq!(
attach.capture_pts_us_for_sequence(total),
Some((total as i64) * 1_000)
);
}
}
@@ -1,12 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use objc2_foundation::{NSError, NSProcessInfo};
pub fn operating_system_version_string() -> String {
let info = NSProcessInfo::processInfo();
info.operatingSystemVersionString().to_string()
}
pub fn ns_error_localized_description(err: &NSError) -> String {
err.localizedDescription().to_string()
}
@@ -1,331 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use core::ffi::c_void;
use core::ptr::NonNull;
pub type IoSurfaceRaw = NonNull<c_void>;
#[cfg(target_os = "macos")]
#[link(name = "IOSurface", kind = "framework")]
unsafe extern "C" {
fn IOSurfaceIncrementUseCount(buffer: *mut c_void);
fn IOSurfaceDecrementUseCount(buffer: *mut c_void);
fn IOSurfaceGetUseCount(buffer: *mut c_void) -> i32;
}
#[cfg(target_os = "macos")]
pub unsafe fn iosurface_increment_use_count(surface: IoSurfaceRaw) {
unsafe { IOSurfaceIncrementUseCount(surface.as_ptr()) };
}
#[cfg(target_os = "macos")]
pub unsafe fn iosurface_decrement_use_count(surface: IoSurfaceRaw) {
unsafe { IOSurfaceDecrementUseCount(surface.as_ptr()) };
}
#[cfg(target_os = "macos")]
pub unsafe fn iosurface_use_count(surface: IoSurfaceRaw) -> i32 {
unsafe { IOSurfaceGetUseCount(surface.as_ptr()) }
}
#[cfg(not(target_os = "macos"))]
pub unsafe fn iosurface_increment_use_count(_surface: IoSurfaceRaw) {}
#[cfg(not(target_os = "macos"))]
pub unsafe fn iosurface_decrement_use_count(_surface: IoSurfaceRaw) {}
#[cfg(not(target_os = "macos"))]
pub unsafe fn iosurface_use_count(_surface: IoSurfaceRaw) -> i32 {
0
}
pub struct IoSurfacePair {
current: Option<IoSurfaceRaw>,
prev: Option<IoSurfaceRaw>,
}
unsafe impl Send for IoSurfacePair {}
impl IoSurfacePair {
pub fn new() -> Self {
let pair = Self {
current: None,
prev: None,
};
assert!(pair.current.is_none());
assert!(pair.prev.is_none());
pair
}
pub fn has_current(&self) -> bool {
let has = self.current.is_some();
assert!(has == self.current.is_some());
has
}
pub fn has_prev(&self) -> bool {
let has = self.prev.is_some();
assert!(has == self.prev.is_some());
has
}
pub unsafe fn push(&mut self, new: IoSurfaceRaw) {
unsafe { iosurface_increment_use_count(new) };
let evicted = self.prev.take();
let rotated = self.current.take();
self.prev = rotated;
self.current = Some(new);
assert!(self.current.is_some());
if let Some(old) = evicted {
unsafe { iosurface_decrement_use_count(old) };
}
}
pub fn take_current(&mut self) -> Option<IoSurfaceRaw> {
let taken = self.current.take();
assert!(self.current.is_none());
taken
}
pub fn peek_current(&self) -> Option<IoSurfaceRaw> {
self.current
}
pub fn peek_prev(&self) -> Option<IoSurfaceRaw> {
self.prev
}
pub fn clear(&mut self) {
let cur = self.current.take();
let prev = self.prev.take();
assert!(self.current.is_none());
assert!(self.prev.is_none());
if let Some(s) = cur {
unsafe { iosurface_decrement_use_count(s) };
}
if let Some(s) = prev {
unsafe { iosurface_decrement_use_count(s) };
}
}
}
impl Default for IoSurfacePair {
fn default() -> Self {
Self::new()
}
}
impl Drop for IoSurfacePair {
fn drop(&mut self) {
let cur = self.current.take();
let prev = self.prev.take();
if let Some(s) = cur {
unsafe { iosurface_decrement_use_count(s) };
}
if let Some(s) = prev {
unsafe { iosurface_decrement_use_count(s) };
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::ptr::NonNull;
fn fake_surface(addr: usize) -> IoSurfaceRaw {
assert!(addr != 0);
NonNull::new(addr as *mut c_void).expect("non-null fake surface")
}
#[test]
fn new_pair_is_empty() {
let pair = IoSurfacePair::new();
assert!(!pair.has_current());
assert!(!pair.has_prev());
}
#[test]
fn take_current_on_empty_returns_none() {
let mut pair = IoSurfacePair::new();
assert!(pair.take_current().is_none());
}
#[cfg(not(target_os = "macos"))]
#[test]
fn push_rotates_current_to_prev_offplatform() {
let mut pair = IoSurfacePair::new();
let a = fake_surface(0x1);
let b = fake_surface(0x2);
unsafe {
pair.push(a);
}
assert_eq!(pair.peek_current(), Some(a));
assert!(pair.peek_prev().is_none());
unsafe {
pair.push(b);
}
assert_eq!(pair.peek_current(), Some(b));
assert_eq!(pair.peek_prev(), Some(a));
}
#[cfg(not(target_os = "macos"))]
#[test]
fn take_current_clears_slot_offplatform() {
let mut pair = IoSurfacePair::new();
let a = fake_surface(0x10);
unsafe {
pair.push(a);
}
let taken = pair.take_current().expect("current is set");
assert_eq!(taken, a);
assert!(pair.peek_current().is_none());
}
#[cfg(not(target_os = "macos"))]
#[test]
fn drop_pair_is_safe_offplatform() {
let mut pair = IoSurfacePair::new();
unsafe {
pair.push(fake_surface(0x100));
pair.push(fake_surface(0x200));
}
drop(pair);
}
}
#[cfg(all(test, target_os = "macos"))]
mod macos_tests {
use super::*;
use core::ffi::c_void;
use core::ptr::NonNull;
use objc2_core_foundation::{
CFDictionary, CFNumber, CFRetained, CFString, kCFAllocatorDefault,
kCFTypeDictionaryKeyCallBacks, kCFTypeDictionaryValueCallBacks,
};
#[link(name = "IOSurface", kind = "framework")]
unsafe extern "C" {
fn IOSurfaceCreate(properties: *const CFDictionary) -> *mut c_void;
}
fn cf_number_i32(v: i32) -> CFRetained<CFNumber> {
CFNumber::new_i32(v)
}
fn cf_string(s: &'static str) -> CFRetained<CFString> {
CFString::from_static_str(s)
}
fn make_iosurface() -> IoSurfaceRaw {
let width_key = cf_string("IOSurfaceWidth");
let height_key = cf_string("IOSurfaceHeight");
let bpe_key = cf_string("IOSurfaceBytesPerElement");
let pf_key = cf_string("IOSurfacePixelFormat");
let width_val = cf_number_i32(32);
let height_val = cf_number_i32(32);
let bpe_val = cf_number_i32(4);
let pf_val = cf_number_i32(i32::from_be_bytes(*b"BGRA"));
let keys: [*const c_void; 4] = [
CFRetained::as_ptr(&width_key).as_ptr() as *const c_void,
CFRetained::as_ptr(&height_key).as_ptr() as *const c_void,
CFRetained::as_ptr(&bpe_key).as_ptr() as *const c_void,
CFRetained::as_ptr(&pf_key).as_ptr() as *const c_void,
];
let vals: [*const c_void; 4] = [
CFRetained::as_ptr(&width_val).as_ptr() as *const c_void,
CFRetained::as_ptr(&height_val).as_ptr() as *const c_void,
CFRetained::as_ptr(&bpe_val).as_ptr() as *const c_void,
CFRetained::as_ptr(&pf_val).as_ptr() as *const c_void,
];
let dict_opt = unsafe {
CFDictionary::new(
kCFAllocatorDefault,
keys.as_ptr() as *mut *const c_void,
vals.as_ptr() as *mut *const c_void,
4,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks,
)
};
let dict = dict_opt.expect("CFDictionary::new returned non-null");
let dict_ptr = CFRetained::as_ptr(&dict).as_ptr() as *const CFDictionary;
let raw = unsafe { IOSurfaceCreate(dict_ptr) };
assert!(!raw.is_null(), "IOSurfaceCreate must produce a surface");
NonNull::new(raw).expect("non-null IOSurface")
}
#[test]
fn push_increments_use_count_and_take_current_does_not_release() {
let a = make_iosurface();
let before = unsafe { iosurface_use_count(a) };
let mut pair = IoSurfacePair::new();
unsafe {
pair.push(a);
}
let after_push = unsafe { iosurface_use_count(a) };
assert_eq!(after_push, before + 1);
let taken = pair.take_current().expect("current set");
assert_eq!(taken, a);
let after_take = unsafe { iosurface_use_count(a) };
assert_eq!(after_take, before + 1);
unsafe { iosurface_decrement_use_count(a) };
let after_balance = unsafe { iosurface_use_count(a) };
assert_eq!(after_balance, before);
}
#[test]
fn push_rotates_and_drop_releases_both_slots() {
let a = make_iosurface();
let b = make_iosurface();
let before_a = unsafe { iosurface_use_count(a) };
let before_b = unsafe { iosurface_use_count(b) };
let mut pair = IoSurfacePair::new();
unsafe {
pair.push(a);
pair.push(b);
}
assert_eq!(pair.peek_current(), Some(b));
assert_eq!(pair.peek_prev(), Some(a));
assert_eq!(unsafe { iosurface_use_count(a) }, before_a + 1);
assert_eq!(unsafe { iosurface_use_count(b) }, before_b + 1);
drop(pair);
assert_eq!(unsafe { iosurface_use_count(a) }, before_a);
assert_eq!(unsafe { iosurface_use_count(b) }, before_b);
}
#[test]
fn third_push_evicts_oldest_and_releases_it() {
let a = make_iosurface();
let b = make_iosurface();
let c = make_iosurface();
let before_a = unsafe { iosurface_use_count(a) };
let mut pair = IoSurfacePair::new();
unsafe {
pair.push(a);
pair.push(b);
pair.push(c);
}
assert_eq!(unsafe { iosurface_use_count(a) }, before_a);
assert_eq!(pair.peek_current(), Some(c));
assert_eq!(pair.peek_prev(), Some(b));
}
#[test]
fn clear_releases_both_slots() {
let a = make_iosurface();
let b = make_iosurface();
let before_a = unsafe { iosurface_use_count(a) };
let before_b = unsafe { iosurface_use_count(b) };
let mut pair = IoSurfacePair::new();
unsafe {
pair.push(a);
pair.push(b);
}
pair.clear();
assert_eq!(unsafe { iosurface_use_count(a) }, before_a);
assert_eq!(unsafe { iosurface_use_count(b) }, before_b);
assert!(pair.peek_current().is_none());
assert!(pair.peek_prev().is_none());
}
}
@@ -1,45 +1,10 @@
#![deny(clippy::all)]
// SPDX-License-Identifier: AGPL-3.0-or-later
#![allow(dead_code)]
#![allow(clippy::missing_safety_doc)]
#![allow(clippy::collapsible_if)]
#![allow(clippy::manual_is_multiple_of)]
#![allow(clippy::manual_slice_size_calculation)]
#![allow(clippy::unnecessary_cast)]
#![allow(clippy::not_unsafe_ptr_arg_deref)]
#![allow(clippy::missing_transmute_annotations)]
#![allow(clippy::missing_const_for_thread_local)]
#![allow(clippy::too_many_arguments)]
pub mod audio_pool;
pub mod config;
pub mod encoder_attach;
pub mod iosurface_pair;
pub mod os_version;
pub mod permission_probe;
pub use audio_pool::{
MAC_AUDIO_POOL_CAP, MAX_FRAME_BYTES_PER_SLOT, MacAudioError, MacAudioFramePool,
MacAudioPoolStats, PooledMacAudioFrame,
};
pub use config::{
AUDIO_CHANNEL_COUNT_DEFAULT, AUDIO_CHANNEL_COUNT_MAX, AUDIO_CHANNEL_COUNT_MIN,
AUDIO_SAMPLE_RATE_DEFAULT_HZ, AUDIO_SAMPLE_RATE_MAX_HZ, AUDIO_SAMPLE_RATE_MIN_HZ,
AudioSampleFormat, CaptureFailureSurface, FPS_DEFAULT, FPS_MAX, FPS_MIN,
MacScreenShareAudioFrame, MacScreenShareAudioFrameWithBytes, QUEUE_DEPTH_DEFAULT,
QUEUE_DEPTH_MAX, QUEUE_DEPTH_MIN, SckCaptureConfig, SckCaptureConfigBuilder, SckCaptureFailure,
SckColorSpace, SckError, SckPixelFormat,
};
pub use encoder_attach::{EncoderAttachError, EncoderAttachStats, EncoderAttachment};
pub use iosurface_pair::{IoSurfacePair, IoSurfaceRaw};
#[cfg(target_os = "macos")]
pub mod foundation;
#[cfg(target_os = "macos")]
pub mod sck;
#[cfg(target_os = "macos")]
#[cfg(all(target_os = "macos", not(test)))]
mod napi_surface_macos;
#[cfg(not(target_os = "macos"))]
#[cfg(all(not(target_os = "macos"), not(test)))]
mod napi_surface_stub;
File diff suppressed because it is too large. Load diff
@@ -1,87 +1,8 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
use napi::bindgen_prelude::{Error, Result, Status};
use napi_derive::napi;
fn unsupported() -> Error {
Error::new(
Status::GenericFailure,
"@fluxer/mac-screen-capture is only supported on macOS",
)
}
#[napi(object, js_name = "MacScreenCaptureSource")]
pub struct MacScreenCaptureSource {
pub kind: String,
pub id: String,
pub name: String,
pub width: u32,
pub height: u32,
pub app_name: Option<String>,
pub bundle_id: Option<String>,
pub target_pid: Option<i32>,
}
#[napi(js_name = "listSources")]
pub fn list_sources() -> Result<Vec<MacScreenCaptureSource>> {
Ok(Vec::new())
}
#[napi(object, js_name = "MacScreenCaptureBackendSckAvailability")]
pub struct SckAvailability {
pub supported: bool,
pub macos_version: Option<String>,
}
#[napi(object, js_name = "MacScreenCaptureBackendAvailability")]
pub struct BackendAvailability {
pub sck: SckAvailability,
pub screen_permission: String,
}
#[napi(js_name = "getBackendAvailability")]
pub fn get_backend_availability() -> Result<BackendAvailability> {
Err(unsupported())
}
#[napi(js_name = "probeScreenRecordingAccess")]
pub fn probe_screen_recording_access() -> String {
crate::permission_probe::PROBE_UNSUPPORTED.to_owned()
}
#[napi(object, js_name = "MacScreenCaptureBackendInfo")]
pub struct MacScreenCaptureBackendInfo {
pub backend: String,
pub supported: bool,
pub reason: String,
#[napi(js_name = "minMacosVersion")]
pub min_macos_version: String,
#[napi(js_name = "detectedMacosVersion")]
pub detected_macos_version: Option<String>,
#[napi(js_name = "sckAvailable")]
pub sck_available: bool,
}
#[napi(js_name = "getBackendInfo")]
pub fn get_backend_info() -> MacScreenCaptureBackendInfo {
use crate::os_version::{SCK_MIN_MACOS, format_version};
MacScreenCaptureBackendInfo {
backend: "mac-screen-capture".to_owned(),
supported: false,
reason: "@fluxer/mac-screen-capture is only supported on macOS".to_owned(),
min_macos_version: format_version(SCK_MIN_MACOS),
detected_macos_version: None,
sck_available: false,
}
}
#[napi]
pub struct ScreenCapture;
#[napi]
impl ScreenCapture {
#[napi(constructor)]
pub fn new() -> Result<Self> {
Err(unsupported())
}
}
@@ -1,79 +0,0 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
pub const SCK_MIN_MACOS: (i64, i64, i64) = (12, 3, 0);
pub fn meets_floor(version: (i64, i64, i64), floor: (i64, i64, i64)) -> bool {
if version.0 != floor.0 {
return version.0 > floor.0;
}
if version.1 != floor.1 {
return version.1 > floor.1;
}
version.2 >= floor.2
}
pub fn format_version(version: (i64, i64, i64)) -> String {
if version.2 == 0 {
format!("{}.{}", version.0, version.1)
} else {
format!("{}.{}.{}", version.0, version.1, version.2)
}
}
#[cfg(target_os = "macos")]
pub fn current_macos_version() -> Option<(i64, i64, i64)> {
use objc2_foundation::NSProcessInfo;
let info = NSProcessInfo::processInfo();
let v = info.operatingSystemVersion();
Some((
v.majorVersion as i64,
v.minorVersion as i64,
v.patchVersion as i64,
))
}
#[cfg(not(target_os = "macos"))]
pub fn current_macos_version() -> Option<(i64, i64, i64)> {
None
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SupportClassification {
pub supported: bool,
pub sck_available: bool,
pub reason: String,
}
pub fn classify_support(detected: Option<(i64, i64, i64)>) -> SupportClassification {
let min_sck = format_version(SCK_MIN_MACOS);
match detected {
None => SupportClassification {
supported: false,
sck_available: false,
reason: "mac-screen-capture could not detect the running macOS version. \
Native screen capture unavailable."
.to_owned(),
},
Some(v) => {
let detected_str = format_version(v);
let sck_ok = meets_floor(v, SCK_MIN_MACOS);
let reason = if sck_ok {
format!(
"mac-screen-capture supported on macOS {detected_str} \
(ScreenCaptureKit, requires macOS {min_sck}+)."
)
} else {
format!(
"mac-screen-capture requires macOS {min_sck}+ (ScreenCaptureKit). \
This Mac is running macOS {detected_str}. Native cursor-hidden \
screen capture unavailable; fall back to getDisplayMedia."
)
};
SupportClassification {
supported: sck_ok,
sck_available: sck_ok,
reason,
}
}
}
}
@@ -1,290 +0,0 @@
#![allow(non_snake_case)]
// SPDX-License-Identifier: AGPL-3.0-or-later
use objc2::rc::Retained;
use objc2_core_foundation::{CFString, CGFloat, CGRect};
use objc2_core_media::{CMTime, CMTimeFlags};
use objc2_foundation::NSString;
use objc2_screen_capture_kit::{
SCCaptureDynamicRange, SCContentFilter, SCDisplay, SCRunningApplication, SCStream,
SCStreamConfiguration, SCWindow,
};
use crate::config::{SckCaptureConfig, SckColorSpace, SckPixelFormat};
pub use objc2_screen_capture_kit::SCStreamOutputType;
pub fn cmtime_seconds(value: i64, timescale: i32) -> CMTime {
CMTime {
value,
timescale,
flags: CMTimeFlags(1),
epoch: 0,
}
}
pub fn sc_running_application_process_id(app: &SCRunningApplication) -> i32 {
unsafe { app.processID() }
}
pub fn sc_running_application_bundle_identifier(app: &SCRunningApplication) -> Retained<NSString> {
unsafe { app.bundleIdentifier() }
}
pub fn sc_running_application_name(app: &SCRunningApplication) -> Retained<NSString> {
unsafe { app.applicationName() }
}
pub fn sc_display_frame(display: &SCDisplay) -> CGRect {
unsafe { display.frame() }
}
pub fn sc_display_display_id(display: &SCDisplay) -> u32 {
unsafe { display.displayID() }
}
pub fn sc_display_width(display: &SCDisplay) -> isize {
unsafe { display.width() }
}
pub fn sc_display_height(display: &SCDisplay) -> isize {
unsafe { display.height() }
}
pub fn sc_window_window_id(win: &SCWindow) -> u32 {
unsafe { win.windowID() }
}
pub fn sc_window_owning_application(win: &SCWindow) -> Option<Retained<SCRunningApplication>> {
unsafe { win.owningApplication() }
}
pub fn sc_window_frame(win: &SCWindow) -> CGRect {
unsafe { win.frame() }
}
pub fn sc_window_title(win: &SCWindow) -> Option<Retained<NSString>> {
unsafe { win.title() }
}
pub fn sc_window_is_on_screen(win: &SCWindow) -> bool {
unsafe { win.isOnScreen() }
}
pub fn filter_content_rect_if_available(filter: &SCContentFilter) -> Option<CGRect> {
use objc2::runtime::NSObjectProtocol;
use objc2::{msg_send, sel};
let obj: &objc2::runtime::NSObject = filter.as_ref();
if !obj.respondsToSelector(sel!(contentRect)) {
return None;
}
let rect: CGRect = unsafe { msg_send![obj, contentRect] };
if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
return None;
}
Some(rect)
}
pub fn filter_point_pixel_scale_if_available(filter: &SCContentFilter) -> Option<f32> {
use objc2::runtime::NSObjectProtocol;
use objc2::{msg_send, sel};
let obj: &objc2::runtime::NSObject = filter.as_ref();
if !obj.respondsToSelector(sel!(pointPixelScale)) {
return None;
}
let scale: CGFloat = unsafe { msg_send![obj, pointPixelScale] };
if !scale.is_finite() || scale <= 0.0 {
return None;
}
Some(scale as f32)
}
pub fn cfg_set_scales_to_fit_if_available(cfg: &SCStreamConfiguration, scales_to_fit: bool) {
use objc2::runtime::NSObjectProtocol;
use objc2::{msg_send, sel};
let obj: &objc2::runtime::NSObject = cfg.as_ref();
if obj.respondsToSelector(sel!(setScalesToFit:)) {
unsafe {
let _: () = msg_send![obj, setScalesToFit: scales_to_fit];
}
}
}
pub fn cfg_set_width(cfg: &SCStreamConfiguration, v: usize) {
unsafe { cfg.setWidth(v) }
}
pub fn cfg_set_height(cfg: &SCStreamConfiguration, v: usize) {
unsafe { cfg.setHeight(v) }
}
pub fn cfg_set_queue_depth(cfg: &SCStreamConfiguration, v: isize) {
unsafe { cfg.setQueueDepth(v) }
}
pub fn cfg_set_shows_cursor(cfg: &SCStreamConfiguration, v: bool) {
unsafe { cfg.setShowsCursor(v) }
}
pub fn cfg_set_minimum_frame_interval(cfg: &SCStreamConfiguration, t: CMTime) {
unsafe { cfg.setMinimumFrameInterval(t) }
}
pub fn cfg_set_pixel_format(cfg: &SCStreamConfiguration, format: u32) {
unsafe { cfg.setPixelFormat(format) }
}
pub fn cfg_set_source_rect_if_available(cfg: &SCStreamConfiguration, rect: CGRect) -> bool {
use objc2::runtime::NSObjectProtocol;
use objc2::{msg_send, sel};
assert!(rect.size.width > 0.0);
assert!(rect.size.height > 0.0);
let obj: &objc2::runtime::NSObject = cfg.as_ref();
if !obj.respondsToSelector(sel!(setSourceRect:)) {
return false;
}
unsafe {
let _: () = msg_send![obj, setSourceRect: rect];
}
true
}
pub fn cfg_set_capture_dynamic_range_sdr_if_available(cfg: &SCStreamConfiguration) {
use objc2::runtime::NSObjectProtocol;
use objc2::sel;
let obj: &objc2::runtime::NSObject = cfg.as_ref();
if obj.respondsToSelector(sel!(setCaptureDynamicRange:)) {
unsafe {
cfg.setCaptureDynamicRange(SCCaptureDynamicRange::SDR);
}
}
}
pub fn cfg_set_capture_dynamic_range_hdr_if_available(cfg: &SCStreamConfiguration) {
use objc2::runtime::NSObjectProtocol;
use objc2::sel;
let obj: &objc2::runtime::NSObject = cfg.as_ref();
if obj.respondsToSelector(sel!(setCaptureDynamicRange:)) {
unsafe {
cfg.setCaptureDynamicRange(SCCaptureDynamicRange::HDRLocalDisplay);
}
}
}
pub fn cfg_set_color_space_name_if_available(cfg: &SCStreamConfiguration, name: &str) {
use objc2::runtime::NSObjectProtocol;
use objc2::sel;
assert!(!name.is_empty());
let obj: &objc2::runtime::NSObject = cfg.as_ref();
if obj.respondsToSelector(sel!(setColorSpaceName:)) {
let cf = CFString::from_str(name);
unsafe {
cfg.setColorSpaceName(&cf);
}
}
}
pub fn obs_minimum_frame_interval(target_fps: u32) -> CMTime {
assert!(target_fps >= crate::config::FPS_MIN);
assert!(target_fps <= crate::config::FPS_MAX);
CMTime {
value: crate::config::FRAME_INTERVAL_FACTOR_NUM as i64,
timescale: (crate::config::FRAME_INTERVAL_FACTOR_DEN as i32)
.saturating_mul(target_fps as i32),
flags: CMTimeFlags(1),
epoch: 0,
}
}
pub fn apply_capture_config(cfg: &SCStreamConfiguration, capture: &SckCaptureConfig) {
assert!(capture.target_fps() >= crate::config::FPS_MIN);
assert!(capture.queue_depth() >= crate::config::QUEUE_DEPTH_MIN);
cfg_set_queue_depth(cfg, capture.queue_depth() as isize);
cfg_set_pixel_format(cfg, capture.pixel_format().as_fourcc());
cfg_set_minimum_frame_interval(cfg, obs_minimum_frame_interval(capture.target_fps()));
cfg_set_color_space_name_if_available(cfg, capture.color_space().as_cf_name());
match capture.pixel_format() {
SckPixelFormat::L10rHdr => {
assert!(capture.color_space() == SckColorSpace::DisplayP3);
cfg_set_capture_dynamic_range_hdr_if_available(cfg);
}
SckPixelFormat::Bgra8 | SckPixelFormat::Nv12VideoRange | SckPixelFormat::Nv12FullRange => {
cfg_set_capture_dynamic_range_sdr_if_available(cfg);
}
}
if capture.captures_audio() {
cfg_set_captures_audio_if_available(cfg, true);
cfg_set_audio_sample_rate_if_available(cfg, capture.audio_sample_rate_hz());
cfg_set_audio_channel_count_if_available(cfg, capture.audio_channels());
cfg_set_excludes_current_process_audio_if_available(cfg, true);
} else {
cfg_set_captures_audio_if_available(cfg, false);
}
}
pub fn cfg_set_captures_audio_if_available(cfg: &SCStreamConfiguration, captures_audio: bool) {
use objc2::runtime::NSObjectProtocol;
use objc2::sel;
let obj: &objc2::runtime::NSObject = cfg.as_ref();
if obj.respondsToSelector(sel!(setCapturesAudio:)) {
unsafe {
cfg.setCapturesAudio(captures_audio);
}
}
}
pub fn cfg_set_audio_sample_rate_if_available(cfg: &SCStreamConfiguration, sample_rate_hz: u32) {
use objc2::runtime::NSObjectProtocol;
use objc2::sel;
assert!(sample_rate_hz >= crate::config::AUDIO_SAMPLE_RATE_MIN_HZ);
assert!(sample_rate_hz <= crate::config::AUDIO_SAMPLE_RATE_MAX_HZ);
let obj: &objc2::runtime::NSObject = cfg.as_ref();
if obj.respondsToSelector(sel!(setSampleRate:)) {
unsafe {
cfg.setSampleRate(sample_rate_hz as isize);
}
}
}
pub fn cfg_set_audio_channel_count_if_available(cfg: &SCStreamConfiguration, channels: u32) {
use objc2::runtime::NSObjectProtocol;
use objc2::sel;
assert!(channels >= crate::config::AUDIO_CHANNEL_COUNT_MIN);
assert!(channels <= crate::config::AUDIO_CHANNEL_COUNT_MAX);
let obj: &objc2::runtime::NSObject = cfg.as_ref();
if obj.respondsToSelector(sel!(setChannelCount:)) {
unsafe {
cfg.setChannelCount(channels as isize);
}
}
}
pub fn cfg_set_excludes_current_process_audio_if_available(
cfg: &SCStreamConfiguration,
excludes_self: bool,
) {
use objc2::runtime::NSObjectProtocol;
use objc2::sel;
let obj: &objc2::runtime::NSObject = cfg.as_ref();
if obj.respondsToSelector(sel!(setExcludesCurrentProcessAudio:)) {
unsafe {
cfg.setExcludesCurrentProcessAudio(excludes_self);
}
}
}
pub fn cfg_set_stream_name_if_available(cfg: &SCStreamConfiguration, name: &NSString) {
use objc2::runtime::NSObjectProtocol;
use objc2::sel;
let obj: &objc2::runtime::NSObject = cfg.as_ref();
if obj.respondsToSelector(sel!(setStreamName:)) {
unsafe {
cfg.setStreamName(Some(name));
}
}
}
pub fn sc_stream_add_stream_output(
stream: &SCStream,
output: &objc2::runtime::ProtocolObject<dyn objc2_screen_capture_kit::SCStreamOutput>,
kind: SCStreamOutputType,
queue: Option<&dispatch2::DispatchQueue>,
) -> Result<(), Retained<objc2_foundation::NSError>> {
unsafe { stream.addStreamOutput_type_sampleHandlerQueue_error(output, kind, queue) }
}