#![cfg(any(target_os = "macos", target_os = "ios"))]
use crate::{
error::{Error, Result},
packed_rgba16f_layout, BufferIdentity, DType, PixelFormat, TensorMap, TensorMapTrait,
TensorMemory, TensorTrait,
};
use log::trace;
use num_traits::Num;
use std::{
ffi::c_void,
fmt,
marker::PhantomData,
ops::{Deref, DerefMut},
ptr::NonNull,
sync::Arc,
};
type IOSurfaceRef = *mut c_void;
type CFDictionaryRef = *mut c_void;
type CFStringRef = *mut c_void;
type CFNumberRef = *mut c_void;
const K_CF_NUMBER_LONG_TYPE: i32 = 10; const K_CF_STRING_ENCODING_UTF8: u32 = 0x08000100;
const K_IOSURFACE_LOCK_READ_ONLY: u32 = 0x01;
#[allow(dead_code)]
const K_IOSURFACE_LOCK_AVOID_SYNC: u32 = 0x02;
#[allow(clippy::duplicated_attributes)]
#[link(name = "IOSurface", kind = "framework")]
#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {
fn IOSurfaceCreate(properties: CFDictionaryRef) -> IOSurfaceRef;
fn IOSurfaceLock(surface: IOSurfaceRef, options: u32, seed: *mut u32) -> i32;
fn IOSurfaceUnlock(surface: IOSurfaceRef, options: u32, seed: *mut u32) -> i32;
fn IOSurfaceGetBaseAddress(surface: IOSurfaceRef) -> *mut c_void;
fn IOSurfaceGetAllocSize(surface: IOSurfaceRef) -> usize;
fn IOSurfaceGetBytesPerRow(surface: IOSurfaceRef) -> usize;
fn IOSurfaceGetWidth(surface: IOSurfaceRef) -> usize;
fn IOSurfaceGetHeight(surface: IOSurfaceRef) -> usize;
fn IOSurfaceGetID(surface: IOSurfaceRef) -> u32;
fn CFRetain(cf: *const c_void) -> *const c_void;
fn CFRelease(cf: *const c_void);
fn CFDictionaryCreateMutable(
allocator: *const c_void,
capacity: isize,
key_callbacks: *const c_void,
value_callbacks: *const c_void,
) -> CFDictionaryRef;
fn CFDictionarySetValue(dict: CFDictionaryRef, key: *const c_void, value: *const c_void);
fn CFStringCreateWithCString(
allocator: *const c_void,
cstr: *const i8,
encoding: u32,
) -> CFStringRef;
fn CFNumberCreate(allocator: *const c_void, ty: i32, value_ptr: *const c_void) -> CFNumberRef;
static kCFTypeDictionaryKeyCallBacks: c_void;
static kCFTypeDictionaryValueCallBacks: c_void;
}
#[derive(Debug, Clone)]
pub(crate) struct OwnedIoSurface {
inner: Arc<IoSurfaceHandle>,
}
#[derive(Debug)]
struct IoSurfaceHandle(IOSurfaceRef);
unsafe impl Send for IoSurfaceHandle {}
unsafe impl Sync for IoSurfaceHandle {}
impl Drop for IoSurfaceHandle {
fn drop(&mut self) {
if !self.0.is_null() {
unsafe { CFRelease(self.0 as *const c_void) };
}
}
}
impl OwnedIoSurface {
fn from_created(ptr: IOSurfaceRef) -> Result<Self> {
if ptr.is_null() {
return Err(Error::IoError(std::io::Error::other(
"IOSurfaceCreate returned null",
)));
}
Ok(Self {
inner: Arc::new(IoSurfaceHandle(ptr)),
})
}
pub(crate) fn from_external(ptr: IOSurfaceRef) -> Result<Self> {
if ptr.is_null() {
return Err(Error::InvalidArgument(
"from_external: null IOSurfaceRef".into(),
));
}
unsafe { CFRetain(ptr as *const c_void) };
Ok(Self {
inner: Arc::new(IoSurfaceHandle(ptr)),
})
}
pub(crate) fn as_ptr(&self) -> IOSurfaceRef {
self.inner.0
}
}
#[derive(Debug)]
pub struct IoSurfaceTensor<T>
where
T: Num + Clone + fmt::Debug + Send + Sync,
{
pub name: String,
pub(crate) surface: OwnedIoSurface,
pub shape: Vec<usize>,
pub _marker: PhantomData<T>,
identity: BufferIdentity,
pub(crate) buf_size: usize,
pub(crate) bytes_per_row: usize,
#[allow(dead_code)]
pub(crate) is_imported: bool,
pub(crate) view_offset: usize,
}
unsafe impl<T> Send for IoSurfaceTensor<T> where T: Num + Clone + fmt::Debug + Send + Sync {}
unsafe impl<T> Sync for IoSurfaceTensor<T> where T: Num + Clone + fmt::Debug + Send + Sync {}
impl<T> TensorTrait<T> for IoSurfaceTensor<T>
where
T: Num + Clone + fmt::Debug + Send + Sync,
{
fn new(shape: &[usize], name: Option<&str>) -> Result<Self> {
let byte_size = shape
.iter()
.product::<usize>()
.saturating_mul(std::mem::size_of::<T>());
Self::new_with_byte_size(shape, byte_size, name)
}
fn from_fd(_fd: std::os::fd::OwnedFd, _shape: &[usize], _name: Option<&str>) -> Result<Self> {
Err(Error::NotImplemented(
"IoSurfaceTensor::from_fd: IOSurface is not fd-backed; use from_surface()".into(),
))
}
fn clone_fd(&self) -> Result<std::os::fd::OwnedFd> {
Err(Error::NotImplemented(
"IoSurfaceTensor::clone_fd: use surface_id() for cross-process sharing".into(),
))
}
fn memory(&self) -> TensorMemory {
TensorMemory::Dma
}
fn name(&self) -> String {
self.name.clone()
}
fn shape(&self) -> &[usize] {
&self.shape
}
fn reshape(&mut self, shape: &[usize]) -> Result<()> {
let new_elems: usize = shape.iter().product();
let cur_elems: usize = self.shape.iter().product();
if new_elems != cur_elems {
return Err(Error::InvalidShape(format!(
"reshape: element count mismatch ({cur_elems} → {new_elems})",
)));
}
self.shape = shape.to_vec();
self.view_offset = 0;
Ok(())
}
fn map_with(&self, access: crate::CpuAccess) -> Result<TensorMap<T>> {
let _span = tracing::trace_span!("tensor.map", memory = "iosurface", ?access).entered();
let m = IoSurfaceMap::new(
self.surface.clone(),
self.shape.clone(),
self.buf_size,
self.view_offset,
access,
)?;
Ok(TensorMap::IoSurface(m))
}
fn buffer_identity(&self) -> &BufferIdentity {
&self.identity
}
fn capacity_bytes(&self) -> usize {
self.buf_size
}
fn set_logical_shape(&mut self, shape: &[usize]) -> Result<()> {
if shape.is_empty() {
return Err(Error::InvalidSize(0));
}
let needed = shape.iter().product::<usize>() * std::mem::size_of::<T>();
if needed > self.buf_size {
return Err(Error::InsufficientCapacity {
needed,
capacity: self.buf_size,
});
}
self.shape = shape.to_vec();
Ok(())
}
fn view(&self, offset_bytes: usize, shape: &[usize]) -> Result<Self> {
if !offset_bytes.is_multiple_of(std::mem::align_of::<T>()) {
return Err(Error::InvalidOperation(format!(
"IoSurfaceTensor::view: offset {offset_bytes} not aligned to align_of::<T>()={}",
std::mem::align_of::<T>()
)));
}
let abs_offset = self
.view_offset
.checked_add(offset_bytes)
.ok_or(Error::InvalidSize(offset_bytes))?;
let logical = shape.iter().product::<usize>() * std::mem::size_of::<T>();
let needed = abs_offset
.checked_add(logical)
.ok_or(Error::InvalidSize(logical))?;
if needed > self.buf_size {
return Err(Error::InsufficientCapacity {
needed,
capacity: self.buf_size,
});
}
Ok(Self {
name: self.name.clone(),
surface: self.surface.clone(),
shape: shape.to_vec(),
_marker: PhantomData,
identity: self.identity.clone(),
buf_size: self.buf_size,
bytes_per_row: self.bytes_per_row,
is_imported: self.is_imported,
view_offset: abs_offset,
})
}
}
impl<T> IoSurfaceTensor<T>
where
T: Num + Clone + fmt::Debug + Send + Sync,
{
pub(crate) fn new_with_byte_size(
shape: &[usize],
byte_size: usize,
name: Option<&str>,
) -> Result<Self> {
let _span =
tracing::trace_span!("tensor.alloc", memory = "iosurface", byte_size,).entered();
let (dict, ptr) = unsafe {
let dict = build_props(byte_size.max(1), 1, 1, FOURCC_L008)?;
let ptr = IOSurfaceCreate(dict);
(dict, ptr)
};
unsafe { CFRelease(dict as *const c_void) };
let surface = OwnedIoSurface::from_created(ptr)?;
let alloc = unsafe { IOSurfaceGetAllocSize(surface.as_ptr()) };
let bytes_per_row = unsafe { IOSurfaceGetBytesPerRow(surface.as_ptr()) };
let name = match name {
Some(s) => s.to_owned(),
None => format!("iosurface-{}", uuid::Uuid::new_v4()),
};
trace!("IoSurfaceTensor::new: name={name} bytes={alloc} shape={shape:?}",);
Ok(Self {
name,
surface,
shape: shape.to_vec(),
_marker: PhantomData,
identity: BufferIdentity::new(),
buf_size: alloc,
bytes_per_row,
is_imported: false,
view_offset: 0,
})
}
pub(crate) fn new_image(
width: usize,
height: usize,
format: PixelFormat,
dtype: DType,
shape: &[usize],
name: Option<&str>,
) -> Result<Self> {
let _span =
tracing::trace_span!("tensor.alloc", memory = "iosurface", width, height, ?format,)
.entered();
let (fourcc, bpe) = image_fourcc_and_bpe(format, dtype).ok_or_else(|| {
Error::NotImplemented(format!(
"IoSurfaceTensor::new_image: ({format:?}, {dtype:?}) has no IOSurface FourCC mapping"
))
})?;
let (surface_width, surface_height) = match (format, dtype) {
(PixelFormat::PlanarRgb | PixelFormat::PlanarRgba, DType::F16) => {
let layout =
packed_rgba16f_layout(format, dtype, width, height).ok_or_else(|| {
Error::InvalidShape(format!(
"{format:?} F16 IOSurface requires width%4==0 for RGBA16F packing \
(got width={width})"
))
})?;
(layout.surface_w, layout.surface_h)
}
(PixelFormat::Rgb, DType::U8 | DType::I8) => {
let layout = crate::packed_rgb888_layout(width, height).ok_or_else(|| {
Error::InvalidShape(format!(
"Rgb u8 IOSurface requires width%4==0 for RGBA8888 packing \
(got width={width})"
))
})?;
(layout.surface_w, layout.surface_h)
}
(PixelFormat::PlanarRgb, _) => {
let sh = height.checked_mul(3).ok_or_else(|| {
Error::InvalidShape(format!("PlanarRgb height overflow (height={height})"))
})?;
(width, sh)
}
(PixelFormat::PlanarRgba, _) => {
let sh = height.checked_mul(4).ok_or_else(|| {
Error::InvalidShape(format!("PlanarRgba height overflow (height={height})"))
})?;
(width, sh)
}
(PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv24, _) => format
.semi_planar_surface_dims(width, height, bpe)
.ok_or_else(|| {
Error::InvalidShape(format!(
"{format:?} has no semi-planar surface dims for {width}x{height}"
))
})?,
_ => (width, height),
};
let dict = unsafe { build_props(surface_width, surface_height, bpe, fourcc) }?;
let ptr = unsafe { IOSurfaceCreate(dict) };
unsafe { CFRelease(dict) };
let surface = OwnedIoSurface::from_created(ptr)?;
let alloc = unsafe { IOSurfaceGetAllocSize(surface.as_ptr()) };
let bytes_per_row = unsafe { IOSurfaceGetBytesPerRow(surface.as_ptr()) };
let name = match name {
Some(s) => s.to_owned(),
None => format!("iosurface-img-{}", uuid::Uuid::new_v4()),
};
trace!(
"IoSurfaceTensor::new_image: name={name} surface={surface_width}x{surface_height} \
logical={width}x{height} {format:?}/{dtype:?} fourcc=0x{fourcc:08x} bytes={alloc} \
bytes_per_row={bytes_per_row}",
);
Ok(Self {
name,
surface,
shape: shape.to_vec(),
_marker: PhantomData,
identity: BufferIdentity::new(),
buf_size: alloc,
bytes_per_row,
is_imported: false,
view_offset: 0,
})
}
pub unsafe fn from_surface(
surface_ref: *mut c_void,
shape: &[usize],
name: Option<&str>,
) -> Result<Self> {
let surface = OwnedIoSurface::from_external(surface_ref)?;
let alloc = IOSurfaceGetAllocSize(surface.as_ptr());
let bytes_per_row = IOSurfaceGetBytesPerRow(surface.as_ptr());
let elem_size = std::mem::size_of::<T>();
let elems: usize = shape.iter().product();
let requested = elems.checked_mul(elem_size).ok_or_else(|| {
Error::InvalidShape(format!(
"from_surface: shape footprint overflows usize (shape={shape:?}, sizeof T={elem_size})",
))
})?;
if requested > alloc {
return Err(Error::InvalidShape(format!(
"from_surface: shape requires {requested} bytes but IOSurface only \
has {alloc} (shape={shape:?}, sizeof T={elem_size})",
)));
}
let name = match name {
Some(s) => s.to_owned(),
None => format!("iosurface-imported-{}", uuid::Uuid::new_v4()),
};
Ok(Self {
name,
surface,
shape: shape.to_vec(),
_marker: PhantomData,
identity: BufferIdentity::new(),
buf_size: alloc,
bytes_per_row,
is_imported: true,
view_offset: 0,
})
}
pub(crate) fn bytes_per_row(&self) -> usize {
self.bytes_per_row
}
pub(crate) fn physical_surface_dims(&self) -> (usize, usize) {
let w = unsafe { IOSurfaceGetWidth(self.surface.as_ptr()) };
let h = unsafe { IOSurfaceGetHeight(self.surface.as_ptr()) };
(w, h)
}
pub(crate) fn image_backing_row_stride(&self) -> Option<usize> {
let (_w, h) = self.physical_surface_dims();
(h > 1).then_some(self.bytes_per_row)
}
pub(crate) fn map_with_byte_size(
&self,
byte_size: usize,
access: crate::CpuAccess,
) -> Result<TensorMap<T>> {
let m = IoSurfaceMap::new_with_byte_size(
self.surface.clone(),
self.shape.clone(),
self.buf_size,
byte_size,
self.view_offset,
access,
)?;
Ok(TensorMap::IoSurface(m))
}
pub fn surface_id(&self) -> u32 {
unsafe { IOSurfaceGetID(self.surface.as_ptr()) }
}
pub fn surface_ref(&self) -> *mut c_void {
self.surface.as_ptr()
}
}
impl<T> crate::Tensor<T>
where
T: Num + Clone + fmt::Debug + Send + Sync,
{
pub fn iosurface_ref(&self) -> Option<*mut c_void> {
match &self.storage {
crate::TensorStorage::Dma(io_tensor) => Some(io_tensor.surface_ref()),
_ => None,
}
}
pub fn iosurface_id(&self) -> Option<u32> {
match &self.storage {
crate::TensorStorage::Dma(io_tensor) => Some(io_tensor.surface_id()),
_ => None,
}
}
pub fn iosurface_physical_dims(&self) -> Option<(usize, usize)> {
match &self.storage {
crate::TensorStorage::Dma(io_tensor) => Some(io_tensor.physical_surface_dims()),
_ => None,
}
}
pub unsafe fn from_iosurface(
surface_ref: *mut c_void,
shape: &[usize],
name: Option<&str>,
) -> Result<Self>
where
T: num_traits::Num,
{
let inner = unsafe { IoSurfaceTensor::<T>::from_surface(surface_ref, shape, name)? };
Ok(crate::Tensor::wrap(crate::TensorStorage::Dma(inner)))
}
}
#[derive(Debug)]
pub struct IoSurfaceMap<T>
where
T: Num + Clone + fmt::Debug,
{
surface: OwnedIoSurface,
shape: Vec<usize>,
base_ptr: NonNull<c_void>,
buf_size: usize,
byte_size_override: Option<usize>,
_marker: PhantomData<T>,
lock_options: u32,
writable: bool,
locked: bool,
}
unsafe impl<T> Send for IoSurfaceMap<T> where T: Num + Clone + fmt::Debug {}
unsafe impl<T> Sync for IoSurfaceMap<T> where T: Num + Clone + fmt::Debug {}
impl<T> IoSurfaceMap<T>
where
T: Num + Clone + fmt::Debug,
{
fn new(
surface: OwnedIoSurface,
shape: Vec<usize>,
buf_size: usize,
view_offset: usize,
access: crate::CpuAccess,
) -> Result<Self> {
Self::new_inner(surface, shape, buf_size, None, view_offset, access)
}
fn new_with_byte_size(
surface: OwnedIoSurface,
shape: Vec<usize>,
buf_size: usize,
byte_size: usize,
view_offset: usize,
access: crate::CpuAccess,
) -> Result<Self> {
Self::new_inner(
surface,
shape,
buf_size,
Some(byte_size),
view_offset,
access,
)
}
fn new_inner(
surface: OwnedIoSurface,
shape: Vec<usize>,
buf_size: usize,
byte_size_override: Option<usize>,
view_offset: usize,
access: crate::CpuAccess,
) -> Result<Self> {
let options: u32 = if access.writes() {
0
} else {
K_IOSURFACE_LOCK_READ_ONLY
};
let mut seed: u32 = 0;
let lock_rc = unsafe { IOSurfaceLock(surface.as_ptr(), options, &mut seed) };
if lock_rc != 0 {
return Err(Error::IoError(std::io::Error::other(format!(
"IOSurfaceLock failed (rc={lock_rc})"
))));
}
let base = unsafe { IOSurfaceGetBaseAddress(surface.as_ptr()) };
let base_ptr = NonNull::new(base).ok_or_else(|| {
Error::IoError(std::io::Error::other(
"IOSurfaceGetBaseAddress returned null after lock",
))
})?;
if view_offset > buf_size {
let mut seed: u32 = 0;
unsafe { IOSurfaceUnlock(surface.as_ptr(), options, &mut seed) };
return Err(Error::InsufficientCapacity {
needed: view_offset,
capacity: buf_size,
});
}
let base_ptr = NonNull::new(unsafe { base_ptr.as_ptr().byte_add(view_offset) })
.expect("offset base within locked surface is non-null");
Ok(Self {
surface,
shape,
base_ptr,
buf_size: buf_size - view_offset,
byte_size_override,
_marker: PhantomData,
lock_options: options,
writable: access.writes(),
locked: true,
})
}
fn elem_count(&self) -> usize {
match self.byte_size_override {
Some(bytes) => bytes / std::mem::size_of::<T>(),
None => self.shape.iter().product(),
}
}
}
impl<T> TensorMapTrait<T> for IoSurfaceMap<T>
where
T: Num + Clone + fmt::Debug,
{
fn shape(&self) -> &[usize] {
&self.shape
}
fn len(&self) -> usize {
self.elem_count()
}
fn unmap(&mut self) {
if self.locked {
let mut seed: u32 = 0;
unsafe {
IOSurfaceUnlock(self.surface.as_ptr(), self.lock_options, &mut seed);
}
self.locked = false;
}
}
fn as_slice(&self) -> &[T] {
self.deref()
}
fn as_mut_slice(&mut self) -> &mut [T] {
self.deref_mut()
}
}
impl<T> Deref for IoSurfaceMap<T>
where
T: Num + Clone + fmt::Debug,
{
type Target = [T];
fn deref(&self) -> &[T] {
let ptr = self.base_ptr.as_ptr() as *const T;
let len = self.elem_count();
debug_assert!(
len * std::mem::size_of::<T>() <= self.buf_size,
"IoSurfaceMap deref: {} elems × {} bytes > buf_size {}",
len,
std::mem::size_of::<T>(),
self.buf_size,
);
unsafe { std::slice::from_raw_parts(ptr, len) }
}
}
impl<T> DerefMut for IoSurfaceMap<T>
where
T: Num + Clone + fmt::Debug,
{
fn deref_mut(&mut self) -> &mut [T] {
crate::assert_map_writable(self.writable, "IoSurface");
let ptr = self.base_ptr.as_ptr() as *mut T;
let len = self.elem_count();
debug_assert!(
len * std::mem::size_of::<T>() <= self.buf_size,
"IoSurfaceMap deref_mut: {} elems × {} bytes > buf_size {}",
len,
std::mem::size_of::<T>(),
self.buf_size,
);
unsafe { std::slice::from_raw_parts_mut(ptr, len) }
}
}
impl<T> Drop for IoSurfaceMap<T>
where
T: Num + Clone + fmt::Debug,
{
fn drop(&mut self) {
self.unmap();
}
}
const FOURCC_L008: u32 = u32::from_be_bytes(*b"L008");
fn image_fourcc_and_bpe(format: PixelFormat, dtype: DType) -> Option<(u32, usize)> {
match (format, dtype) {
(PixelFormat::Yuyv, DType::U8) => Some((u32::from_be_bytes(*b"2C08"), 2)),
(PixelFormat::Rgba, DType::U8 | DType::I8) => Some((u32::from_be_bytes(*b"RGBA"), 4)),
(PixelFormat::Bgra, DType::U8) => Some((u32::from_be_bytes(*b"BGRA"), 4)),
(PixelFormat::Rgb, DType::U8 | DType::I8) => Some((u32::from_be_bytes(*b"RGBA"), 4)),
(
PixelFormat::Grey | PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv24,
DType::U8,
) => Some((u32::from_be_bytes(*b"L008"), 1)),
(PixelFormat::Rgba | PixelFormat::PlanarRgb | PixelFormat::PlanarRgba, DType::F16) => {
Some((u32::from_be_bytes(*b"RGhA"), 8))
}
_ => None,
}
}
pub fn image_iosurface_layout(format: PixelFormat, dtype: DType) -> Option<(u32, usize)> {
image_fourcc_and_bpe(format, dtype)
}
unsafe fn build_props(
width: usize,
height: usize,
bytes_per_element: usize,
fourcc: u32,
) -> Result<CFDictionaryRef> {
let bytes_per_row = width
.checked_mul(bytes_per_element)
.and_then(|b| b.checked_add(63))
.map(|b| b & !63)
.ok_or_else(|| {
Error::InvalidShape(format!(
"IOSurface bytes-per-row overflow (width={width}, bpe={bytes_per_element})"
))
})?;
let alloc_size = bytes_per_row.checked_mul(height).ok_or_else(|| {
Error::InvalidShape(format!(
"IOSurface allocation size overflow (bytes_per_row={bytes_per_row}, height={height})"
))
})?;
let dict = CFDictionaryCreateMutable(
std::ptr::null(),
0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks,
);
if dict.is_null() {
return Err(Error::IoError(std::io::Error::other(
"CFDictionaryCreateMutable returned null",
)));
}
let set_num = |key: &str, value: i64| -> Result<()> {
let key_c = std::ffi::CString::new(key)
.map_err(|e| Error::InvalidArgument(format!("CString: {e}")))?;
let key_cf =
CFStringCreateWithCString(std::ptr::null(), key_c.as_ptr(), K_CF_STRING_ENCODING_UTF8);
if key_cf.is_null() {
return Err(Error::IoError(std::io::Error::other(
"CFStringCreateWithCString returned null",
)));
}
let value_cf = CFNumberCreate(
std::ptr::null(),
K_CF_NUMBER_LONG_TYPE,
&value as *const i64 as *const c_void,
);
if value_cf.is_null() {
CFRelease(key_cf as *const c_void);
return Err(Error::IoError(std::io::Error::other(
"CFNumberCreate returned null",
)));
}
CFDictionarySetValue(dict, key_cf as *const c_void, value_cf as *const c_void);
CFRelease(key_cf as *const c_void);
CFRelease(value_cf as *const c_void);
Ok(())
};
let result = (|| -> Result<()> {
set_num("IOSurfaceWidth", width as i64)?;
set_num("IOSurfaceHeight", height as i64)?;
set_num("IOSurfaceBytesPerElement", bytes_per_element as i64)?;
set_num("IOSurfacePixelFormat", fourcc as i64)?;
set_num("IOSurfaceBytesPerRow", bytes_per_row as i64)?;
set_num("IOSurfaceAllocSize", alloc_size as i64)?;
Ok(())
})();
if let Err(e) = result {
CFRelease(dict as *const c_void);
return Err(e);
}
Ok(dict)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alloc_map_write_read_roundtrip() {
let t = IoSurfaceTensor::<u8>::new(&[256], None).expect("alloc");
assert!(t.buf_size >= 256, "buf_size should accommodate shape");
assert_eq!(t.memory(), TensorMemory::Dma);
{
let mut m = t.map().expect("map");
let slice = m.as_mut_slice();
assert!(slice.len() >= 256);
for (i, b) in slice.iter_mut().take(256).enumerate() {
*b = (i & 0xff) as u8;
}
}
{
let m = t.map().expect("remap");
let slice = m.as_slice();
for (i, b) in slice.iter().take(256).enumerate() {
assert_eq!(*b, (i & 0xff) as u8, "byte {i} mismatch");
}
}
}
#[test]
fn image_surface_strided_map_honours_bytes_per_row() {
use crate::Tensor;
let h = 3usize;
let w = 17usize;
let t = Tensor::<u8>::image(
w,
h,
PixelFormat::Rgba,
Some(TensorMemory::Dma),
crate::CpuAccess::ReadWrite,
)
.expect("non-aligned packed RGBA should allocate a padded IOSurface");
let stride = t.effective_row_stride().expect("stride");
assert!(stride >= w * 4, "stride {stride} >= natural {}", w * 4);
assert_eq!(stride % 64, 0, "IOSurface pads bytes_per_row to 64");
assert!(stride > w * 4, "width 17 RGBA must be padded (68 -> 128)");
assert_eq!(t.width(), Some(w));
assert_eq!(t.height(), Some(h));
{
let mut m = t.map().expect("strided IOSurface map");
let buf = m.as_mut_slice();
assert_eq!(buf.len(), stride * h, "map exposes the full padded surface");
for row in 0..h {
for col in 0..w * 4 {
buf[row * stride + col] = (row * 37 + col) as u8;
}
}
}
{
let m = t.map().expect("remap");
let buf = m.as_slice();
for row in 0..h {
for col in 0..w * 4 {
assert_eq!(
buf[row * stride + col],
(row * 37 + col) as u8,
"pixel byte ({row},{col}) mismatch"
);
}
}
}
}
#[test]
fn copy_to_flat_compacts_padded_rows() {
use crate::{Tensor, TensorDyn};
let (w, h) = (17usize, 3usize);
let t = Tensor::<u8>::image(
w,
h,
PixelFormat::Rgba,
Some(TensorMemory::Dma),
crate::CpuAccess::ReadWrite,
)
.expect("padded IOSurface alloc");
let stride = t.effective_row_stride().expect("stride");
assert!(stride > w * 4, "test requires a padded stride");
{
let mut m = t.map().expect("map");
let buf = m.as_mut_slice();
for row in 0..h {
for col in 0..w * 4 {
buf[row * stride + col] = (row * 91 + col) as u8;
}
}
}
let mut flat = vec![0u8; h * w * 4];
t.copy_to_flat(&mut flat).expect("copy_to_flat");
for row in 0..h {
for col in 0..w * 4 {
assert_eq!(
flat[row * w * 4 + col],
(row * 91 + col) as u8,
"flat byte ({row},{col})"
);
}
}
let mut short = vec![0u8; h * w * 4 - 1];
assert!(t.copy_to_flat(&mut short).is_err());
let tight = TensorDyn::image(
64,
4,
PixelFormat::Rgba,
DType::U8,
Some(TensorMemory::Mem),
crate::CpuAccess::ReadWrite,
)
.expect("tight Mem alloc");
{
let tu8 = tight.as_u8().unwrap();
let mut m = tu8.map().expect("map");
for (i, b) in m.as_mut_slice().iter_mut().enumerate() {
*b = (i % 251) as u8;
}
}
let mut flat2 = vec![0u8; 64 * 4 * 4];
tight.copy_to_flat(&mut flat2).expect("tight copy_to_flat");
for (i, b) in flat2.iter().enumerate() {
assert_eq!(*b, (i % 251) as u8, "tight byte {i}");
}
}
#[test]
fn explicit_dma_unmapped_image_format_errors() {
use crate::Tensor;
for (name, result) in [
(
"PlanarRgb u8",
Tensor::<u8>::image(
64,
8,
PixelFormat::PlanarRgb,
Some(TensorMemory::Dma),
crate::CpuAccess::ReadWrite,
)
.map(drop),
),
(
"Rgb f32",
Tensor::<f32>::image(
64,
8,
PixelFormat::Rgb,
Some(TensorMemory::Dma),
crate::CpuAccess::ReadWrite,
)
.map(drop),
),
] {
match result {
Err(crate::Error::InvalidArgument(msg)) => {
assert!(
msg.contains("no zero-copy IOSurface mapping"),
"{name}: error must name the missing mapping: {msg}"
);
}
other => panic!("{name}: expected InvalidArgument, got {other:?}"),
}
}
let nv12 = Tensor::<u8>::image(
64,
8,
PixelFormat::Nv12,
Some(TensorMemory::Dma),
crate::CpuAccess::ReadWrite,
)
.expect("NV12 u8 keeps its designed L008 IOSurface mapping");
assert_eq!(nv12.memory(), TensorMemory::Dma);
}
#[test]
fn surface_id_is_nonzero() {
let t = IoSurfaceTensor::<u8>::new(&[64], None).expect("alloc");
assert!(t.surface_id() != 0, "IOSurface IDs should be nonzero");
}
#[test]
fn shape_reshape_roundtrip() {
let mut t = IoSurfaceTensor::<u8>::new(&[16, 16], None).expect("alloc");
assert_eq!(t.shape(), &[16, 16]);
t.reshape(&[256]).expect("flatten");
assert_eq!(t.shape(), &[256]);
assert!(t.reshape(&[100]).is_err());
}
#[test]
fn fourcc_layout_u8_packed_unchanged() {
assert_eq!(
image_fourcc_and_bpe(PixelFormat::Rgba, DType::U8),
Some((u32::from_be_bytes(*b"RGBA"), 4))
);
assert_eq!(
image_fourcc_and_bpe(PixelFormat::Bgra, DType::U8),
Some((u32::from_be_bytes(*b"BGRA"), 4))
);
assert_eq!(
image_fourcc_and_bpe(PixelFormat::Yuyv, DType::U8),
Some((u32::from_be_bytes(*b"2C08"), 2))
);
assert_eq!(
image_fourcc_and_bpe(PixelFormat::Rgb, DType::U8),
Some((u32::from_be_bytes(*b"RGBA"), 4))
);
}
#[test]
fn fourcc_layout_f16_packed_rgba16f() {
let expected = (u32::from_be_bytes(*b"RGhA"), 8);
assert_eq!(
image_fourcc_and_bpe(PixelFormat::Rgba, DType::F16),
Some(expected)
);
assert_eq!(
image_fourcc_and_bpe(PixelFormat::PlanarRgb, DType::F16),
Some(expected)
);
assert_eq!(
image_fourcc_and_bpe(PixelFormat::PlanarRgba, DType::F16),
Some(expected)
);
}
#[test]
fn fourcc_layout_unsupported_combinations_return_none() {
assert_eq!(
image_fourcc_and_bpe(PixelFormat::PlanarRgb, DType::U8),
None
);
assert_eq!(
image_fourcc_and_bpe(PixelFormat::PlanarRgba, DType::U8),
None
);
assert_eq!(image_fourcc_and_bpe(PixelFormat::Rgba, DType::F32), None);
assert_eq!(
image_fourcc_and_bpe(PixelFormat::PlanarRgb, DType::F32),
None
);
assert_eq!(
image_fourcc_and_bpe(PixelFormat::Nv12, DType::U8),
Some((u32::from_be_bytes(*b"L008"), 1))
);
assert_eq!(image_fourcc_and_bpe(PixelFormat::Yuyv, DType::F16), None);
assert_eq!(image_fourcc_and_bpe(PixelFormat::Grey, DType::F16), None);
}
#[test]
fn new_image_planar_f16_packs_into_rgba16f() {
let shape = [3, 32, 64];
let t = IoSurfaceTensor::<half::f16>::new_image(
64,
32,
PixelFormat::PlanarRgb,
DType::F16,
&shape,
None,
)
.expect("PlanarRgb F16 IOSurface allocation");
assert!(t.buf_size >= 3 * 32 * 64 * 2);
}
#[test]
fn new_image_planar_f16_rejects_misaligned_width() {
let shape = [3, 32, 63];
let err = IoSurfaceTensor::<half::f16>::new_image(
63,
32,
PixelFormat::PlanarRgb,
DType::F16,
&shape,
None,
)
.expect_err("misaligned width must be rejected");
match err {
Error::InvalidShape(msg) => assert!(
msg.contains("width%4==0") || msg.contains("RGBA16F"),
"unexpected message: {msg}"
),
other => panic!("expected InvalidShape, got {other:?}"),
}
}
#[test]
fn new_image_packed_rgba_f16_alloc_size_matches_dtype() {
let shape = [32, 64, 4];
let t = IoSurfaceTensor::<half::f16>::new_image(
64,
32,
PixelFormat::Rgba,
DType::F16,
&shape,
None,
)
.expect("Rgba F16 IOSurface allocation");
assert!(t.buf_size >= 32 * 64 * 8);
}
#[test]
fn from_surface_rejects_shape_overflowing_alloc() {
let src = IoSurfaceTensor::<u8>::new(&[64], None).expect("alloc");
let alloc = src.buf_size;
let surface_ref = src.surface.as_ptr();
let bad_shape = [alloc + 1];
let err = unsafe { IoSurfaceTensor::<u32>::from_surface(surface_ref, &bad_shape, None) }
.expect_err("oversized shape must be rejected");
match err {
Error::InvalidShape(msg) => assert!(
msg.contains("IOSurface only has"),
"unexpected message: {msg}"
),
other => panic!("expected InvalidShape, got {other:?}"),
}
let ok_shape = [alloc / std::mem::size_of::<u32>()];
unsafe { IoSurfaceTensor::<u32>::from_surface(surface_ref, &ok_shape, None) }
.expect("fitting shape should succeed");
}
#[test]
fn subview_iosurface_shares_identity_and_offsets_map() {
use crate::{Tensor, TensorTrait};
let parent = Tensor::<u8>::new(&[256], Some(TensorMemory::Dma), None).expect("alloc");
assert_eq!(parent.memory(), TensorMemory::Dma);
{
let mut m = parent.map().expect("map parent");
for (i, b) in m.as_mut_slice().iter_mut().take(256).enumerate() {
*b = (i & 0xff) as u8;
}
}
let view = parent
.subview(64, &[64])
.expect("iosurface subview must succeed");
assert_eq!(
view.buffer_identity().id(),
parent.buffer_identity().id(),
"subview shares the parent BufferIdentity (one GL import)"
);
let m = view.map().expect("map iosurface subview");
let s = m.as_slice();
assert_eq!(s.len(), 64, "window exposes exactly its logical length");
for (i, b) in s.iter().enumerate() {
assert_eq!(
*b,
((i + 64) & 0xff) as u8,
"view byte {i} must be parent[{}]",
i + 64
);
}
drop(m);
{
let mut mw = view.map().expect("remap for write");
mw.as_mut_slice()[0] = 0xAB;
}
let mp = parent.map().expect("remap parent");
assert_eq!(
mp.as_slice()[64],
0xAB,
"write through view is visible in parent"
);
}
}