// SPDX-License-Identifier: AGPL-3.0-or-later use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use crate::{MAX_FRAME_HEIGHT, MAX_FRAME_WIDTH, nv12_byte_size}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TextureFormat { Nv12, P010, Bgra8, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BackendError { DimensionsOutOfRange { width: u32, height: u32 }, UnsupportedFormat { format: TextureFormat }, PlatformUnsupported { reason: &'static str }, KeyMismatch { expected: u64, observed: u64 }, AcquireWhileWriting { slot_index: u32 }, ReleaseWithoutAcquire { slot_index: u32 }, WouldBlock { slot_index: u32 }, } impl std::fmt::Display for BackendError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::DimensionsOutOfRange { width, height } => { write!(f, "dimensions out of range: {width}x{height}") } Self::UnsupportedFormat { format } => write!(f, "unsupported format: {format:?}"), Self::PlatformUnsupported { reason } => write!(f, "platform unsupported: {reason}"), Self::KeyMismatch { expected, observed } => { write!( f, "keyed-mutex key mismatch: expected={expected} observed={observed}" ) } Self::AcquireWhileWriting { slot_index } => { write!(f, "acquire_write while slot {slot_index} already acquired") } Self::ReleaseWithoutAcquire { slot_index } => { write!(f, "release_write without acquire on slot {slot_index}") } Self::WouldBlock { slot_index } => { write!( f, "keyed mutex busy on slot {slot_index}; skipped without blocking" ) } } } } impl std::error::Error for BackendError {} pub const NUM_SLOTS_DEFAULT: usize = 8; pub trait KeyedMutexBackend: Send { type SlotHandle: Send + Clone; const NUM_SLOTS: usize; fn create_slots( &mut self, width: u32, height: u32, format: TextureFormat, ) -> Result, BackendError>; fn acquire_write(&mut self, slot: &Self::SlotHandle, key: u64) -> Result<(), BackendError>; fn release_write(&mut self, slot: &Self::SlotHandle, next_key: u64) -> Result<(), BackendError>; fn poll_complete(&mut self, slot: &Self::SlotHandle) -> bool; fn mark_consumed(&mut self, slot: &Self::SlotHandle); fn fill_test_pattern(&mut self, _slot: &Self::SlotHandle, _value: u8) {} } #[derive(Clone)] pub struct CpuSlotHandle { inner: Arc, } impl CpuSlotHandle { pub fn slot_index(&self) -> u32 { let idx = self.inner.slot_index; assert!((idx as usize) < NUM_SLOTS_DEFAULT, "slot_index in range"); assert!( self.inner.buffer.len() == self.inner.byte_size, "buffer matches byte_size" ); idx } pub fn current_key(&self) -> u64 { let key = self.inner.current_key.load(Ordering::Acquire); assert!( self.inner.buffer.len() == self.inner.byte_size, "buffer intact" ); assert!( (self.inner.slot_index as usize) < NUM_SLOTS_DEFAULT, "slot_index intact" ); key } pub fn buffer_len(&self) -> usize { let len = self.inner.byte_size; assert!(len > 0, "byte_size positive"); assert!(self.inner.buffer.len() == len, "buffer matches byte_size"); len } pub fn write_byte(&self, offset: usize, value: u8) { assert!(offset < self.inner.byte_size, "offset within buffer"); assert!( self.inner.acquired.load(Ordering::Acquire), "writes only while acquired", ); unsafe { let ptr = self.inner.buffer.as_ptr().add(offset) as *mut u8; ptr.write_volatile(value); } } } struct CpuSlotInner { slot_index: u32, byte_size: usize, buffer: Vec, current_key: AtomicU64, acquired: AtomicBool, completed: AtomicBool, } pub struct CpuMemcpyBackend { width: u32, height: u32, format: TextureFormat, slots_created: bool, slot_count: u32, } impl CpuMemcpyBackend { pub fn new() -> Self { let backend = Self { width: 0, height: 0, format: TextureFormat::Nv12, slots_created: false, slot_count: 0, }; assert!(!backend.slots_created, "fresh backend has no slots"); assert_eq!(backend.slot_count, 0, "fresh slot_count zero"); backend } pub fn width(&self) -> u32 { assert!(self.width <= MAX_FRAME_WIDTH, "width within cap"); self.width } pub fn height(&self) -> u32 { assert!(self.height <= MAX_FRAME_HEIGHT, "height within cap"); self.height } } impl Default for CpuMemcpyBackend { fn default() -> Self { Self::new() } } impl KeyedMutexBackend for CpuMemcpyBackend { type SlotHandle = CpuSlotHandle; const NUM_SLOTS: usize = NUM_SLOTS_DEFAULT; fn create_slots( &mut self, width: u32, height: u32, format: TextureFormat, ) -> Result, BackendError> { if width == 0 || height == 0 || width > MAX_FRAME_WIDTH || height > MAX_FRAME_HEIGHT { return Err(BackendError::DimensionsOutOfRange { width, height }); } if !matches!(format, TextureFormat::Nv12) { return Err(BackendError::UnsupportedFormat { format }); } assert!(!self.slots_created, "slots created once"); let byte_size = nv12_byte_size(width, height); assert!(byte_size > 0, "byte_size positive"); let mut out: Vec = Vec::with_capacity(Self::NUM_SLOTS); for idx in 0..Self::NUM_SLOTS { let inner = CpuSlotInner { slot_index: idx as u32, byte_size, buffer: vec![0u8; byte_size], current_key: AtomicU64::new(0), acquired: AtomicBool::new(false), completed: AtomicBool::new(false), }; out.push(CpuSlotHandle { inner: Arc::new(inner), }); } self.width = width; self.height = height; self.format = format; self.slots_created = true; self.slot_count = Self::NUM_SLOTS as u32; assert_eq!(out.len(), Self::NUM_SLOTS, "slot vector length"); assert!(self.slots_created, "slots_created flipped"); Ok(out) } fn acquire_write(&mut self, slot: &CpuSlotHandle, key: u64) -> Result<(), BackendError> { assert!(self.slots_created, "slots must exist before acquire"); let current = slot.inner.current_key.load(Ordering::Acquire); if current != key { return Err(BackendError::KeyMismatch { expected: key, observed: current, }); } let was_acquired = slot.inner.acquired.swap(true, Ordering::AcqRel); if was_acquired { return Err(BackendError::AcquireWhileWriting { slot_index: slot.inner.slot_index, }); } slot.inner.completed.store(false, Ordering::Release); assert!( slot.inner.acquired.load(Ordering::Acquire), "acquired flag set" ); assert!( !slot.inner.completed.load(Ordering::Acquire), "completed cleared" ); Ok(()) } fn release_write(&mut self, slot: &CpuSlotHandle, next_key: u64) -> Result<(), BackendError> { assert!(self.slots_created, "slots must exist before release"); let was_acquired = slot.inner.acquired.swap(false, Ordering::AcqRel); if !was_acquired { return Err(BackendError::ReleaseWithoutAcquire { slot_index: slot.inner.slot_index, }); } slot.inner.current_key.store(next_key, Ordering::Release); slot.inner.completed.store(true, Ordering::Release); assert!( !slot.inner.acquired.load(Ordering::Acquire), "acquired cleared" ); assert!( slot.inner.completed.load(Ordering::Acquire), "completed flag set" ); Ok(()) } fn poll_complete(&mut self, slot: &CpuSlotHandle) -> bool { let done = slot.inner.completed.load(Ordering::Acquire); assert!(self.slots_created, "slots exist for poll"); assert!( (slot.inner.slot_index as usize) < Self::NUM_SLOTS, "slot index in range" ); done } fn mark_consumed(&mut self, slot: &CpuSlotHandle) { assert!(self.slots_created, "slots exist for mark_consumed"); assert!( (slot.inner.slot_index as usize) < Self::NUM_SLOTS, "slot index in range" ); slot.inner.completed.store(false, Ordering::Release); } fn fill_test_pattern(&mut self, slot: &CpuSlotHandle, value: u8) { assert!(self.slots_created, "slots exist for fill"); assert!( slot.inner.acquired.load(Ordering::Acquire), "fill only while acquired" ); let len = slot.inner.byte_size; for offset in 0..len { slot.write_byte(offset, value); } } } #[cfg(test)] mod tests { use super::*; #[test] fn create_slots_for_1080p_nv12_yields_eight_slots() { let mut backend = CpuMemcpyBackend::new(); let slots = backend .create_slots(1920, 1080, TextureFormat::Nv12) .expect("creation succeeds"); assert_eq!(slots.len(), CpuMemcpyBackend::NUM_SLOTS); assert_eq!(slots.len(), 8); for (idx, slot) in slots.iter().enumerate() { assert_eq!(slot.slot_index(), idx as u32); assert_eq!(slot.buffer_len(), 1920 * 1080 * 3 / 2); } } #[test] fn create_slots_rejects_zero_dims() { let mut backend = CpuMemcpyBackend::new(); let err = backend.create_slots(0, 1080, TextureFormat::Nv12).err(); assert!(matches!( err, Some(BackendError::DimensionsOutOfRange { .. }) )); } #[test] fn create_slots_rejects_unsupported_format() { let mut backend = CpuMemcpyBackend::new(); let err = backend.create_slots(1920, 1080, TextureFormat::P010).err(); assert!(matches!(err, Some(BackendError::UnsupportedFormat { .. }))); } #[test] fn acquire_release_round_trip_marks_complete() { let mut backend = CpuMemcpyBackend::new(); let slots = backend .create_slots(64, 64, TextureFormat::Nv12) .expect("create"); let slot = slots[0].clone(); backend.acquire_write(&slot, 0).expect("acquire"); backend.release_write(&slot, 1).expect("release"); assert!(backend.poll_complete(&slot)); backend.mark_consumed(&slot); assert!(!backend.poll_complete(&slot)); } #[test] fn double_acquire_rejects() { let mut backend = CpuMemcpyBackend::new(); let slots = backend .create_slots(64, 64, TextureFormat::Nv12) .expect("create"); let slot = slots[0].clone(); backend.acquire_write(&slot, 0).expect("first acquire"); let err = backend.acquire_write(&slot, 0).err(); assert!(matches!( err, Some(BackendError::AcquireWhileWriting { .. }) )); } }