pub mod colorimetry;
pub mod covguard;
mod cuda;
#[cfg(target_os = "linux")]
mod dma;
#[cfg(target_os = "linux")]
mod dmabuf;
mod error;
mod format;
#[cfg(target_os = "macos")]
mod iosurface;
mod mem;
mod pbo;
#[cfg(unix)]
mod shm;
mod tensor_dyn;
pub use colorimetry::{
ColorEncoding, ColorRange, ColorSpace, ColorTransfer, Colorimetry, MatrixWeights, RangeScaling,
};
#[cfg(all(coverage, target_os = "linux"))]
#[used]
#[link_section = ".init_array"]
static __EDGEFIRST_COV_INSTALL: extern "C" fn() = {
extern "C" fn ctor() {
crate::covguard::install();
}
ctor
};
#[cfg(target_os = "linux")]
pub(crate) use crate::dma::{DmaMap, DmaTensor};
#[cfg(target_os = "macos")]
pub use crate::iosurface::image_iosurface_layout;
#[cfg(target_os = "macos")]
pub(crate) use crate::iosurface::{IoSurfaceMap, IoSurfaceTensor};
pub(crate) use crate::mem::{MemMap, MemTensor};
pub use crate::pbo::{PboMap, PboMapping, PboOps, PboTensor};
#[cfg(unix)]
pub(crate) use crate::shm::{ShmMap, ShmTensor};
pub use cuda::{
gl_map_resource, gl_register_buffer, gl_unmap_resource, gl_unregister_resource,
is_cuda_available, memcpy_device_to_host, stream_create, stream_destroy, stream_synchronize,
CudaGlOps, CudaHandle, CudaMap, CudaStream,
};
pub use error::{Error, Result};
pub use format::{ChromaLayout, PixelFormat, PixelLayout};
use num_traits::Num;
use serde::{Deserialize, Serialize};
#[cfg(unix)]
use std::os::fd::OwnedFd;
use std::{
fmt,
ops::{Deref, DerefMut},
sync::{
atomic::{AtomicU64, Ordering},
Arc, Weak,
},
};
pub use tensor_dyn::TensorDyn;
pub type ForeignOwner = Box<dyn std::any::Any + Send + Sync>;
pub use half::f16;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PackedRgba16fLayout {
pub surface_w: usize,
pub surface_h: usize,
pub bytes_per_texel: usize,
pub pitch: usize,
}
pub fn packed_rgba16f_layout(
format: PixelFormat,
dtype: DType,
width: usize,
height: usize,
) -> Option<PackedRgba16fLayout> {
if dtype != DType::F16 {
return None;
}
let planes: usize = match format {
PixelFormat::PlanarRgb => 3,
PixelFormat::PlanarRgba => 4,
_ => return None,
};
if !width.is_multiple_of(4) {
return None;
}
let surface_w = width / 4;
let surface_h = planes.checked_mul(height)?;
let bytes_per_texel = 8;
let pitch = surface_w.checked_mul(bytes_per_texel)?;
Some(PackedRgba16fLayout {
surface_w,
surface_h,
bytes_per_texel,
pitch,
})
}
#[cfg(unix)]
pub struct PlaneDescriptor {
fd: OwnedFd,
stride: Option<usize>,
offset: Option<usize>,
}
#[cfg(unix)]
impl PlaneDescriptor {
pub fn new(fd: std::os::fd::BorrowedFd<'_>) -> Result<Self> {
let owned = fd.try_clone_to_owned()?;
Ok(Self {
fd: owned,
stride: None,
offset: None,
})
}
pub fn with_stride(mut self, stride: usize) -> Self {
self.stride = Some(stride);
self
}
pub fn with_offset(mut self, offset: usize) -> Self {
self.offset = Some(offset);
self
}
pub fn into_fd(self) -> OwnedFd {
self.fd
}
pub fn stride(&self) -> Option<usize> {
self.stride
}
pub fn offset(&self) -> Option<usize> {
self.offset
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Region {
pub x: usize,
pub y: usize,
pub width: usize,
pub height: usize,
}
impl Region {
pub fn new(x: usize, y: usize, width: usize, height: usize) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn fits_within(&self, width: usize, height: usize) -> bool {
self.x.saturating_add(self.width) <= width && self.y.saturating_add(self.height) <= height
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ViewOrigin {
pub parent_width: usize,
pub parent_height: usize,
pub parent_row_stride: usize,
pub x: usize,
pub y: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[repr(u8)]
#[non_exhaustive]
pub enum DType {
U8,
I8,
U16,
I16,
U32,
I32,
U64,
I64,
F16,
F32,
F64,
}
impl DType {
pub const fn size(&self) -> usize {
match self {
Self::U8 | Self::I8 => 1,
Self::U16 | Self::I16 | Self::F16 => 2,
Self::U32 | Self::I32 | Self::F32 => 4,
Self::U64 | Self::I64 | Self::F64 => 8,
}
}
pub const fn name(&self) -> &'static str {
match self {
Self::U8 => "u8",
Self::I8 => "i8",
Self::U16 => "u16",
Self::I16 => "i16",
Self::U32 => "u32",
Self::I32 => "i32",
Self::U64 => "u64",
Self::I64 => "i64",
Self::F16 => "f16",
Self::F32 => "f32",
Self::F64 => "f64",
}
}
}
impl fmt::Display for DType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
pub(crate) fn dtype_of<T: 'static>() -> Option<DType> {
use std::any::TypeId;
let id = TypeId::of::<T>();
if id == TypeId::of::<u8>() {
Some(DType::U8)
} else if id == TypeId::of::<i8>() {
Some(DType::I8)
} else if id == TypeId::of::<u16>() {
Some(DType::U16)
} else if id == TypeId::of::<i16>() {
Some(DType::I16)
} else if id == TypeId::of::<u32>() {
Some(DType::U32)
} else if id == TypeId::of::<i32>() {
Some(DType::I32)
} else if id == TypeId::of::<u64>() {
Some(DType::U64)
} else if id == TypeId::of::<i64>() {
Some(DType::I64)
} else if id == TypeId::of::<half::f16>() {
Some(DType::F16)
} else if id == TypeId::of::<f32>() {
Some(DType::F32)
} else if id == TypeId::of::<f64>() {
Some(DType::F64)
} else {
None
}
}
mod sealed {
pub trait Sealed {}
impl Sealed for u8 {}
impl Sealed for i8 {}
impl Sealed for u16 {}
impl Sealed for i16 {}
impl Sealed for u32 {}
impl Sealed for i32 {}
impl Sealed for u64 {}
impl Sealed for i64 {}
}
pub trait IntegerType: sealed::Sealed {}
impl IntegerType for u8 {}
impl IntegerType for i8 {}
impl IntegerType for u16 {}
impl IntegerType for i16 {}
impl IntegerType for u32 {}
impl IntegerType for i32 {}
impl IntegerType for u64 {}
impl IntegerType for i64 {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Quantization {
#[serde(deserialize_with = "deserialize_scalar_or_vec_f32")]
scale: Vec<f32>,
#[serde(
default,
deserialize_with = "deserialize_opt_scalar_or_vec_i32",
skip_serializing_if = "Option::is_none"
)]
zero_point: Option<Vec<i32>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
axis: Option<usize>,
}
#[derive(Debug, Clone, Copy)]
pub enum QuantMode<'a> {
PerTensorSymmetric {
scale: f32,
},
PerTensor {
scale: f32,
zero_point: i32,
},
PerChannelSymmetric {
scales: &'a [f32],
axis: usize,
},
PerChannel {
scales: &'a [f32],
zero_points: &'a [i32],
axis: usize,
},
}
impl Quantization {
pub fn per_tensor_symmetric(scale: f32) -> Self {
Self {
scale: vec![scale],
zero_point: None,
axis: None,
}
}
pub fn per_tensor(scale: f32, zero_point: i32) -> Self {
Self {
scale: vec![scale],
zero_point: Some(vec![zero_point]),
axis: None,
}
}
pub fn per_channel_symmetric(scales: Vec<f32>, axis: usize) -> Result<Self> {
if scales.is_empty() {
return Err(Error::QuantizationInvalid {
field: "scale.len",
expected: "non-empty per-channel scales".to_string(),
got: "length 0".to_string(),
});
}
Ok(Self {
scale: scales,
zero_point: None,
axis: Some(axis),
})
}
pub fn per_channel(scales: Vec<f32>, zero_points: Vec<i32>, axis: usize) -> Result<Self> {
if scales.is_empty() {
return Err(Error::QuantizationInvalid {
field: "scale.len",
expected: "non-empty per-channel scales".to_string(),
got: "length 0".to_string(),
});
}
if scales.len() != zero_points.len() {
return Err(Error::QuantizationInvalid {
field: "zero_point.len",
expected: format!("length matches scale ({})", scales.len()),
got: format!("length {}", zero_points.len()),
});
}
Ok(Self {
scale: scales,
zero_point: Some(zero_points),
axis: Some(axis),
})
}
pub fn mode(&self) -> QuantMode<'_> {
match (self.scale.len(), self.zero_point.as_deref(), self.axis) {
(1, None, _) => QuantMode::PerTensorSymmetric {
scale: self.scale[0],
},
(1, Some(zps), _) => QuantMode::PerTensor {
scale: self.scale[0],
zero_point: zps.first().copied().unwrap_or(0),
},
(_, None, Some(axis)) => QuantMode::PerChannelSymmetric {
scales: &self.scale,
axis,
},
(_, Some(zps), Some(axis)) => QuantMode::PerChannel {
scales: &self.scale,
zero_points: zps,
axis,
},
_ => {
debug_assert!(
false,
"Quantization::mode: per-channel without axis is unreachable"
);
QuantMode::PerTensorSymmetric {
scale: self.scale.first().copied().unwrap_or(1.0),
}
}
}
}
pub fn is_per_tensor(&self) -> bool {
self.scale.len() == 1
}
pub fn is_per_channel(&self) -> bool {
self.scale.len() > 1
}
pub fn is_symmetric(&self) -> bool {
match &self.zero_point {
None => true,
Some(zps) => zps.iter().all(|&z| z == 0),
}
}
pub fn scale(&self) -> &[f32] {
&self.scale
}
pub fn zero_point(&self) -> Option<&[i32]> {
self.zero_point.as_deref()
}
pub fn axis(&self) -> Option<usize> {
self.axis
}
pub(crate) fn validate(&self, shape: &[usize]) -> Result<()> {
if self.scale.is_empty() {
return Err(Error::QuantizationInvalid {
field: "scale.len",
expected: ">= 1".to_string(),
got: "0".to_string(),
});
}
if let Some(zps) = self.zero_point.as_ref() {
let expected = if self.scale.len() == 1 {
1
} else {
self.scale.len()
};
if zps.len() != expected {
return Err(Error::QuantizationInvalid {
field: "zero_point.len",
expected: format!(
"{expected} (matching {})",
if self.scale.len() == 1 {
"per-tensor scale"
} else {
"per-channel scale.len"
}
),
got: format!("length {}", zps.len()),
});
}
}
match (self.scale.len(), self.axis) {
(1, None) => Ok(()),
(1, Some(_)) => Err(Error::QuantizationInvalid {
field: "per_tensor_redundant_axis",
expected: "axis=None for per-tensor quantization".to_string(),
got: format!("axis={:?}", self.axis),
}),
(_, None) => Err(Error::QuantizationInvalid {
field: "per_channel_requires_axis",
expected: format!(
"axis=Some(_) for per-channel quantization (scale.len={})",
self.scale.len()
),
got: "axis=None".to_string(),
}),
(n, Some(axis)) => {
if axis >= shape.len() {
return Err(Error::QuantizationInvalid {
field: "axis",
expected: format!("axis < tensor rank ({})", shape.len()),
got: format!("axis={axis}"),
});
}
if shape[axis] != n {
return Err(Error::QuantizationInvalid {
field: "scale.len",
expected: format!("length matches shape[{axis}] ({})", shape[axis]),
got: format!("length {n}"),
});
}
Ok(())
}
}
}
}
impl From<(f32, i32)> for Quantization {
fn from((scale, zero_point): (f32, i32)) -> Self {
Self::per_tensor(scale, zero_point)
}
}
fn deserialize_scalar_or_vec_f32<'de, D: serde::Deserializer<'de>>(
de: D,
) -> std::result::Result<Vec<f32>, D::Error> {
use serde::de::{self, Visitor};
struct V;
impl<'de> Visitor<'de> for V {
type Value = Vec<f32>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("f32 or array of f32")
}
fn visit_f64<E: de::Error>(self, v: f64) -> std::result::Result<Self::Value, E> {
Ok(vec![v as f32])
}
#[allow(clippy::cast_possible_truncation)]
fn visit_i64<E: de::Error>(self, v: i64) -> std::result::Result<Self::Value, E> {
Ok(vec![v as f32])
}
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
fn visit_u64<E: de::Error>(self, v: u64) -> std::result::Result<Self::Value, E> {
Ok(vec![v as f32])
}
fn visit_seq<A: de::SeqAccess<'de>>(
self,
mut seq: A,
) -> std::result::Result<Self::Value, A::Error> {
let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(1));
while let Some(x) = seq.next_element::<f32>()? {
out.push(x);
}
Ok(out)
}
}
de.deserialize_any(V)
}
fn deserialize_opt_scalar_or_vec_i32<'de, D: serde::Deserializer<'de>>(
de: D,
) -> std::result::Result<Option<Vec<i32>>, D::Error> {
use serde::de::{self, Visitor};
struct V;
impl<'de> Visitor<'de> for V {
type Value = Option<Vec<i32>>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("null, i32, or array of i32")
}
fn visit_none<E: de::Error>(self) -> std::result::Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> std::result::Result<Self::Value, E> {
Ok(None)
}
fn visit_some<D2: serde::Deserializer<'de>>(
self,
de: D2,
) -> std::result::Result<Self::Value, D2::Error> {
struct Inner;
impl<'de> Visitor<'de> for Inner {
type Value = Vec<i32>;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("i32 or array of i32")
}
#[allow(clippy::cast_possible_truncation)]
fn visit_i64<E: de::Error>(self, v: i64) -> std::result::Result<Self::Value, E> {
Ok(vec![v as i32])
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
fn visit_u64<E: de::Error>(self, v: u64) -> std::result::Result<Self::Value, E> {
Ok(vec![v as i32])
}
fn visit_seq<A: de::SeqAccess<'de>>(
self,
mut seq: A,
) -> std::result::Result<Self::Value, A::Error> {
let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(1));
while let Some(x) = seq.next_element::<i32>()? {
out.push(x);
}
Ok(out)
}
}
de.deserialize_any(Inner).map(Some)
}
#[allow(clippy::cast_possible_truncation)]
fn visit_i64<E: de::Error>(self, v: i64) -> std::result::Result<Self::Value, E> {
Ok(Some(vec![v as i32]))
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
fn visit_u64<E: de::Error>(self, v: u64) -> std::result::Result<Self::Value, E> {
Ok(Some(vec![v as i32]))
}
fn visit_seq<A: de::SeqAccess<'de>>(
self,
mut seq: A,
) -> std::result::Result<Self::Value, A::Error> {
let mut out = Vec::with_capacity(seq.size_hint().unwrap_or(1));
while let Some(x) = seq.next_element::<i32>()? {
out.push(x);
}
Ok(Some(out))
}
}
de.deserialize_option(V)
}
static NEXT_BUFFER_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone)]
pub struct BufferIdentity {
id: u64,
guard: Arc<()>,
}
impl BufferIdentity {
pub fn new() -> Self {
Self {
id: NEXT_BUFFER_ID.fetch_add(1, Ordering::Relaxed),
guard: Arc::new(()),
}
}
pub fn id(&self) -> u64 {
self.id
}
pub fn weak(&self) -> Weak<()> {
Arc::downgrade(&self.guard)
}
}
impl Default for BufferIdentity {
fn default() -> Self {
Self::new()
}
}
#[cfg(target_os = "linux")]
use nix::sys::stat::{major, minor};
pub trait TensorTrait<T>: Send + Sync
where
T: Num + Clone + fmt::Debug,
{
fn new(shape: &[usize], name: Option<&str>) -> Result<Self>
where
Self: Sized;
#[cfg(unix)]
fn from_fd(fd: std::os::fd::OwnedFd, shape: &[usize], name: Option<&str>) -> Result<Self>
where
Self: Sized;
#[cfg(unix)]
fn clone_fd(&self) -> Result<std::os::fd::OwnedFd>;
fn memory(&self) -> TensorMemory;
fn name(&self) -> String;
fn len(&self) -> usize {
self.shape().iter().product()
}
fn is_empty(&self) -> bool {
self.len() == 0
}
fn size(&self) -> usize {
self.len() * std::mem::size_of::<T>()
}
fn shape(&self) -> &[usize];
fn reshape(&mut self, shape: &[usize]) -> Result<()>;
fn capacity_bytes(&self) -> usize {
self.size()
}
fn set_logical_shape(&mut self, shape: &[usize]) -> Result<()> {
self.reshape(shape)
}
fn map(&self) -> Result<TensorMap<T>>;
fn buffer_identity(&self) -> &BufferIdentity;
fn view(&self, offset_bytes: usize, shape: &[usize]) -> Result<Self>
where
Self: Sized,
{
let _ = (offset_bytes, shape);
Err(Error::NotImplemented(
"view (zero-copy sub-region) is not supported for this tensor backend".to_owned(),
))
}
}
pub trait TensorMapTrait<T>
where
T: Num + Clone + fmt::Debug,
{
fn shape(&self) -> &[usize];
fn unmap(&mut self);
fn len(&self) -> usize {
self.shape().iter().product()
}
fn is_empty(&self) -> bool {
self.len() == 0
}
fn size(&self) -> usize {
self.len() * std::mem::size_of::<T>()
}
fn as_slice(&self) -> &[T];
fn as_mut_slice(&mut self) -> &mut [T];
#[cfg(feature = "ndarray")]
fn view(&'_ self) -> Result<ndarray::ArrayView<'_, T, ndarray::Dim<ndarray::IxDynImpl>>> {
Ok(ndarray::ArrayView::from_shape(
self.shape(),
self.as_slice(),
)?)
}
#[cfg(feature = "ndarray")]
fn view_mut(
&'_ mut self,
) -> Result<ndarray::ArrayViewMut<'_, T, ndarray::Dim<ndarray::IxDynImpl>>> {
let shape = self.shape().to_vec();
Ok(ndarray::ArrayViewMut::from_shape(
shape,
self.as_mut_slice(),
)?)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TensorMemory {
Dma,
#[cfg(unix)]
Shm,
Mem,
Pbo,
}
impl From<TensorMemory> for String {
fn from(memory: TensorMemory) -> Self {
match memory {
TensorMemory::Dma => "dma".to_owned(),
#[cfg(unix)]
TensorMemory::Shm => "shm".to_owned(),
TensorMemory::Mem => "mem".to_owned(),
TensorMemory::Pbo => "pbo".to_owned(),
}
}
}
impl TryFrom<&str> for TensorMemory {
type Error = Error;
fn try_from(s: &str) -> Result<Self> {
match s {
"dma" => Ok(TensorMemory::Dma),
#[cfg(unix)]
"shm" => Ok(TensorMemory::Shm),
"mem" => Ok(TensorMemory::Mem),
"pbo" => Ok(TensorMemory::Pbo),
_ => Err(Error::InvalidMemoryType(s.to_owned())),
}
}
}
#[derive(Debug)]
#[allow(dead_code)] pub(crate) enum TensorStorage<T>
where
T: Num + Clone + fmt::Debug + Send + Sync,
{
#[cfg(target_os = "linux")]
Dma(DmaTensor<T>),
#[cfg(target_os = "macos")]
Dma(IoSurfaceTensor<T>),
#[cfg(unix)]
Shm(ShmTensor<T>),
Mem(MemTensor<T>),
Pbo(PboTensor<T>),
}
impl<T> TensorStorage<T>
where
T: Num + Clone + fmt::Debug + Send + Sync,
{
pub(crate) fn backing_row_stride(&self) -> Option<usize> {
match self {
#[cfg(target_os = "macos")]
TensorStorage::Dma(t) => t.image_backing_row_stride(),
_ => None,
}
}
fn new(shape: &[usize], memory: Option<TensorMemory>, name: Option<&str>) -> Result<Self> {
match memory {
#[cfg(target_os = "linux")]
Some(TensorMemory::Dma) => {
DmaTensor::<T>::new(shape, name).map(TensorStorage::Dma)
}
#[cfg(target_os = "macos")]
Some(TensorMemory::Dma) => {
IoSurfaceTensor::<T>::new(shape, name).map(TensorStorage::Dma)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
Some(TensorMemory::Dma) => Err(crate::error::Error::NotImplemented(
"TensorMemory::Dma is only available on Linux (DMA-BUF) and macOS (IOSurface)"
.to_owned(),
)),
#[cfg(unix)]
Some(TensorMemory::Shm) => {
ShmTensor::<T>::new(shape, name).map(TensorStorage::Shm)
}
Some(TensorMemory::Mem) => {
MemTensor::<T>::new(shape, name).map(TensorStorage::Mem)
}
Some(TensorMemory::Pbo) => Err(crate::error::Error::NotImplemented(
"PboTensor cannot be created via Tensor::new() — use ImageProcessor::create_image()".to_owned(),
)),
None => {
if std::env::var("EDGEFIRST_TENSOR_FORCE_MEM")
.is_ok_and(|x| x != "0" && x.to_lowercase() != "false")
{
MemTensor::<T>::new(shape, name).map(TensorStorage::Mem)
} else {
#[cfg(target_os = "linux")]
{
match DmaTensor::<T>::new(shape, name) {
Ok(tensor) => Ok(TensorStorage::Dma(tensor)),
Err(_) => MemTensor::<T>::new(shape, name).map(TensorStorage::Mem),
}
}
#[cfg(target_os = "macos")]
{
match IoSurfaceTensor::<T>::new(shape, name) {
Ok(tensor) => Ok(TensorStorage::Dma(tensor)),
Err(_) => MemTensor::<T>::new(shape, name).map(TensorStorage::Mem),
}
}
#[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
{
MemTensor::<T>::new(shape, name).map(TensorStorage::Mem)
}
#[cfg(not(unix))]
{
MemTensor::<T>::new(shape, name).map(TensorStorage::Mem)
}
}
}
}
}
#[cfg(target_os = "linux")]
pub(crate) fn new_dma_with_byte_size(
shape: &[usize],
byte_size: usize,
name: Option<&str>,
) -> Result<Self> {
DmaTensor::<T>::new_with_byte_size(shape, byte_size, name).map(TensorStorage::Dma)
}
pub(crate) fn new_mem_with_byte_size(
shape: &[usize],
byte_size: usize,
name: Option<&str>,
) -> Result<Self>
where
T: 'static,
{
MemTensor::<T>::with_capacity_bytes(shape, byte_size, name).map(TensorStorage::Mem)
}
#[cfg(unix)]
pub(crate) fn new_shm_with_byte_size(
shape: &[usize],
byte_size: usize,
name: Option<&str>,
) -> Result<Self> {
ShmTensor::<T>::new_with_byte_size(shape, byte_size, name).map(TensorStorage::Shm)
}
#[cfg(target_os = "macos")]
pub(crate) fn new_image_iosurface(
width: usize,
height: usize,
format: PixelFormat,
dtype: DType,
shape: &[usize],
name: Option<&str>,
) -> Result<Self> {
IoSurfaceTensor::<T>::new_image(width, height, format, dtype, shape, name)
.map(TensorStorage::Dma)
}
#[cfg(unix)]
fn from_fd(fd: OwnedFd, shape: &[usize], name: Option<&str>) -> Result<Self> {
#[cfg(target_os = "linux")]
{
use nix::sys::stat::fstat;
let stat = fstat(&fd)?;
let major = major(stat.st_dev);
let minor = minor(stat.st_dev);
log::debug!("Creating tensor from fd: major={major}, minor={minor}");
if major != 0 {
return Err(Error::UnknownDeviceType(major, minor));
}
match minor {
9 | 10 => {
DmaTensor::<T>::from_fd(fd, shape, name).map(TensorStorage::Dma)
}
_ => {
ShmTensor::<T>::from_fd(fd, shape, name).map(TensorStorage::Shm)
}
}
}
#[cfg(all(unix, not(target_os = "linux")))]
{
ShmTensor::<T>::from_fd(fd, shape, name).map(TensorStorage::Shm)
}
}
}
impl<T> TensorTrait<T> for TensorStorage<T>
where
T: Num + Clone + fmt::Debug + Send + Sync,
{
fn new(shape: &[usize], name: Option<&str>) -> Result<Self> {
Self::new(shape, None, name)
}
#[cfg(unix)]
fn from_fd(fd: OwnedFd, shape: &[usize], name: Option<&str>) -> Result<Self> {
Self::from_fd(fd, shape, name)
}
#[cfg(unix)]
fn clone_fd(&self) -> Result<OwnedFd> {
match self {
TensorStorage::Dma(t) => t.clone_fd(),
TensorStorage::Shm(t) => t.clone_fd(),
TensorStorage::Mem(t) => t.clone_fd(),
TensorStorage::Pbo(t) => t.clone_fd(),
}
}
fn memory(&self) -> TensorMemory {
match self {
#[cfg(any(target_os = "linux", target_os = "macos"))]
TensorStorage::Dma(_) => TensorMemory::Dma,
#[cfg(unix)]
TensorStorage::Shm(_) => TensorMemory::Shm,
TensorStorage::Mem(_) => TensorMemory::Mem,
TensorStorage::Pbo(_) => TensorMemory::Pbo,
}
}
fn name(&self) -> String {
match self {
#[cfg(any(target_os = "linux", target_os = "macos"))]
TensorStorage::Dma(t) => t.name(),
#[cfg(unix)]
TensorStorage::Shm(t) => t.name(),
TensorStorage::Mem(t) => t.name(),
TensorStorage::Pbo(t) => t.name(),
}
}
fn shape(&self) -> &[usize] {
match self {
#[cfg(any(target_os = "linux", target_os = "macos"))]
TensorStorage::Dma(t) => t.shape(),
#[cfg(unix)]
TensorStorage::Shm(t) => t.shape(),
TensorStorage::Mem(t) => t.shape(),
TensorStorage::Pbo(t) => t.shape(),
}
}
fn reshape(&mut self, shape: &[usize]) -> Result<()> {
match self {
#[cfg(any(target_os = "linux", target_os = "macos"))]
TensorStorage::Dma(t) => t.reshape(shape),
#[cfg(unix)]
TensorStorage::Shm(t) => t.reshape(shape),
TensorStorage::Mem(t) => t.reshape(shape),
TensorStorage::Pbo(t) => t.reshape(shape),
}
}
fn capacity_bytes(&self) -> usize {
match self {
#[cfg(any(target_os = "linux", target_os = "macos"))]
TensorStorage::Dma(t) => t.capacity_bytes(),
#[cfg(unix)]
TensorStorage::Shm(t) => t.capacity_bytes(),
TensorStorage::Mem(t) => t.capacity_bytes(),
TensorStorage::Pbo(t) => t.capacity_bytes(),
}
}
fn set_logical_shape(&mut self, shape: &[usize]) -> Result<()> {
match self {
#[cfg(any(target_os = "linux", target_os = "macos"))]
TensorStorage::Dma(t) => t.set_logical_shape(shape),
#[cfg(unix)]
TensorStorage::Shm(t) => t.set_logical_shape(shape),
TensorStorage::Mem(t) => t.set_logical_shape(shape),
TensorStorage::Pbo(t) => t.set_logical_shape(shape),
}
}
fn map(&self) -> Result<TensorMap<T>> {
match self {
#[cfg(any(target_os = "linux", target_os = "macos"))]
TensorStorage::Dma(t) => t.map(),
#[cfg(unix)]
TensorStorage::Shm(t) => t.map(),
TensorStorage::Mem(t) => t.map(),
TensorStorage::Pbo(t) => t.map(),
}
}
fn buffer_identity(&self) -> &BufferIdentity {
match self {
#[cfg(any(target_os = "linux", target_os = "macos"))]
TensorStorage::Dma(t) => t.buffer_identity(),
#[cfg(unix)]
TensorStorage::Shm(t) => t.buffer_identity(),
TensorStorage::Mem(t) => t.buffer_identity(),
TensorStorage::Pbo(t) => t.buffer_identity(),
}
}
fn view(&self, offset_bytes: usize, shape: &[usize]) -> Result<Self> {
match self {
#[cfg(any(target_os = "linux", target_os = "macos"))]
TensorStorage::Dma(t) => t.view(offset_bytes, shape).map(TensorStorage::Dma),
#[cfg(unix)]
TensorStorage::Shm(t) => t.view(offset_bytes, shape).map(TensorStorage::Shm),
TensorStorage::Mem(t) => t.view(offset_bytes, shape).map(TensorStorage::Mem),
TensorStorage::Pbo(t) => t.view(offset_bytes, shape).map(TensorStorage::Pbo),
}
}
}
#[derive(Debug)]
pub struct Tensor<T>
where
T: Num + Clone + fmt::Debug + Send + Sync,
{
cuda: Option<crate::cuda::CudaHandle>,
pub(crate) storage: TensorStorage<T>,
format: Option<PixelFormat>,
chroma: Option<Box<Tensor<T>>>,
row_stride: Option<usize>,
plane_offset: Option<usize>,
pub(crate) quantization: Option<Quantization>,
colorimetry: Option<crate::Colorimetry>,
view_origin: Option<ViewOrigin>,
}
impl<T> Tensor<T>
where
T: Num + Clone + fmt::Debug + Send + Sync,
{
pub(crate) fn wrap(storage: TensorStorage<T>) -> Self {
Self {
storage,
format: None,
chroma: None,
row_stride: None,
plane_offset: None,
quantization: None,
cuda: None,
colorimetry: None,
view_origin: None,
}
}
pub fn from_slice(values: &[T], shape: &[usize]) -> Result<Self>
where
T: Copy,
{
let expected: usize = shape.iter().product();
if values.len() != expected {
return Err(Error::InvalidShape(format!(
"from_slice: values.len()={} but shape product={expected} (shape={shape:?})",
values.len()
)));
}
let t = Self::new(shape, Some(TensorMemory::Mem), None)?;
{
let mut m = t.map()?;
m.as_mut_slice().copy_from_slice(values);
}
Ok(t)
}
pub unsafe fn from_foreign(
ptr: *mut T,
shape: &[usize],
owner: Option<crate::ForeignOwner>,
name: Option<&str>,
) -> Result<Self> {
if shape.is_empty() {
return Err(Error::InvalidSize(0));
}
if ptr.is_null() {
return Err(Error::InvalidArgument(
"from_foreign: ptr must be non-null".to_owned(),
));
}
shape
.iter()
.copied()
.try_fold(1usize, |acc, dim| acc.checked_mul(dim))
.ok_or_else(|| {
Error::InvalidArgument(format!(
"from_foreign: shape.product() overflows usize (shape={shape:?})"
))
})?;
let mem = MemTensor::<T>::from_foreign(ptr, shape, owner, name);
Ok(Self::wrap(TensorStorage::Mem(mem)))
}
#[cfg(feature = "ndarray")]
pub fn from_arrayview3(view: ndarray::ArrayView3<'_, T>) -> Result<Self>
where
T: Copy,
{
let (h, w, c) = view.dim();
let t = Self::new(&[h, w, c], Some(TensorMemory::Mem), None)?;
{
let mut m = t.map()?;
let dst = m.as_mut_slice();
if let Some(src) = view.as_slice() {
dst.copy_from_slice(src);
} else {
for (d, &s) in dst.iter_mut().zip(view.iter()) {
*d = s;
}
}
}
Ok(t)
}
pub fn new(shape: &[usize], memory: Option<TensorMemory>, name: Option<&str>) -> Result<Self> {
let _span = tracing::trace_span!(
"tensor.alloc",
?shape,
memory = ?memory,
dtype = std::any::type_name::<T>(),
)
.entered();
#[cfg_attr(not(target_os = "linux"), allow(unused_mut))]
let mut t = TensorStorage::new(shape, memory, name).map(Self::wrap)?;
#[cfg(target_os = "linux")]
t.try_init_dma_cuda();
Ok(t)
}
pub fn image(
width: usize,
height: usize,
format: PixelFormat,
memory: Option<TensorMemory>,
) -> Result<Self>
where
T: 'static,
{
let shape = format.image_shape(width, height).ok_or_else(|| {
Error::InvalidArgument(format!(
"invalid dimensions {width}x{height} for format {format:?}"
))
})?;
#[cfg(target_os = "macos")]
if matches!(memory, Some(TensorMemory::Dma)) {
let natural_row_bytes = match format.layout() {
PixelLayout::Planar => width * std::mem::size_of::<T>(),
_ => width * format.channels() * std::mem::size_of::<T>(),
};
let has_image_fourcc = dtype_of::<T>()
.and_then(|dt| crate::iosurface::image_iosurface_layout(format, dt))
.is_some();
let padded_ok = has_image_fourcc && format.layout() != PixelLayout::Planar;
if !natural_row_bytes.is_multiple_of(64) && !padded_ok {
let elem_size = std::mem::size_of::<T>();
let per_pixel_bytes = match format.layout() {
PixelLayout::Planar => elem_size.max(1),
_ => format.channels().max(1) * elem_size.max(1),
};
let aligned_row_bytes = natural_row_bytes.next_multiple_of(64);
let pad_hint =
if per_pixel_bytes > 0 && aligned_row_bytes.is_multiple_of(per_pixel_bytes) {
let w = aligned_row_bytes / per_pixel_bytes;
format!("Pad width to {w} (the next 64-byte-aligned stride), ")
} else {
String::new()
};
return Err(Error::InvalidArgument(format!(
"Tensor::image: {format:?} {width}x{height} with element \
size {elem_size} produces a {natural_row_bytes}-byte natural \
row pitch, which is not 64-byte aligned. \
IOSurface rounds bytes_per_row up to 64 bytes, so a \
contiguous CPU map of this tensor would read garbage. \
{pad_hint}pass memory=None to auto-fall-back to SHM, or \
pass memory=Some(TensorMemory::Shm) or \
Some(TensorMemory::Mem) explicitly."
)));
}
if let Some(dtype) = dtype_of::<T>() {
if let Ok(storage) = TensorStorage::<T>::new_image_iosurface(
width, height, format, dtype, &shape, None,
) {
let mut t = Self::wrap(storage);
t.format = Some(format);
if let TensorStorage::Dma(ref io) = t.storage {
let bpr = io.bytes_per_row();
if let Some(natural) = t.effective_row_stride() {
if bpr > natural {
t.set_row_stride_unchecked(bpr);
}
}
}
return Ok(t);
}
}
}
let elem = std::mem::size_of::<T>();
let channels = format.channels();
let (natural_stride, total_rows) = match format.layout() {
PixelLayout::SemiPlanar => (width.next_multiple_of(2) * elem, shape[0]),
PixelLayout::Packed => (width * channels * elem, height),
PixelLayout::Planar => (width * elem, channels * height),
};
let aligned_stride = natural_stride.next_multiple_of(64);
let semi = format.layout() == PixelLayout::SemiPlanar;
let host_stride = if semi { aligned_stride } else { natural_stride };
let host_byte_size = host_stride * total_rows;
#[cfg(target_os = "linux")]
let dma_byte_size = aligned_stride * total_rows;
let (storage, used_stride) = match memory {
#[cfg(target_os = "linux")]
Some(TensorMemory::Dma) => (
TensorStorage::<T>::new_dma_with_byte_size(&shape, dma_byte_size, None)?,
aligned_stride,
),
#[cfg(unix)]
Some(TensorMemory::Shm) => (
TensorStorage::<T>::new_shm_with_byte_size(&shape, host_byte_size, None)?,
host_stride,
),
Some(TensorMemory::Mem) => (
TensorStorage::<T>::new_mem_with_byte_size(&shape, host_byte_size, None)?,
host_stride,
),
#[allow(unused_variables)]
Some(other) => {
return {
let mut t = Self::new(&shape, Some(other), None)?;
t.format = Some(format);
Ok(t)
};
}
None => {
#[cfg(target_os = "linux")]
{
match TensorStorage::<T>::new_dma_with_byte_size(&shape, dma_byte_size, None) {
Ok(s) => (s, aligned_stride),
Err(_) => (
TensorStorage::<T>::new_mem_with_byte_size(
&shape,
host_byte_size,
None,
)?,
host_stride,
),
}
}
#[cfg(not(target_os = "linux"))]
{
(
TensorStorage::<T>::new_mem_with_byte_size(&shape, host_byte_size, None)?,
host_stride,
)
}
}
};
let mut t = Self::wrap(storage);
t.format = Some(format);
if semi || used_stride > natural_stride {
t.set_row_stride_unchecked(used_stride);
}
debug_assert!(
t.row_stride.is_some() || !semi,
"image() must always set row_stride for semi-planar tensors"
);
#[cfg(target_os = "linux")]
t.try_init_dma_cuda();
Ok(t)
}
pub fn image_with_stride(
width: usize,
height: usize,
format: PixelFormat,
row_stride_bytes: usize,
memory: Option<TensorMemory>,
) -> Result<Self> {
#[cfg(not(target_os = "linux"))]
{
let _ = (width, height, format, row_stride_bytes, memory);
Err(Error::NotImplemented(
"image_with_stride requires DMA support (Linux only)".to_owned(),
))
}
#[cfg(target_os = "linux")]
{
if format.layout() != PixelLayout::Packed {
return Err(Error::NotImplemented(format!(
"Tensor::image_with_stride only supports packed pixel layouts, got {format:?}"
)));
}
let elem = std::mem::size_of::<T>();
let min_stride = width
.checked_mul(format.channels())
.and_then(|p| p.checked_mul(elem))
.ok_or_else(|| {
Error::InvalidArgument(format!(
"image_with_stride: width {width} × channels {} × sizeof::<T>={elem} \
overflows usize",
format.channels()
))
})?;
if row_stride_bytes < min_stride {
return Err(Error::InvalidArgument(format!(
"image_with_stride: row_stride {row_stride_bytes} < minimum {min_stride} \
({width} px × {} ch × {elem} B)",
format.channels()
)));
}
let total_byte_size = row_stride_bytes.checked_mul(height).ok_or_else(|| {
Error::InvalidArgument(format!(
"image_with_stride: row_stride {row_stride_bytes} × height {height} overflows usize"
))
})?;
let shape = vec![height, width, format.channels()];
let storage = match memory {
Some(TensorMemory::Dma) | None => {
TensorStorage::<T>::new_dma_with_byte_size(&shape, total_byte_size, None)?
}
Some(other) => {
return Err(Error::NotImplemented(format!(
"image_with_stride: only TensorMemory::Dma is supported, got {other:?}"
)));
}
};
let mut t = Self::wrap(storage);
t.format = Some(format);
t.row_stride = Some(row_stride_bytes);
t.try_init_dma_cuda();
Ok(t)
}
}
pub fn set_format(&mut self, format: PixelFormat) -> Result<()> {
let shape = self.shape();
match format.layout() {
PixelLayout::Packed => {
if shape.len() != 3 || shape[2] != format.channels() {
return Err(Error::InvalidShape(format!(
"packed format {format:?} expects [H, W, {}], got {shape:?}",
format.channels()
)));
}
}
PixelLayout::Planar => {
if shape.len() != 3 || shape[0] != format.channels() {
return Err(Error::InvalidShape(format!(
"planar format {format:?} expects [{}, H, W], got {shape:?}",
format.channels()
)));
}
}
PixelLayout::SemiPlanar => {
if shape.len() != 2 {
return Err(Error::InvalidShape(format!(
"semi-planar format {format:?} expects [H*k, W], got {shape:?}"
)));
}
match format {
PixelFormat::Nv12 if shape[0] % 3 == 1 => {
return Err(Error::InvalidShape(format!(
"NV12 contiguous shape[0] must be H + ceil(H/2) for some height; \
{} is unreachable (≡ 1 mod 3)",
shape[0]
)));
}
PixelFormat::Nv16 if !shape[0].is_multiple_of(2) => {
return Err(Error::InvalidShape(format!(
"NV16 contiguous shape[0] must be even, got {}",
shape[0]
)));
}
PixelFormat::Nv24 if !shape[0].is_multiple_of(3) => {
return Err(Error::InvalidShape(format!(
"NV24 contiguous shape[0] must be a multiple of 3 (= 3H), got {}",
shape[0]
)));
}
_ => {}
}
}
}
if self.format != Some(format) {
self.row_stride = None;
self.plane_offset = None;
match self.storage {
TensorStorage::Mem(ref mut m) => m.set_offset(0),
#[cfg(target_os = "linux")]
TensorStorage::Dma(ref mut dma) => dma.mmap_offset = 0,
_ => {}
}
}
self.format = Some(format);
Ok(())
}
pub fn configure_image(
&mut self,
width: usize,
height: usize,
format: PixelFormat,
) -> Result<()> {
let shape = format.image_shape(width, height).ok_or_else(|| {
Error::InvalidArgument(format!(
"invalid dimensions {width}x{height} for format {format:?}"
))
})?;
let prior_stride = self.row_stride;
self.storage.set_logical_shape(&shape)?;
self.set_format(format)?;
let elem = std::mem::size_of::<T>();
let channels = format.channels();
let (min_stride, total_rows) = match format.layout() {
PixelLayout::SemiPlanar => (width.next_multiple_of(2) * elem, shape[0]),
PixelLayout::Packed => (width * channels * elem, height),
PixelLayout::Planar => (width * elem, channels * height),
};
let needs_align = self.storage.memory() == TensorMemory::Dma
|| format.layout() == PixelLayout::SemiPlanar;
let active_stride = if let Some(pitch) = self.storage.backing_row_stride() {
let natural = self.effective_row_stride().unwrap_or(0);
if pitch > natural {
self.set_row_stride_unchecked(pitch);
pitch
} else {
natural
}
} else if needs_align {
let aligned = min_stride.next_multiple_of(64);
let capacity = self.storage.capacity_bytes();
let candidate = if let Some(ps) = prior_stride {
if ps >= min_stride && ps % 64 == 0 && ps * total_rows <= capacity {
ps
} else {
aligned
}
} else {
aligned
};
if candidate * total_rows <= capacity {
self.set_row_stride_unchecked(candidate);
candidate
} else {
self.effective_row_stride().unwrap_or(0)
}
} else {
self.effective_row_stride().unwrap_or(0)
};
if needs_align && active_stride > 0 {
let needed = active_stride * total_rows;
let capacity = self.storage.capacity_bytes();
if needed > capacity {
return Err(Error::InsufficientCapacity { needed, capacity });
}
}
Ok(())
}
pub fn image_with_capacity(
width: usize,
height: usize,
format: PixelFormat,
memory: Option<TensorMemory>,
) -> Result<Self>
where
T: 'static,
{
Self::image(width, height, format, memory)
}
pub fn format(&self) -> Option<PixelFormat> {
self.format
}
pub fn width(&self) -> Option<usize> {
let fmt = self.format?;
let shape = self.shape();
match fmt.layout() {
PixelLayout::Packed => Some(shape[1]),
PixelLayout::Planar => Some(shape[2]),
PixelLayout::SemiPlanar => Some(shape[1]),
}
}
pub fn height(&self) -> Option<usize> {
let fmt = self.format?;
let shape = self.shape();
match fmt.layout() {
PixelLayout::Packed => Some(shape[0]),
PixelLayout::Planar => Some(shape[1]),
PixelLayout::SemiPlanar => {
if self.is_multiplane() {
Some(shape[0])
} else {
match fmt {
PixelFormat::Nv12 => Some(shape[0] * 2 / 3),
PixelFormat::Nv16 => Some(shape[0] / 2),
PixelFormat::Nv24 => Some(shape[0] / 3),
_ => None,
}
}
}
}
}
pub fn from_planes(luma: Tensor<T>, chroma: Tensor<T>, format: PixelFormat) -> Result<Self> {
if format.layout() != PixelLayout::SemiPlanar {
return Err(Error::InvalidArgument(format!(
"from_planes requires a semi-planar format, got {format:?}"
)));
}
if chroma.format.is_some() || chroma.chroma.is_some() {
return Err(Error::InvalidArgument(
"chroma tensor must be a raw tensor (no format or chroma metadata)".into(),
));
}
let luma_shape = luma.shape();
let chroma_shape = chroma.shape();
if luma_shape.len() != 2 || chroma_shape.len() != 2 {
return Err(Error::InvalidArgument(format!(
"from_planes expects 2D shapes, got luma={luma_shape:?} chroma={chroma_shape:?}"
)));
}
if luma_shape[1] != chroma_shape[1] {
return Err(Error::InvalidArgument(format!(
"luma width {} != chroma width {}",
luma_shape[1], chroma_shape[1]
)));
}
match format {
PixelFormat::Nv12 => {
if luma_shape[0] % 2 != 0 {
return Err(Error::InvalidArgument(format!(
"NV12 requires even luma height, got {}",
luma_shape[0]
)));
}
if chroma_shape[0] != luma_shape[0] / 2 {
return Err(Error::InvalidArgument(format!(
"NV12 chroma height {} != luma height / 2 ({})",
chroma_shape[0],
luma_shape[0] / 2
)));
}
}
PixelFormat::Nv16 => {
if chroma_shape[0] != luma_shape[0] {
return Err(Error::InvalidArgument(format!(
"NV16 chroma height {} != luma height {}",
chroma_shape[0], luma_shape[0]
)));
}
}
_ => {
return Err(Error::InvalidArgument(format!(
"from_planes only supports NV12 and NV16 (NV24 multiplane not yet \
supported — use a contiguous NV24 tensor), got {format:?}"
)));
}
}
Ok(Tensor {
storage: luma.storage,
format: Some(format),
chroma: Some(Box::new(chroma)),
row_stride: luma.row_stride,
plane_offset: luma.plane_offset,
quantization: luma.quantization,
cuda: None,
colorimetry: luma.colorimetry,
view_origin: None,
})
}
pub fn is_multiplane(&self) -> bool {
self.chroma.is_some()
}
pub fn chroma(&self) -> Option<&Tensor<T>> {
self.chroma.as_deref()
}
pub fn chroma_mut(&mut self) -> Option<&mut Tensor<T>> {
self.chroma.as_deref_mut()
}
pub fn row_stride(&self) -> Option<usize> {
self.row_stride
}
pub fn effective_row_stride(&self) -> Option<usize> {
if let Some(s) = self.row_stride {
return Some(s);
}
let fmt = self.format?;
let w = self.width()?;
let elem = std::mem::size_of::<T>();
Some(match fmt.layout() {
PixelLayout::Packed => w * fmt.channels() * elem,
PixelLayout::Planar => w * elem,
PixelLayout::SemiPlanar => w.next_multiple_of(2) * elem,
})
}
pub fn set_row_stride(&mut self, stride: usize) -> Result<()> {
let fmt = self.format.ok_or_else(|| {
Error::InvalidArgument("cannot set row_stride without a pixel format".into())
})?;
let w = self.width().ok_or_else(|| {
Error::InvalidArgument("cannot determine width for row_stride validation".into())
})?;
let elem = std::mem::size_of::<T>();
let min_stride = match fmt.layout() {
PixelLayout::Packed => w * fmt.channels() * elem,
PixelLayout::Planar => w * elem,
PixelLayout::SemiPlanar => w.next_multiple_of(2) * elem,
};
if stride < min_stride {
return Err(Error::InvalidArgument(format!(
"row_stride {stride} < minimum {min_stride} for {fmt:?} at width {w}"
)));
}
self.row_stride = Some(stride);
Ok(())
}
pub fn set_row_stride_unchecked(&mut self, stride: usize) {
self.row_stride = Some(stride);
}
pub fn with_row_stride(mut self, stride: usize) -> Result<Self> {
self.set_row_stride(stride)?;
Ok(self)
}
pub fn plane_offset(&self) -> Option<usize> {
self.plane_offset
}
pub fn view_origin(&self) -> Option<ViewOrigin> {
self.view_origin
}
pub fn set_plane_offset(&mut self, offset: usize) {
self.plane_offset = Some(offset);
match self.storage {
TensorStorage::Mem(ref mut m) => m.set_offset(offset),
#[cfg(target_os = "linux")]
TensorStorage::Dma(ref mut dma) => dma.mmap_offset = offset,
_ => {}
}
}
pub fn colorimetry(&self) -> Option<crate::Colorimetry> {
self.colorimetry
}
pub fn set_colorimetry(&mut self, c: Option<crate::Colorimetry>) {
self.colorimetry = c;
}
pub fn with_colorimetry(mut self, c: crate::Colorimetry) -> Self {
self.colorimetry = Some(c);
self
}
pub(crate) fn subview(&self, offset_bytes: usize, shape: &[usize]) -> Result<Tensor<T>> {
let abs_offset = self
.plane_offset
.unwrap_or(0)
.checked_add(offset_bytes)
.ok_or(Error::InvalidSize(offset_bytes))?;
let mut t = Tensor::wrap(self.storage.view(offset_bytes, shape)?);
if let Some(fmt) = self.format {
t.set_format(fmt)?;
}
if let Some(rs) = self.row_stride {
t.set_row_stride_unchecked(rs);
}
t.quantization = self.quantization.clone();
t.set_colorimetry(self.colorimetry);
if abs_offset > 0 {
t.set_plane_offset(abs_offset);
}
Ok(t)
}
pub fn batch(&self, n: usize) -> Result<Tensor<T>> {
let shape = self.shape();
if let Some(fmt) = self.format {
let elem_rank = match fmt.layout() {
PixelLayout::SemiPlanar => 2,
_ => 3,
};
if shape.len() != elem_rank + 1 {
return Err(Error::InvalidShape(format!(
"batch(): tensor is not batched ({fmt:?} expects a leading N over a \
{elem_rank}-D element, got shape {shape:?})"
)));
}
}
let batch = *shape
.first()
.ok_or_else(|| Error::InvalidShape("batch(): empty shape".into()))?;
if n >= batch {
return Err(Error::BatchIndexOutOfBounds { index: n, batch });
}
let elem_shape: Vec<usize> = shape[1..].to_vec();
let elem_count: usize = elem_shape.iter().product();
let elem_bytes = elem_count
.checked_mul(std::mem::size_of::<T>())
.ok_or(Error::InvalidSize(elem_count))?;
let offset = n.checked_mul(elem_bytes).ok_or(Error::InvalidSize(n))?;
let view_origin = match self.format.map(|f| f.layout()) {
Some(PixelLayout::Packed) => {
let tile_h = elem_shape[0];
let tile_w = elem_shape[1];
let bpp = elem_shape[2] * std::mem::size_of::<T>();
let parent_stride = self.effective_row_stride().unwrap_or(tile_w * bpp);
Some(self.compose_view_origin(tile_w, batch * tile_h, parent_stride, 0, n * tile_h))
}
_ => None,
};
let mut t = self.subview(offset, &elem_shape)?;
t.view_origin = view_origin;
Ok(t)
}
pub fn view(&self, region: Region) -> Result<Tensor<T>> {
let fmt = self.format.ok_or_else(|| {
Error::InvalidOperation("view() requires a formatted image tensor".into())
})?;
if fmt.layout() != PixelLayout::Packed {
return Err(Error::InvalidOperation(format!(
"view() supports packed formats only (got {fmt:?}); use batch(n) for batched \
planar tensors"
)));
}
let w = self
.width()
.ok_or_else(|| Error::InvalidOperation("view(): tensor has no image width".into()))?;
let h = self
.height()
.ok_or_else(|| Error::InvalidOperation("view(): tensor has no image height".into()))?;
if !region.fits_within(w, h) {
return Err(Error::RegionOutOfBounds {
region,
bounds: (w, h),
});
}
let elem = std::mem::size_of::<T>();
let bpp = fmt.channels() * elem;
let stride = self.effective_row_stride().unwrap_or(w * bpp);
let offset = region
.y
.checked_mul(stride)
.and_then(|yo| yo.checked_add(region.x.checked_mul(bpp)?))
.ok_or(Error::InvalidSize(region.y))?;
let sub_shape = fmt
.image_shape(region.width, region.height)
.ok_or_else(|| Error::InvalidShape(format!("view(): invalid shape for {fmt:?}")))?;
let mut t = self.subview(offset, &sub_shape)?;
let view_stride = if region.height > 1 {
stride
} else {
region.width * bpp
};
t.set_row_stride_unchecked(view_stride);
t.view_origin = Some(self.compose_view_origin(w, h, stride, region.x, region.y));
Ok(t)
}
fn compose_view_origin(
&self,
parent_width: usize,
parent_height: usize,
parent_row_stride: usize,
x: usize,
y: usize,
) -> ViewOrigin {
match self.view_origin {
Some(root) => ViewOrigin {
parent_width: root.parent_width,
parent_height: root.parent_height,
parent_row_stride: root.parent_row_stride,
x: root.x.saturating_add(x),
y: root.y.saturating_add(y),
},
None => ViewOrigin {
parent_width,
parent_height,
parent_row_stride,
x,
y,
},
}
}
pub fn as_pbo(&self) -> Option<&PboTensor<T>> {
match &self.storage {
TensorStorage::Pbo(p) => Some(p),
_ => None,
}
}
#[cfg(target_os = "linux")]
pub fn as_dma(&self) -> Option<&DmaTensor<T>> {
match &self.storage {
TensorStorage::Dma(d) => Some(d),
_ => None,
}
}
#[cfg(target_os = "linux")]
pub fn dmabuf(&self) -> Result<std::os::fd::BorrowedFd<'_>> {
use std::os::fd::AsFd;
match &self.storage {
TensorStorage::Dma(dma) => Ok(dma.fd.as_fd()),
_ => Err(Error::NotImplemented(format!(
"dmabuf requires DMA-backed tensor, got {:?}",
self.storage.memory()
))),
}
}
pub fn from_pbo(pbo: PboTensor<T>) -> Self {
Self {
storage: TensorStorage::Pbo(pbo),
format: None,
chroma: None,
row_stride: None,
plane_offset: None,
quantization: None,
cuda: None,
colorimetry: None,
view_origin: None,
}
}
pub fn cuda(&self) -> Option<&crate::cuda::CudaHandle> {
self.cuda.as_ref()
}
pub fn set_cuda_handle(&mut self, h: crate::cuda::CudaHandle) {
self.cuda = Some(h);
}
pub fn cuda_map(&self) -> Option<crate::cuda::CudaMap<'_>> {
self.cuda.as_ref()?.map()
}
#[cfg(target_os = "linux")]
pub fn try_init_dma_cuda(&mut self) {
if self.cuda.is_some() || !crate::cuda::is_cuda_available() {
return;
}
let (raw_fd, buf_size) = match &self.storage {
TensorStorage::Dma(dma) => {
use std::os::fd::AsRawFd;
(dma.fd.as_raw_fd(), dma.buf_size)
}
_ => return,
};
if let Some((ext, dptr)) = crate::cuda::import_dma_fd(raw_fd, buf_size) {
self.cuda = Some(crate::cuda::CudaHandle::new_external(ext, dptr, buf_size));
}
}
}
impl<T> Tensor<T>
where
T: IntegerType + Num + Clone + fmt::Debug + Send + Sync,
{
pub fn quantization(&self) -> Option<&Quantization> {
self.quantization.as_ref()
}
pub fn set_quantization(&mut self, q: Quantization) -> Result<()> {
q.validate(self.shape())?;
self.quantization = Some(q);
Ok(())
}
pub fn with_quantization(mut self, q: Quantization) -> Result<Self> {
self.set_quantization(q)?;
Ok(self)
}
pub fn clear_quantization(&mut self) {
self.quantization = None;
}
}
impl<T> TensorTrait<T> for Tensor<T>
where
T: Num + Clone + fmt::Debug + Send + Sync,
{
fn new(shape: &[usize], name: Option<&str>) -> Result<Self>
where
Self: Sized,
{
Self::new(shape, None, name)
}
#[cfg(unix)]
fn from_fd(fd: std::os::fd::OwnedFd, shape: &[usize], name: Option<&str>) -> Result<Self>
where
Self: Sized,
{
#[cfg_attr(not(target_os = "linux"), allow(unused_mut))]
let mut t = Self::wrap(TensorStorage::from_fd(fd, shape, name)?);
#[cfg(target_os = "linux")]
t.try_init_dma_cuda();
Ok(t)
}
#[cfg(unix)]
fn clone_fd(&self) -> Result<std::os::fd::OwnedFd> {
self.storage.clone_fd()
}
fn memory(&self) -> TensorMemory {
self.storage.memory()
}
fn name(&self) -> String {
self.storage.name()
}
fn shape(&self) -> &[usize] {
self.storage.shape()
}
fn reshape(&mut self, shape: &[usize]) -> Result<()> {
if self.chroma.is_some() {
return Err(Error::InvalidOperation(
"cannot reshape a multiplane tensor — decompose planes first".into(),
));
}
self.storage.reshape(shape)?;
self.format = None;
self.row_stride = None;
self.plane_offset = None;
match self.storage {
TensorStorage::Mem(ref mut m) => m.set_offset(0),
#[cfg(target_os = "linux")]
TensorStorage::Dma(ref mut dma) => dma.mmap_offset = 0,
_ => {}
}
Ok(())
}
fn map(&self) -> Result<TensorMap<T>> {
let _span = tracing::trace_span!(
"tensor.map",
memory = ?self.storage.memory(),
)
.entered();
if let Some(stride) = self.row_stride {
let rows = *self.shape().first().ok_or_else(|| {
Error::InvalidOperation(
"Tensor::map: strided mapping requires a non-empty shape".into(),
)
})?;
let total_bytes = stride.checked_mul(rows).ok_or_else(|| {
Error::InvalidOperation(format!(
"Tensor::map: row_stride {stride} × rows {rows} overflows usize"
))
})?;
match &self.storage {
#[cfg(target_os = "linux")]
TensorStorage::Dma(dma) if !dma.is_imported => {
let available_bytes = dma.buf_size.saturating_sub(dma.mmap_offset);
if total_bytes > available_bytes {
return Err(Error::InvalidOperation(format!(
"Tensor::map: strided mapping needs {total_bytes} bytes \
but DMA buffer only has {available_bytes} available \
(buf_size={}, mmap_offset={}, stride={stride}, rows={rows}); \
the row_stride was likely set larger than the original allocation",
dma.buf_size, dma.mmap_offset
)));
}
return dma.map_with_byte_size(total_bytes).map(TensorMap::Dma);
}
TensorStorage::Mem(mem) => {
let capacity = self.storage.capacity_bytes();
if total_bytes > capacity {
return Err(Error::InsufficientCapacity {
needed: total_bytes,
capacity,
});
}
return mem.map_with_byte_size(total_bytes);
}
#[cfg(unix)]
TensorStorage::Shm(shm) => {
let capacity = self.storage.capacity_bytes();
if total_bytes > capacity {
return Err(Error::InsufficientCapacity {
needed: total_bytes,
capacity,
});
}
return shm.map_with_byte_size(total_bytes);
}
#[cfg(target_os = "macos")]
TensorStorage::Dma(io) => {
let available = io.buf_size.saturating_sub(io.view_offset);
if total_bytes > available {
return Err(Error::InsufficientCapacity {
needed: total_bytes,
capacity: available,
});
}
return io.map_with_byte_size(total_bytes);
}
TensorStorage::Pbo(pbo) => {
let available = pbo.capacity_bytes().saturating_sub(pbo.view_offset);
if total_bytes > available {
return Err(Error::InsufficientCapacity {
needed: total_bytes,
capacity: available,
});
}
return pbo.map_with_byte_size(total_bytes);
}
#[allow(unreachable_patterns)]
_ => {
return Err(Error::InvalidOperation(
"CPU mapping of strided tensors is supported only for HAL-allocated \
Mem/Shm (any platform), self-allocated DMA (Linux), IOSurface \
(macOS), and PBO; imported DMA-BUF without self-allocation is \
GPU-path only"
.into(),
));
}
}
}
if self.plane_offset.is_some_and(|o| o > 0) {
let supported = matches!(self.storage, TensorStorage::Mem(_) | TensorStorage::Pbo(_));
#[cfg(any(target_os = "linux", target_os = "macos"))]
let supported = supported || matches!(self.storage, TensorStorage::Dma(_));
#[cfg(unix)]
let supported = supported || matches!(self.storage, TensorStorage::Shm(_));
if !supported {
return Err(Error::InvalidOperation(
"plane offset only supported for DMA, Mem, Shm, and PBO tensors".into(),
));
}
}
self.storage.map()
}
fn buffer_identity(&self) -> &BufferIdentity {
self.storage.buffer_identity()
}
}
pub enum TensorMap<T>
where
T: Num + Clone + fmt::Debug,
{
#[cfg(target_os = "linux")]
Dma(DmaMap<T>),
#[cfg(target_os = "macos")]
IoSurface(IoSurfaceMap<T>),
#[cfg(unix)]
Shm(ShmMap<T>),
Mem(MemMap<T>),
Pbo(PboMap<T>),
}
impl<T> TensorMapTrait<T> for TensorMap<T>
where
T: Num + Clone + fmt::Debug,
{
fn shape(&self) -> &[usize] {
match self {
#[cfg(target_os = "linux")]
TensorMap::Dma(map) => map.shape(),
#[cfg(target_os = "macos")]
TensorMap::IoSurface(map) => map.shape(),
#[cfg(unix)]
TensorMap::Shm(map) => map.shape(),
TensorMap::Mem(map) => map.shape(),
TensorMap::Pbo(map) => map.shape(),
}
}
fn unmap(&mut self) {
match self {
#[cfg(target_os = "linux")]
TensorMap::Dma(map) => map.unmap(),
#[cfg(target_os = "macos")]
TensorMap::IoSurface(map) => map.unmap(),
#[cfg(unix)]
TensorMap::Shm(map) => map.unmap(),
TensorMap::Mem(map) => map.unmap(),
TensorMap::Pbo(map) => map.unmap(),
}
}
fn as_slice(&self) -> &[T] {
match self {
#[cfg(target_os = "linux")]
TensorMap::Dma(map) => map.as_slice(),
#[cfg(target_os = "macos")]
TensorMap::IoSurface(map) => map.deref(),
#[cfg(unix)]
TensorMap::Shm(map) => map.as_slice(),
TensorMap::Mem(map) => map.as_slice(),
TensorMap::Pbo(map) => map.as_slice(),
}
}
fn as_mut_slice(&mut self) -> &mut [T] {
match self {
#[cfg(target_os = "linux")]
TensorMap::Dma(map) => map.as_mut_slice(),
#[cfg(target_os = "macos")]
TensorMap::IoSurface(map) => map.deref_mut(),
#[cfg(unix)]
TensorMap::Shm(map) => map.as_mut_slice(),
TensorMap::Mem(map) => map.as_mut_slice(),
TensorMap::Pbo(map) => map.as_mut_slice(),
}
}
}
impl<T> Deref for TensorMap<T>
where
T: Num + Clone + fmt::Debug,
{
type Target = [T];
fn deref(&self) -> &[T] {
match self {
#[cfg(target_os = "linux")]
TensorMap::Dma(map) => map.deref(),
#[cfg(target_os = "macos")]
TensorMap::IoSurface(map) => map.deref(),
#[cfg(unix)]
TensorMap::Shm(map) => map.deref(),
TensorMap::Mem(map) => map.deref(),
TensorMap::Pbo(map) => map.deref(),
}
}
}
impl<T> DerefMut for TensorMap<T>
where
T: Num + Clone + fmt::Debug,
{
fn deref_mut(&mut self) -> &mut [T] {
match self {
#[cfg(target_os = "linux")]
TensorMap::Dma(map) => map.deref_mut(),
#[cfg(target_os = "macos")]
TensorMap::IoSurface(map) => map.deref_mut(),
#[cfg(unix)]
TensorMap::Shm(map) => map.deref_mut(),
TensorMap::Mem(map) => map.deref_mut(),
TensorMap::Pbo(map) => map.deref_mut(),
}
}
}
#[cfg(target_os = "linux")]
static DMA_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
#[cfg(target_os = "macos")]
static IOSURFACE_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
#[cfg(target_os = "linux")]
pub fn is_dma_available() -> bool {
*DMA_AVAILABLE.get_or_init(|| Tensor::<u8>::new(&[64], Some(TensorMemory::Dma), None).is_ok())
}
#[cfg(not(target_os = "linux"))]
pub fn is_dma_available() -> bool {
false
}
#[cfg(target_os = "macos")]
pub fn is_iosurface_available() -> bool {
*IOSURFACE_AVAILABLE.get_or_init(|| {
Tensor::<u8>::new(&[64], Some(TensorMemory::Dma), None).is_ok()
})
}
#[cfg(not(target_os = "macos"))]
pub fn is_iosurface_available() -> bool {
false
}
pub fn is_gpu_buffer_available() -> bool {
#[cfg(target_os = "linux")]
{
is_dma_available()
}
#[cfg(target_os = "macos")]
{
is_iosurface_available()
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
false
}
}
#[cfg(unix)]
static SHM_AVAILABLE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
#[cfg(unix)]
pub fn is_shm_available() -> bool {
*SHM_AVAILABLE.get_or_init(|| Tensor::<u8>::new(&[64], Some(TensorMemory::Shm), None).is_ok())
}
#[cfg(not(unix))]
pub fn is_shm_available() -> bool {
false
}
#[cfg(test)]
mod dtype_tests {
use super::*;
#[test]
fn dtype_size() {
assert_eq!(DType::U8.size(), 1);
assert_eq!(DType::I8.size(), 1);
assert_eq!(DType::U16.size(), 2);
assert_eq!(DType::I16.size(), 2);
assert_eq!(DType::U32.size(), 4);
assert_eq!(DType::I32.size(), 4);
assert_eq!(DType::U64.size(), 8);
assert_eq!(DType::I64.size(), 8);
assert_eq!(DType::F16.size(), 2);
assert_eq!(DType::F32.size(), 4);
assert_eq!(DType::F64.size(), 8);
}
#[test]
fn dtype_name() {
assert_eq!(DType::U8.name(), "u8");
assert_eq!(DType::F16.name(), "f16");
assert_eq!(DType::F32.name(), "f32");
}
#[test]
fn dtype_serde_roundtrip() {
use serde_json;
let dt = DType::F16;
let json = serde_json::to_string(&dt).unwrap();
let back: DType = serde_json::from_str(&json).unwrap();
assert_eq!(dt, back);
}
}
#[cfg(test)]
mod image_tests {
use super::*;
#[test]
fn image_shape_per_layout() {
assert_eq!(
PixelFormat::Rgb.image_shape(640, 480),
Some(vec![480, 640, 3])
);
assert_eq!(
PixelFormat::Grey.image_shape(640, 480),
Some(vec![480, 640, 1])
);
assert_eq!(
PixelFormat::Nv12.image_shape(640, 480),
Some(vec![720, 640])
);
assert_eq!(
PixelFormat::Nv12.image_shape(640, 481),
Some(vec![722, 640])
);
assert_eq!(
PixelFormat::Nv12.image_shape(641, 480),
Some(vec![720, 641])
);
assert_eq!(
PixelFormat::Nv16.image_shape(641, 480),
Some(vec![960, 641])
);
assert_eq!(
PixelFormat::PlanarRgb.image_shape(640, 480),
Some(vec![3, 480, 640])
);
assert_eq!(
PixelFormat::Nv16.image_shape(640, 480),
Some(vec![960, 640])
);
}
#[test]
fn raw_tensor_has_no_format() {
let t = Tensor::<u8>::new(&[480, 640, 3], None, None).unwrap();
assert!(t.format().is_none());
assert!(t.width().is_none());
assert!(t.height().is_none());
assert!(!t.is_multiplane());
assert!(t.chroma().is_none());
}
#[test]
fn image_tensor_packed() {
let t = Tensor::<u8>::image(640, 480, PixelFormat::Rgba, None).unwrap();
assert_eq!(t.format(), Some(PixelFormat::Rgba));
assert_eq!(t.width(), Some(640));
assert_eq!(t.height(), Some(480));
assert_eq!(t.shape(), &[480, 640, 4]);
assert!(!t.is_multiplane());
}
#[test]
fn image_tensor_planar() {
let t = Tensor::<u8>::image(640, 480, PixelFormat::PlanarRgb, None).unwrap();
assert_eq!(t.format(), Some(PixelFormat::PlanarRgb));
assert_eq!(t.width(), Some(640));
assert_eq!(t.height(), Some(480));
assert_eq!(t.shape(), &[3, 480, 640]);
}
#[test]
#[cfg(target_os = "macos")]
fn image_tensor_dma_non_aligned_packed_width_pads_zero_copy() {
let t = Tensor::<u8>::image(4, 4, PixelFormat::Rgba, Some(TensorMemory::Dma))
.expect("padded RGBA IOSurface should allocate");
assert_eq!(t.format(), Some(PixelFormat::Rgba));
assert_eq!(t.width(), Some(4));
assert_eq!(t.height(), Some(4));
let stride = t.effective_row_stride().expect("stride");
assert_eq!(stride % 64, 0, "padded to 64-byte row alignment");
assert!(stride >= 16);
let m = t.map().expect("strided IOSurface map");
assert_eq!(m.as_slice().len(), stride * 4);
}
#[test]
#[cfg(target_os = "macos")]
fn image_tensor_dma_rejects_indivisible_pixel_pitch_without_pad_hint() {
let err = Tensor::<u8>::image(10, 10, PixelFormat::Rgb, Some(TensorMemory::Dma))
.expect_err("RGB u8 with 3 B/pixel and non-aligned width must be rejected");
match err {
Error::InvalidArgument(msg) => {
assert!(
msg.contains("64-byte aligned"),
"error must still name the alignment requirement: {msg}"
);
assert!(
!msg.contains("Pad width"),
"indivisible per-pixel pitch makes a width suggestion impossible; \
hint must be omitted, got: {msg}"
);
assert!(
msg.contains("memory=None") && msg.contains("TensorMemory::Mem"),
"error must still list the always-applicable alternatives: {msg}"
);
}
other => panic!("expected InvalidArgument, got {other:?}"),
}
}
#[test]
#[cfg(target_os = "macos")]
fn image_tensor_dma_planar_f16_alignment() {
let err =
Tensor::<half::f16>::image(16, 16, PixelFormat::PlanarRgb, Some(TensorMemory::Dma))
.expect_err("width=16 PlanarRgb F16 is 32-byte row, must reject");
assert!(matches!(err, Error::InvalidArgument(_)), "got {err:?}");
let t = Tensor::<half::f16>::image(32, 8, PixelFormat::PlanarRgb, Some(TensorMemory::Dma))
.expect("width=32 PlanarRgb F16 is 64-byte row, must succeed");
assert_eq!(t.format(), Some(PixelFormat::PlanarRgb));
}
#[test]
fn image_tensor_semi_planar_contiguous() {
let t = Tensor::<u8>::image(640, 480, PixelFormat::Nv12, None).unwrap();
assert_eq!(t.format(), Some(PixelFormat::Nv12));
assert_eq!(t.width(), Some(640));
assert_eq!(t.height(), Some(480));
assert_eq!(t.shape(), &[720, 640]);
assert!(!t.is_multiplane());
}
#[test]
#[cfg(target_os = "linux")]
fn image_tensor_with_stride_preserves_logical_width() {
if !is_dma_available() {
eprintln!("SKIPPED: DMA heap not available");
return;
}
let stride = 12032;
let t = Tensor::<u8>::image_with_stride(
3004,
1688,
PixelFormat::Rgba,
stride,
Some(TensorMemory::Dma),
)
.unwrap();
assert_eq!(t.width(), Some(3004));
assert_eq!(t.height(), Some(1688));
assert_eq!(t.shape(), &[1688, 3004, 4]);
assert_eq!(t.effective_row_stride(), Some(stride));
use crate::TensorMapTrait;
{
let map = t.map().unwrap();
assert!(
map.as_slice().len() >= stride * 1688,
"mapped buffer {} bytes < expected {}",
map.as_slice().len(),
stride * 1688
);
}
{
let mut map = t.map().unwrap();
let slice = map.as_mut_slice();
for y in 0..1688 {
let row_start = y * stride;
for x in 0..3004 {
let p = row_start + x * 4;
slice[p] = (y & 0xFF) as u8;
slice[p + 1] = (x & 0xFF) as u8;
slice[p + 2] = 0x42;
slice[p + 3] = 0xFF;
}
}
}
{
let map = t.map().unwrap();
let slice = map.as_slice();
assert_eq!(slice[0], 0x00);
assert_eq!(slice[1], 0x00);
assert_eq!(slice[2], 0x42);
assert_eq!(slice[3], 0xFF);
let mid = 100 * stride + 50 * 4;
assert_eq!(slice[mid], 100);
assert_eq!(slice[mid + 1], 50);
assert_eq!(slice[mid + 2], 0x42);
}
}
#[test]
#[cfg(target_os = "linux")]
fn image_tensor_with_stride_rejects_foreign_strided_map() {
if !is_dma_available() {
eprintln!("SKIPPED: DMA heap not available");
return;
}
let backing = Tensor::<u8>::new(&[240 * 320 * 4], Some(TensorMemory::Dma), None).unwrap();
let fd = backing.clone_fd().unwrap();
let shape = [240usize, 320, 4];
let storage = TensorStorage::<u8>::from_fd(fd, &shape, None).unwrap();
let mut t = Tensor::<u8>::wrap(storage);
t.set_format(PixelFormat::Bgra).unwrap();
t.set_row_stride(320 * 4).unwrap(); let err = t.map();
assert!(
matches!(err, Err(Error::InvalidOperation(_))),
"foreign strided map should error"
);
}
#[test]
#[cfg(target_os = "linux")]
fn image_tensor_with_stride_map_rejects_tampered_stride() {
if !is_dma_available() {
eprintln!("SKIPPED: DMA heap not available");
return;
}
let mut t = Tensor::<u8>::image_with_stride(
640,
480,
PixelFormat::Rgba,
3072,
Some(TensorMemory::Dma),
)
.unwrap();
t.set_row_stride(12288).unwrap();
let err = t.map();
assert!(
matches!(err, Err(Error::InvalidOperation(_))),
"map() with oversized stride must return InvalidOperation"
);
}
#[test]
fn dma_tensor_new_with_byte_size_rejects_shape_overflow() {
#[cfg(target_os = "linux")]
{
let err = crate::dma::DmaTensor::<u64>::new_with_byte_size(
&[usize::MAX, 2, 2],
usize::MAX,
None,
);
assert!(
matches!(err, Err(Error::InvalidArgument(_))),
"new_with_byte_size must detect shape.product() overflow"
);
}
}
#[test]
#[cfg(target_os = "linux")]
fn image_tensor_with_stride_rejects_too_small_stride() {
let err = Tensor::<u8>::image_with_stride(
640,
480,
PixelFormat::Rgba,
2400,
Some(TensorMemory::Dma),
);
assert!(matches!(err, Err(Error::InvalidArgument(_))));
}
#[test]
#[cfg(target_os = "linux")]
fn image_tensor_with_stride_rejects_non_packed() {
let err = Tensor::<u8>::image_with_stride(
640,
480,
PixelFormat::Nv12,
640,
Some(TensorMemory::Dma),
);
assert!(matches!(err, Err(Error::NotImplemented(_))));
}
#[test]
fn set_format_valid() {
let mut t = Tensor::<u8>::new(&[480, 640, 3], None, None).unwrap();
assert!(t.format().is_none());
t.set_format(PixelFormat::Rgb).unwrap();
assert_eq!(t.format(), Some(PixelFormat::Rgb));
assert_eq!(t.width(), Some(640));
assert_eq!(t.height(), Some(480));
}
#[test]
fn set_format_invalid_shape() {
let mut t = Tensor::<u8>::new(&[480, 640, 4], None, None).unwrap();
let err = t.set_format(PixelFormat::Rgb);
assert!(err.is_err());
assert!(t.format().is_none());
}
#[test]
fn reshape_clears_format() {
let mut t = Tensor::<u8>::image(640, 480, PixelFormat::Rgba, None).unwrap();
assert_eq!(t.format(), Some(PixelFormat::Rgba));
t.reshape(&[480 * 640 * 4]).unwrap();
assert!(t.format().is_none());
}
#[test]
fn from_planes_nv12() {
let y = Tensor::<u8>::new(&[480, 640], None, None).unwrap();
let uv = Tensor::<u8>::new(&[240, 640], None, None).unwrap();
let img = Tensor::from_planes(y, uv, PixelFormat::Nv12).unwrap();
assert_eq!(img.format(), Some(PixelFormat::Nv12));
assert!(img.is_multiplane());
assert!(img.chroma().is_some());
assert_eq!(img.width(), Some(640));
assert_eq!(img.height(), Some(480));
}
#[test]
fn from_planes_rejects_non_semiplanar() {
let y = Tensor::<u8>::new(&[480, 640], None, None).unwrap();
let uv = Tensor::<u8>::new(&[240, 640], None, None).unwrap();
let err = Tensor::from_planes(y, uv, PixelFormat::Rgb);
assert!(err.is_err());
}
#[test]
fn reshape_multiplane_errors() {
let y = Tensor::<u8>::new(&[480, 640], None, None).unwrap();
let uv = Tensor::<u8>::new(&[240, 640], None, None).unwrap();
let mut img = Tensor::from_planes(y, uv, PixelFormat::Nv12).unwrap();
let err = img.reshape(&[480 * 640 + 240 * 640]);
assert!(err.is_err());
}
}
#[cfg(test)]
mod tests {
#[cfg(target_os = "linux")]
use nix::unistd::{access, AccessFlags};
#[cfg(target_os = "linux")]
use std::io::Write as _;
use std::sync::RwLock;
use super::*;
#[ctor::ctor(unsafe)]
fn init() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
}
#[cfg(target_os = "linux")]
macro_rules! function {
() => {{
fn f() {}
fn type_name_of<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
let name = type_name_of(f);
match &name[..name.len() - 3].rfind(':') {
Some(pos) => &name[pos + 1..name.len() - 3],
None => &name[..name.len() - 3],
}
}};
}
#[test]
#[cfg(target_os = "linux")]
fn test_tensor() {
let _lock = FD_LOCK.read().unwrap();
let shape = vec![1];
let tensor = DmaTensor::<f32>::new(&shape, Some("dma_tensor"));
let dma_enabled = tensor.is_ok();
let tensor = Tensor::<f32>::new(&shape, None, None).expect("Failed to create tensor");
match dma_enabled {
true => assert_eq!(tensor.memory(), TensorMemory::Dma),
false => assert_eq!(tensor.memory(), TensorMemory::Mem),
}
}
#[test]
#[cfg(target_os = "macos")]
fn test_tensor() {
let shape = vec![1];
let tensor = Tensor::<f32>::new(&shape, None, None).expect("Failed to create tensor");
let m = tensor.memory();
assert!(
matches!(m, TensorMemory::Dma | TensorMemory::Mem),
"Unexpected auto-fallback result on macOS: {m:?}"
);
}
#[test]
#[cfg(all(unix, not(any(target_os = "linux", target_os = "macos"))))]
fn test_tensor() {
let shape = vec![1];
let tensor = Tensor::<f32>::new(&shape, None, None).expect("Failed to create tensor");
assert_eq!(tensor.memory(), TensorMemory::Mem);
}
#[test]
#[cfg(not(unix))]
fn test_tensor() {
let shape = vec![1];
let tensor = Tensor::<f32>::new(&shape, None, None).expect("Failed to create tensor");
assert_eq!(tensor.memory(), TensorMemory::Mem);
}
#[test]
#[cfg(target_os = "linux")]
fn test_dma_tensor() {
let _lock = FD_LOCK.read().unwrap();
match access(
"/dev/dma_heap/linux,cma",
AccessFlags::R_OK | AccessFlags::W_OK,
) {
Ok(_) => println!("/dev/dma_heap/linux,cma is available"),
Err(_) => match access(
"/dev/dma_heap/system",
AccessFlags::R_OK | AccessFlags::W_OK,
) {
Ok(_) => println!("/dev/dma_heap/system is available"),
Err(e) => {
writeln!(
&mut std::io::stdout(),
"[WARNING] DMA Heap is unavailable: {e}"
)
.unwrap();
return;
}
},
}
let shape = vec![2, 3, 4];
let tensor =
DmaTensor::<f32>::new(&shape, Some("test_tensor")).expect("Failed to create tensor");
const DUMMY_VALUE: f32 = 12.34;
assert_eq!(tensor.memory(), TensorMemory::Dma);
assert_eq!(tensor.name(), "test_tensor");
assert_eq!(tensor.shape(), &shape);
assert_eq!(tensor.size(), 2 * 3 * 4 * std::mem::size_of::<f32>());
assert_eq!(tensor.len(), 2 * 3 * 4);
{
let mut tensor_map = tensor.map().expect("Failed to map DMA memory");
tensor_map.fill(42.0);
assert!(tensor_map.iter().all(|&x| x == 42.0));
}
{
let shared = Tensor::<f32>::from_fd(
tensor
.clone_fd()
.expect("Failed to duplicate tensor file descriptor"),
&shape,
Some("test_tensor_shared"),
)
.expect("Failed to create tensor from fd");
assert_eq!(shared.memory(), TensorMemory::Dma);
assert_eq!(shared.name(), "test_tensor_shared");
assert_eq!(shared.shape(), &shape);
let mut tensor_map = shared.map().expect("Failed to map DMA memory from fd");
tensor_map.fill(DUMMY_VALUE);
assert!(tensor_map.iter().all(|&x| x == DUMMY_VALUE));
}
{
let tensor_map = tensor.map().expect("Failed to map DMA memory");
assert!(tensor_map.iter().all(|&x| x == DUMMY_VALUE));
}
let mut tensor = DmaTensor::<u8>::new(&shape, None).expect("Failed to create tensor");
assert_eq!(tensor.shape(), &shape);
let new_shape = vec![3, 4, 4];
assert!(
tensor.reshape(&new_shape).is_err(),
"Reshape should fail due to size mismatch"
);
assert_eq!(tensor.shape(), &shape, "Shape should remain unchanged");
let new_shape = vec![2, 3, 4];
tensor.reshape(&new_shape).expect("Reshape should succeed");
assert_eq!(
tensor.shape(),
&new_shape,
"Shape should be updated after successful reshape"
);
{
let mut tensor_map = tensor.map().expect("Failed to map DMA memory");
tensor_map.fill(1);
assert!(tensor_map.iter().all(|&x| x == 1));
}
{
let mut tensor_map = tensor.map().expect("Failed to map DMA memory");
tensor_map[2] = 42;
assert_eq!(tensor_map[1], 1, "Value at index 1 should be 1");
assert_eq!(tensor_map[2], 42, "Value at index 2 should be 42");
}
}
#[test]
#[cfg(unix)]
fn test_shm_tensor() {
let _lock = FD_LOCK.read().unwrap();
let shape = vec![2, 3, 4];
let tensor =
ShmTensor::<f32>::new(&shape, Some("test_tensor")).expect("Failed to create tensor");
assert_eq!(tensor.shape(), &shape);
assert_eq!(tensor.size(), 2 * 3 * 4 * std::mem::size_of::<f32>());
assert_eq!(tensor.name(), "test_tensor");
const DUMMY_VALUE: f32 = 12.34;
{
let mut tensor_map = tensor.map().expect("Failed to map shared memory");
tensor_map.fill(42.0);
assert!(tensor_map.iter().all(|&x| x == 42.0));
}
{
let shared = Tensor::<f32>::from_fd(
tensor
.clone_fd()
.expect("Failed to duplicate tensor file descriptor"),
&shape,
Some("test_tensor_shared"),
)
.expect("Failed to create tensor from fd");
assert_eq!(shared.memory(), TensorMemory::Shm);
assert_eq!(shared.name(), "test_tensor_shared");
assert_eq!(shared.shape(), &shape);
let mut tensor_map = shared.map().expect("Failed to map shared memory from fd");
tensor_map.fill(DUMMY_VALUE);
assert!(tensor_map.iter().all(|&x| x == DUMMY_VALUE));
}
{
let tensor_map = tensor.map().expect("Failed to map shared memory");
assert!(tensor_map.iter().all(|&x| x == DUMMY_VALUE));
}
let mut tensor = ShmTensor::<u8>::new(&shape, None).expect("Failed to create tensor");
assert_eq!(tensor.shape(), &shape);
let new_shape = vec![3, 4, 4];
assert!(
tensor.reshape(&new_shape).is_err(),
"Reshape should fail due to size mismatch"
);
assert_eq!(tensor.shape(), &shape, "Shape should remain unchanged");
let new_shape = vec![2, 3, 4];
tensor.reshape(&new_shape).expect("Reshape should succeed");
assert_eq!(
tensor.shape(),
&new_shape,
"Shape should be updated after successful reshape"
);
{
let mut tensor_map = tensor.map().expect("Failed to map shared memory");
tensor_map.fill(1);
assert!(tensor_map.iter().all(|&x| x == 1));
}
{
let mut tensor_map = tensor.map().expect("Failed to map shared memory");
tensor_map[2] = 42;
assert_eq!(tensor_map[1], 1, "Value at index 1 should be 1");
assert_eq!(tensor_map[2], 42, "Value at index 2 should be 42");
}
}
#[test]
fn mem_subview_partitions_parent_buffer() {
let parent = Tensor::<u8>::new(&[2, 4], Some(TensorMemory::Mem), None).unwrap();
let view0 = parent.subview(0, &[1, 4]).expect("subview at offset 0");
let view1 = parent.subview(4, &[1, 4]).expect("subview at offset 4");
view1
.map()
.unwrap()
.as_mut_slice()
.copy_from_slice(&[10, 20, 30, 40]);
view0
.map()
.unwrap()
.as_mut_slice()
.copy_from_slice(&[1, 2, 3, 4]);
assert_eq!(view0.map().unwrap().as_slice(), &[1, 2, 3, 4]);
assert_eq!(view1.map().unwrap().as_slice(), &[10, 20, 30, 40]);
assert_eq!(
parent.map().unwrap().as_slice(),
&[1, 2, 3, 4, 10, 20, 30, 40]
);
}
#[test]
fn batch_partitions_leading_dim() {
let parent = Tensor::<u8>::new(&[4, 2, 2, 3], Some(TensorMemory::Mem), None).unwrap();
for i in 0..4u8 {
let e = parent.batch(i as usize).expect("batch element");
assert_eq!(e.shape(), &[2, 2, 3]);
assert_eq!(e.buffer_identity().id(), parent.buffer_identity().id());
for b in e.map().unwrap().as_mut_slice() {
*b = i + 1;
}
}
let whole = parent.map().unwrap();
let s = whole.as_slice();
for i in 0..4usize {
assert!(
s[i * 12..(i + 1) * 12].iter().all(|&b| b == (i as u8 + 1)),
"band {i} not partitioned: {:?}",
&s[i * 12..(i + 1) * 12]
);
}
}
#[test]
fn view_origin_snapshots_parent_and_composes() {
let parent =
Tensor::<u8>::image(100, 80, PixelFormat::Rgba, Some(TensorMemory::Mem)).unwrap();
assert_eq!(
parent.view_origin(),
None,
"whole tensor has no view_origin"
);
let v = parent.view(Region::new(10, 20, 30, 40)).unwrap();
assert_eq!(
v.view_origin(),
Some(ViewOrigin {
parent_width: 100,
parent_height: 80,
parent_row_stride: 100 * 4, x: 10,
y: 20
})
);
let v2 = v.view(Region::new(5, 5, 10, 10)).unwrap();
assert_eq!(
v2.view_origin(),
Some(ViewOrigin {
parent_width: 100,
parent_height: 80,
parent_row_stride: 100 * 4,
x: 15,
y: 25
}),
"nested view composes onto the root parent"
);
}
#[test]
fn view_origin_none_for_raw_batch() {
let parent = Tensor::<u8>::new(&[4, 2, 2, 3], Some(TensorMemory::Mem), None).unwrap();
assert_eq!(parent.batch(2).unwrap().view_origin(), None);
}
#[test]
fn batch_rejects_out_of_bounds_index() {
let parent = Tensor::<u8>::new(&[4, 2, 2, 3], Some(TensorMemory::Mem), None).unwrap();
match parent.batch(4) {
Err(Error::BatchIndexOutOfBounds { index, batch }) => {
assert_eq!((index, batch), (4, 4));
}
other => panic!("expected BatchIndexOutOfBounds, got {other:?}"),
}
}
#[test]
fn batch_zero_on_unit_n_is_whole() {
let parent = Tensor::<u8>::new(&[1, 2, 2, 3], Some(TensorMemory::Mem), None).unwrap();
let e = parent.batch(0).unwrap();
assert_eq!(e.shape(), &[2, 2, 3]);
assert_eq!(e.plane_offset(), None);
assert_eq!(e.buffer_identity().id(), parent.buffer_identity().id());
}
#[test]
fn mem_subview_rejects_unaligned_offset() {
let parent = Tensor::<f32>::new(&[8], Some(TensorMemory::Mem), None).unwrap();
assert!(parent.subview(2, &[1]).is_err());
assert!(parent.subview(4, &[1]).is_ok());
}
#[test]
fn mem_subview_rejects_out_of_bounds() {
let parent = Tensor::<u8>::new(&[8], Some(TensorMemory::Mem), None).unwrap();
assert!(parent.subview(6, &[4]).is_err());
}
#[test]
fn subview_shares_buffer_identity_all_backends() {
let assert_shares = |memory: TensorMemory, label: &str| {
let parent = Tensor::<u8>::new(&[64], Some(memory), None)
.unwrap_or_else(|e| panic!("{label}: parent alloc failed: {e:?}"));
let parent_id = parent.buffer_identity().id();
let v0 = parent
.subview(0, &[16])
.unwrap_or_else(|e| panic!("{label}: subview(0) failed: {e:?}"));
let v1 = parent
.subview(16, &[16])
.unwrap_or_else(|e| panic!("{label}: subview(16) failed: {e:?}"));
assert_eq!(
v0.buffer_identity().id(),
parent_id,
"{label}: subview(0) minted a fresh BufferIdentity"
);
assert_eq!(
v1.buffer_identity().id(),
parent_id,
"{label}: subview(16) minted a fresh BufferIdentity"
);
};
assert_shares(TensorMemory::Mem, "Mem");
#[cfg(unix)]
if crate::is_shm_available() {
assert_shares(TensorMemory::Shm, "Shm");
}
if crate::is_gpu_buffer_available() {
assert_shares(TensorMemory::Dma, "Dma");
}
}
#[test]
fn mem_subview_four_views_no_aliasing() {
let parent = Tensor::<f32>::new(&[4, 3], Some(TensorMemory::Mem), None).unwrap();
let frame = 3 * std::mem::size_of::<f32>();
for i in 0..4 {
let v = parent.subview(i * frame, &[1, 3]).unwrap();
let val = i as f32 + 1.0;
v.map()
.unwrap()
.as_mut_slice()
.copy_from_slice(&[val, val, val]);
}
assert_eq!(
parent.map().unwrap().as_slice(),
&[1.0, 1.0, 1.0, 2.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 4.0, 4.0]
);
}
#[test]
fn mem_subview_inherits_format_and_row_stride() {
let mut parent =
Tensor::<u8>::image(100, 100, PixelFormat::Rgba, Some(TensorMemory::Mem)).unwrap();
parent.set_row_stride_unchecked(512); let view = parent.subview(4096, &[10, 10, 4]).unwrap();
assert_eq!(view.format(), Some(PixelFormat::Rgba), "format inherited");
assert_eq!(view.row_stride(), Some(512), "row_stride inherited");
}
#[test]
fn mem_strided_subview_maps_offset_and_byte_size() {
let parent = Tensor::<u8>::new(&[2048], Some(TensorMemory::Mem), None).unwrap();
let mut view = parent.subview(128, &[8, 16]).unwrap(); assert_eq!(view.plane_offset(), Some(128));
view.set_row_stride_unchecked(32);
{
let mut m = view.map().unwrap();
let s = m.as_mut_slice();
assert_eq!(
s.len(),
256,
"strided map exposes the full padded byte window"
);
s[0] = 0xAA; s[32] = 0xBB; }
let p = parent.map().unwrap();
let pb = p.as_slice();
assert_eq!(pb[128], 0xAA, "row 0 writes at parent offset 128");
assert_eq!(
pb[128 + 32],
0xBB,
"row 1 writes at parent offset 128 + stride"
);
}
#[test]
#[cfg(unix)]
fn shm_subview_partitions_parent_buffer() {
if !crate::is_shm_available() {
eprintln!("SKIPPED: shm not available");
return;
}
let parent = Tensor::<u8>::new(&[2, 4], Some(TensorMemory::Shm), None).unwrap();
let view0 = parent.subview(0, &[1, 4]).expect("shm subview at offset 0");
let view1 = parent.subview(4, &[1, 4]).expect("shm subview at offset 4");
view1
.map()
.unwrap()
.as_mut_slice()
.copy_from_slice(&[10, 20, 30, 40]);
view0
.map()
.unwrap()
.as_mut_slice()
.copy_from_slice(&[1, 2, 3, 4]);
assert_eq!(view0.map().unwrap().as_slice(), &[1, 2, 3, 4]);
assert_eq!(view1.map().unwrap().as_slice(), &[10, 20, 30, 40]);
assert_eq!(
parent.map().unwrap().as_slice(),
&[1, 2, 3, 4, 10, 20, 30, 40]
);
let nested = view1.subview(2, &[1, 2]).expect("nested shm subview");
assert_eq!(nested.map().unwrap().as_slice(), &[30, 40]);
}
#[test]
#[cfg(unix)]
fn shm_subview_rejects_unaligned_and_oob() {
if !crate::is_shm_available() {
eprintln!("SKIPPED: shm not available");
return;
}
let parent = Tensor::<f32>::new(&[8], Some(TensorMemory::Shm), None).unwrap();
assert!(parent.subview(2, &[1]).is_err());
assert!(parent.subview(4, &[1]).is_ok());
let p2 = Tensor::<u8>::new(&[8], Some(TensorMemory::Shm), None).unwrap();
assert!(p2.subview(6, &[4]).is_err());
}
#[test]
#[cfg(target_os = "linux")]
fn dma_subview_matches_mem_subview() {
let _lock = FD_LOCK.read().unwrap();
let dma = match Tensor::<u8>::new(&[8], Some(TensorMemory::Dma), None) {
Ok(t) => t,
Err(_) => {
eprintln!("SKIPPED: DMA not available");
return;
}
};
let mem = Tensor::<u8>::new(&[8], Some(TensorMemory::Mem), None).unwrap();
for parent in [&dma, &mem] {
let v0 = parent.subview(0, &[4]).unwrap();
let v1 = parent.subview(4, &[4]).unwrap();
v0.map()
.unwrap()
.as_mut_slice()
.copy_from_slice(&[1, 2, 3, 4]);
v1.map()
.unwrap()
.as_mut_slice()
.copy_from_slice(&[5, 6, 7, 8]);
assert_eq!(parent.map().unwrap().as_slice(), &[1, 2, 3, 4, 5, 6, 7, 8]);
}
}
#[test]
#[cfg(target_os = "linux")]
fn dma_strided_subview_maps_padded_window() {
let _lock = FD_LOCK.read().unwrap();
let parent = match Tensor::<u8>::new(&[2048], Some(TensorMemory::Dma), None) {
Ok(t) => t,
Err(_) => {
eprintln!("SKIPPED: DMA not available");
return;
}
};
let mut view = parent.subview(128, &[8, 16]).unwrap();
assert_eq!(view.plane_offset(), Some(128));
view.set_row_stride_unchecked(32);
{
let mut m = view.map().unwrap();
let s = m.as_mut_slice();
assert_eq!(s.len(), 256, "strided DMA map exposes stride(32) × rows(8)");
s[0] = 0xAA; s[32] = 0xBB; }
let p = parent.map().unwrap();
let pb = p.as_slice();
assert_eq!(pb[128], 0xAA, "row 0 writes at parent offset 128");
assert_eq!(
pb[128 + 32],
0xBB,
"row 1 writes at parent offset 128 + stride"
);
}
#[test]
#[cfg(target_os = "linux")]
fn view_single_row_snapshots_parent_stride() {
let _lock = FD_LOCK.read().unwrap();
let parent = match Tensor::<u8>::image_with_stride(
8,
4,
PixelFormat::Rgba,
64,
Some(TensorMemory::Dma),
) {
Ok(t) => t,
Err(_) => {
eprintln!("SKIPPED: DMA not available");
return;
}
};
assert_eq!(parent.effective_row_stride(), Some(64));
let row = parent.view(Region::new(2, 3, 4, 1)).unwrap();
assert_eq!(row.effective_row_stride(), Some(16));
let vo = row.view_origin().expect("a view carries a view_origin");
assert_eq!(
vo.parent_row_stride, 64,
"GL keys/pitches a view on the parent stride, not its tight one"
);
assert_eq!(row.map().unwrap().as_slice().len(), 16);
}
#[test]
fn test_mem_tensor() {
let shape = vec![2, 3, 4];
let tensor =
MemTensor::<f32>::new(&shape, Some("test_tensor")).expect("Failed to create tensor");
assert_eq!(tensor.shape(), &shape);
assert_eq!(tensor.size(), 2 * 3 * 4 * std::mem::size_of::<f32>());
assert_eq!(tensor.name(), "test_tensor");
{
let mut tensor_map = tensor.map().expect("Failed to map memory");
tensor_map.fill(42.0);
assert!(tensor_map.iter().all(|&x| x == 42.0));
}
let mut tensor = MemTensor::<u8>::new(&shape, None).expect("Failed to create tensor");
assert_eq!(tensor.shape(), &shape);
let new_shape = vec![3, 4, 4];
assert!(
tensor.reshape(&new_shape).is_err(),
"Reshape should fail due to size mismatch"
);
assert_eq!(tensor.shape(), &shape, "Shape should remain unchanged");
let new_shape = vec![2, 3, 4];
tensor.reshape(&new_shape).expect("Reshape should succeed");
assert_eq!(
tensor.shape(),
&new_shape,
"Shape should be updated after successful reshape"
);
{
let mut tensor_map = tensor.map().expect("Failed to map memory");
tensor_map.fill(1);
assert!(tensor_map.iter().all(|&x| x == 1));
}
{
let mut tensor_map = tensor.map().expect("Failed to map memory");
tensor_map[2] = 42;
assert_eq!(tensor_map[1], 1, "Value at index 1 should be 1");
assert_eq!(tensor_map[2], 42, "Value at index 2 should be 42");
}
}
#[test]
#[cfg(target_os = "linux")]
fn test_dma_no_fd_leaks() {
let _lock = FD_LOCK.write().unwrap();
if !is_dma_available() {
log::warn!(
"SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
function!()
);
return;
}
let proc = procfs::process::Process::myself()
.expect("Failed to get current process using /proc/self");
let start_open_fds = proc
.fd_count()
.expect("Failed to get open file descriptor count");
for _ in 0..100 {
let tensor = Tensor::<u8>::new(&[100, 100], Some(TensorMemory::Dma), None)
.expect("Failed to create tensor");
let mut map = tensor.map().unwrap();
map.as_mut_slice().fill(233);
}
let end_open_fds = proc
.fd_count()
.expect("Failed to get open file descriptor count");
assert_eq!(
start_open_fds, end_open_fds,
"File descriptor leak detected: {} -> {}",
start_open_fds, end_open_fds
);
}
#[test]
#[cfg(target_os = "linux")]
fn test_dma_from_fd_no_fd_leaks() {
let _lock = FD_LOCK.write().unwrap();
if !is_dma_available() {
log::warn!(
"SKIPPED: {} - DMA memory allocation not available (permission denied or no DMA-BUF support)",
function!()
);
return;
}
let proc = procfs::process::Process::myself()
.expect("Failed to get current process using /proc/self");
let start_open_fds = proc
.fd_count()
.expect("Failed to get open file descriptor count");
let orig = Tensor::<u8>::new(&[100, 100], Some(TensorMemory::Dma), None).unwrap();
for _ in 0..100 {
let tensor =
Tensor::<u8>::from_fd(orig.clone_fd().unwrap(), orig.shape(), None).unwrap();
let mut map = tensor.map().unwrap();
map.as_mut_slice().fill(233);
}
drop(orig);
let end_open_fds = proc.fd_count().unwrap();
assert_eq!(
start_open_fds, end_open_fds,
"File descriptor leak detected: {} -> {}",
start_open_fds, end_open_fds
);
}
#[test]
#[cfg(target_os = "linux")]
fn test_shm_no_fd_leaks() {
let _lock = FD_LOCK.write().unwrap();
if !is_shm_available() {
log::warn!(
"SKIPPED: {} - SHM memory allocation not available (permission denied or no SHM support)",
function!()
);
return;
}
let proc = procfs::process::Process::myself()
.expect("Failed to get current process using /proc/self");
let start_open_fds = proc
.fd_count()
.expect("Failed to get open file descriptor count");
for _ in 0..100 {
let tensor = Tensor::<u8>::new(&[100, 100], Some(TensorMemory::Shm), None)
.expect("Failed to create tensor");
let mut map = tensor.map().unwrap();
map.as_mut_slice().fill(233);
}
let end_open_fds = proc
.fd_count()
.expect("Failed to get open file descriptor count");
assert_eq!(
start_open_fds, end_open_fds,
"File descriptor leak detected: {} -> {}",
start_open_fds, end_open_fds
);
}
#[test]
#[cfg(target_os = "linux")]
fn test_shm_from_fd_no_fd_leaks() {
let _lock = FD_LOCK.write().unwrap();
if !is_shm_available() {
log::warn!(
"SKIPPED: {} - SHM memory allocation not available (permission denied or no SHM support)",
function!()
);
return;
}
let proc = procfs::process::Process::myself()
.expect("Failed to get current process using /proc/self");
let start_open_fds = proc
.fd_count()
.expect("Failed to get open file descriptor count");
let orig = Tensor::<u8>::new(&[100, 100], Some(TensorMemory::Shm), None).unwrap();
for _ in 0..100 {
let tensor =
Tensor::<u8>::from_fd(orig.clone_fd().unwrap(), orig.shape(), None).unwrap();
let mut map = tensor.map().unwrap();
map.as_mut_slice().fill(233);
}
drop(orig);
let end_open_fds = proc.fd_count().unwrap();
assert_eq!(
start_open_fds, end_open_fds,
"File descriptor leak detected: {} -> {}",
start_open_fds, end_open_fds
);
}
#[cfg(feature = "ndarray")]
#[test]
fn test_ndarray() {
let _lock = FD_LOCK.read().unwrap();
let shape = vec![2, 3, 4];
let tensor = Tensor::<f32>::new(&shape, None, None).expect("Failed to create tensor");
let mut tensor_map = tensor.map().expect("Failed to map tensor memory");
tensor_map.fill(1.0);
let view = tensor_map.view().expect("Failed to get ndarray view");
assert_eq!(view.shape(), &[2, 3, 4]);
assert!(view.iter().all(|&x| x == 1.0));
let mut view_mut = tensor_map
.view_mut()
.expect("Failed to get mutable ndarray view");
view_mut[[0, 0, 0]] = 42.0;
assert_eq!(view_mut[[0, 0, 0]], 42.0);
assert_eq!(tensor_map[0], 42.0, "Value at index 0 should be 42");
}
#[test]
fn test_buffer_identity_unique() {
let id1 = BufferIdentity::new();
let id2 = BufferIdentity::new();
assert_ne!(
id1.id(),
id2.id(),
"Two identities should have different ids"
);
}
#[test]
fn test_buffer_identity_clone_shares_guard() {
let id1 = BufferIdentity::new();
let weak = id1.weak();
assert!(
weak.upgrade().is_some(),
"Weak should be alive while original exists"
);
let id2 = id1.clone();
assert_eq!(id1.id(), id2.id(), "Cloned identity should have same id");
drop(id1);
assert!(
weak.upgrade().is_some(),
"Weak should still be alive (clone holds Arc)"
);
drop(id2);
assert!(
weak.upgrade().is_none(),
"Weak should be dead after all clones dropped"
);
}
#[test]
fn test_tensor_buffer_identity() {
let t1 = Tensor::<u8>::new(&[100], Some(TensorMemory::Mem), Some("t1")).unwrap();
let t2 = Tensor::<u8>::new(&[100], Some(TensorMemory::Mem), Some("t2")).unwrap();
assert_ne!(
t1.buffer_identity().id(),
t2.buffer_identity().id(),
"Different tensors should have different buffer ids"
);
}
#[test]
fn test_quantization_per_tensor_constructors() {
let q = Quantization::per_tensor(0.1, -5);
assert!(q.is_per_tensor());
assert!(!q.is_per_channel());
assert!(!q.is_symmetric());
assert_eq!(q.scale(), &[0.1]);
assert_eq!(q.zero_point(), Some(&[-5][..]));
let qs = Quantization::per_tensor_symmetric(0.05);
assert!(qs.is_per_tensor());
assert!(qs.is_symmetric());
assert_eq!(qs.zero_point(), None);
}
#[test]
fn test_quantization_per_channel_constructors() {
let q = Quantization::per_channel(vec![0.1, 0.2, 0.3], vec![0, -1, 1], 2).unwrap();
assert!(q.is_per_channel());
assert!(!q.is_symmetric());
assert_eq!(q.axis(), Some(2));
assert_eq!(q.scale().len(), 3);
let qs = Quantization::per_channel_symmetric(vec![0.054, 0.089, 0.195], 0).unwrap();
assert!(qs.is_per_channel());
assert!(qs.is_symmetric());
assert_eq!(qs.axis(), Some(0));
}
#[test]
fn test_quantization_per_channel_length_mismatch_rejected() {
let err = Quantization::per_channel(vec![0.1, 0.2], vec![0, 0, 0], 0).unwrap_err();
assert!(matches!(err, Error::QuantizationInvalid { .. }));
}
#[test]
fn test_quantization_per_channel_empty_rejected() {
let err = Quantization::per_channel_symmetric(vec![], 0).unwrap_err();
assert!(matches!(err, Error::QuantizationInvalid { .. }));
}
#[test]
fn test_quantization_validate_rejects_malformed_deserialize() {
let mut t = Tensor::<i8>::new(&[1, 1, 4], Some(TensorMemory::Mem), None).unwrap();
let q: Quantization = serde_json::from_str(r#"{"scale": []}"#).unwrap();
assert!(matches!(
t.set_quantization(q).unwrap_err(),
Error::QuantizationInvalid { .. }
));
let q: Quantization =
serde_json::from_str(r#"{"scale": 0.1, "zero_point": [0, 0, 0]}"#).unwrap();
assert!(matches!(
t.set_quantization(q).unwrap_err(),
Error::QuantizationInvalid { .. }
));
let q: Quantization = serde_json::from_str(
r#"{"scale": [0.1, 0.2, 0.3, 0.4], "zero_point": [0, 0], "axis": 2}"#,
)
.unwrap();
assert!(matches!(
t.set_quantization(q).unwrap_err(),
Error::QuantizationInvalid { .. }
));
}
#[test]
fn test_quantization_mode_dispatch() {
let pt = Quantization::per_tensor(0.1, -5);
assert!(matches!(
pt.mode(),
QuantMode::PerTensor { scale, zero_point } if scale == 0.1 && zero_point == -5
));
let pts = Quantization::per_tensor_symmetric(0.05);
assert!(matches!(
pts.mode(),
QuantMode::PerTensorSymmetric { scale } if scale == 0.05
));
let pc = Quantization::per_channel(vec![0.1, 0.2], vec![0, -1], 2).unwrap();
assert!(matches!(pc.mode(), QuantMode::PerChannel { axis: 2, .. }));
let pcs = Quantization::per_channel_symmetric(vec![0.1, 0.2], 0).unwrap();
assert!(matches!(
pcs.mode(),
QuantMode::PerChannelSymmetric { axis: 0, .. }
));
}
#[test]
fn test_tensor_quantization_roundtrip_integer() {
let mut t = Tensor::<i8>::new(&[2, 3, 4], Some(TensorMemory::Mem), None).unwrap();
assert!(t.quantization().is_none());
t.set_quantization(Quantization::per_tensor(0.1, -5))
.unwrap();
let q = t.quantization().unwrap();
assert_eq!(q.scale(), &[0.1]);
t.clear_quantization();
assert!(t.quantization().is_none());
}
#[test]
fn test_tensor_with_quantization_builder() {
let t = Tensor::<i8>::new(&[4, 4], Some(TensorMemory::Mem), None)
.unwrap()
.with_quantization(Quantization::per_tensor_symmetric(0.05))
.unwrap();
assert!(t.quantization().is_some());
}
#[test]
fn test_tensor_dyn_quantization_float_arm_returns_none() {
let t = Tensor::<f32>::new(&[2, 2], Some(TensorMemory::Mem), None).unwrap();
let td = TensorDyn::F32(t);
assert!(td.quantization().is_none());
}
#[test]
fn test_tensor_dyn_set_quantization_float_arm_errors() {
let t = Tensor::<f32>::new(&[2, 2], Some(TensorMemory::Mem), None).unwrap();
let mut td = TensorDyn::F32(t);
let err = td
.set_quantization(Quantization::per_tensor(0.1, 0))
.unwrap_err();
assert!(matches!(err, Error::QuantizationInvalid { .. }));
}
fn _compile_fail_doctest_anchor() {}
pub static FD_LOCK: RwLock<()> = RwLock::new(());
#[test]
#[cfg(not(target_os = "linux"))]
fn test_dma_not_available_on_non_linux() {
assert!(
!is_dma_available(),
"DMA memory allocation should NOT be available on non-Linux platforms"
);
}
#[test]
fn colorimetry_defaults_none_and_roundtrips_without_auto_fill() {
use crate::{ColorEncoding, ColorRange, Colorimetry, PixelFormat, TensorMemory};
let mut t =
Tensor::<u8>::image(1280, 720, PixelFormat::Nv12, Some(TensorMemory::Mem)).unwrap();
assert_eq!(t.colorimetry(), None); let c = Colorimetry::default()
.with_encoding(ColorEncoding::Bt709)
.with_range(ColorRange::Limited);
t.set_colorimetry(Some(c));
assert_eq!(t.colorimetry(), Some(c));
t.configure_image(640, 480, PixelFormat::Grey).unwrap();
assert_eq!(t.colorimetry(), Some(c));
}
#[test]
fn configure_image_within_capacity() {
let mut t = Tensor::<u8>::image_with_capacity(640, 480, PixelFormat::Rgb, None).unwrap();
t.configure_image(320, 240, PixelFormat::Nv12).unwrap();
assert_eq!(t.format(), Some(PixelFormat::Nv12));
assert_eq!(t.width(), Some(320));
assert_eq!(t.height(), Some(240));
assert_eq!(t.shape(), &[360, 320]); }
#[test]
fn configure_image_too_large_errors() {
let mut t = Tensor::<u8>::image_with_capacity(64, 64, PixelFormat::Grey, None).unwrap();
let err = t
.configure_image(1920, 1080, PixelFormat::Nv12)
.unwrap_err();
assert!(matches!(err, Error::InsufficientCapacity { .. }));
}
#[test]
#[cfg(target_os = "macos")]
fn configure_image_preserves_iosurface_physical_stride() {
let mut pool =
Tensor::<u8>::image(100, 64, PixelFormat::Grey, Some(TensorMemory::Dma)).unwrap();
let pitch = pool.effective_row_stride().unwrap();
assert!(
pitch >= 128 && pitch.is_multiple_of(64),
"padded bytesPerRow, got {pitch}"
);
pool.configure_image(32, 16, PixelFormat::Nv12).unwrap();
assert_eq!(pool.format(), Some(PixelFormat::Nv12));
assert_eq!(pool.width(), Some(32));
assert_eq!(pool.height(), Some(16));
assert_eq!(
pool.effective_row_stride(),
Some(pitch),
"configure_image must preserve the IOSurface physical bytesPerRow"
);
pool.configure_image(32, 16, PixelFormat::Nv24).unwrap();
assert_eq!(pool.effective_row_stride(), Some(pitch));
}
#[test]
fn configure_image_mem_aligns_stride() {
let mut t =
Tensor::<u8>::image_with_capacity(64, 64, PixelFormat::Rgba, Some(TensorMemory::Mem))
.unwrap();
t.configure_image(32, 16, PixelFormat::Nv12).unwrap();
let s = t.effective_row_stride().unwrap();
assert_eq!(s % 64, 0, "stride must be 64-aligned");
assert!(s >= 32, "stride must cover the even-width minimum");
assert_eq!(s, 64);
}
#[test]
fn strided_mem_tensor_cpu_maps_full_padded_buffer() {
let mut t =
Tensor::<u8>::image_with_capacity(16, 3, PixelFormat::Rgba, Some(TensorMemory::Mem))
.unwrap(); t.configure_image(8, 3, PixelFormat::Rgba).unwrap(); t.set_row_stride(48).unwrap();
let map = t.map().expect("strided Mem tensor should CPU-map");
assert_eq!(map.as_slice().len(), 144);
assert_eq!(map.shape(), &[3, 8, 4]);
}
#[test]
fn strided_mem_tensor_over_capacity_errors() {
let mut t = Tensor::<u8>::new(&[3, 8, 4], Some(TensorMemory::Mem), None).unwrap();
t.set_format(PixelFormat::Rgba).unwrap();
t.set_row_stride(64).unwrap();
assert!(matches!(t.map(), Err(Error::InsufficientCapacity { .. })));
}
#[test]
#[cfg(unix)]
fn test_shm_available_and_usable() {
assert!(
is_shm_available(),
"SHM memory allocation should be available on Unix systems"
);
let tensor = Tensor::<u8>::new(&[100, 100], Some(TensorMemory::Shm), None)
.expect("Failed to create SHM tensor");
let mut map = tensor.map().expect("Failed to map SHM tensor");
map.as_mut_slice().fill(0xAB);
assert!(
map.as_slice().iter().all(|&b| b == 0xAB),
"SHM tensor data should be writable and readable"
);
}
#[test]
fn packed_rgba16f_layout_planar_rgb_f16() {
let layout =
packed_rgba16f_layout(PixelFormat::PlanarRgb, DType::F16, 640, 640).expect("Some");
assert_eq!(layout.surface_w, 160);
assert_eq!(layout.surface_h, 1920);
assert_eq!(layout.bytes_per_texel, 8);
assert_eq!(layout.pitch, 1280);
}
#[test]
fn packed_rgba16f_layout_planar_rgba_f16() {
let layout =
packed_rgba16f_layout(PixelFormat::PlanarRgba, DType::F16, 640, 640).expect("Some");
assert_eq!(layout.surface_w, 160);
assert_eq!(layout.surface_h, 2560); assert_eq!(layout.bytes_per_texel, 8);
assert_eq!(layout.pitch, 1280);
}
#[test]
fn packed_rgba16f_layout_rejects_misaligned() {
assert!(packed_rgba16f_layout(PixelFormat::PlanarRgb, DType::F16, 642, 640).is_none());
}
#[test]
fn packed_rgba16f_layout_rejects_non_f16() {
assert!(packed_rgba16f_layout(PixelFormat::PlanarRgb, DType::U8, 640, 640).is_none());
assert!(packed_rgba16f_layout(PixelFormat::Rgb, DType::F32, 640, 640).is_none());
assert!(packed_rgba16f_layout(PixelFormat::Rgba, DType::F16, 640, 640).is_none());
}
#[test]
fn cuda_map_fast_fails_to_none_without_handle() {
let t = Tensor::<f32>::new(&[4], Some(TensorMemory::Mem), None).unwrap();
assert!(t.cuda().is_none());
assert!(t.cuda_map().is_none()); }
#[test]
fn cuda_returns_none_without_handle() {
let t = Tensor::<f32>::new(&[2, 2], Some(TensorMemory::Mem), None).unwrap();
assert!(t.cuda().is_none(), "no CUDA handle on a Mem tensor");
assert!(t.cuda_map().is_none(), "fast-fail map → None");
}
#[test]
fn cuda_map_then_host_map_fallback() {
let t = Tensor::<f32>::new(&[2, 2], Some(TensorMemory::Mem), None).unwrap();
let cuda = t.cuda_map();
if let Some(_c) = cuda {
unreachable!("a Mem tensor has no CUDA handle");
} else {
let host = t.map().expect("host map fallback must succeed");
assert_eq!(host.len(), 4); }
}
#[test]
fn from_foreign_valid_wrap_and_readback() {
let mut buf: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let ptr = buf.as_mut_ptr();
let t = unsafe { Tensor::<f32>::from_foreign(ptr, &[2, 3], None, Some("test_foreign")) }
.expect("valid from_foreign must succeed");
assert_eq!(t.shape(), &[2, 3]);
assert_eq!(t.memory(), TensorMemory::Mem);
assert_eq!(t.name(), "test_foreign");
let m = t.map().unwrap();
assert_eq!(m.as_slice(), &[1.0_f32, 2.0, 3.0, 4.0, 5.0, 6.0]);
}
#[test]
fn from_foreign_write_visible_in_caller_allocation() {
let mut buf: Vec<u8> = vec![0u8; 6];
let ptr = buf.as_mut_ptr();
let t = unsafe { Tensor::<u8>::from_foreign(ptr, &[2, 3], None, None) }.unwrap();
{
let mut m = t.map().unwrap();
m.as_mut_slice().copy_from_slice(&[10, 20, 30, 40, 50, 60]);
}
drop(t);
assert_eq!(buf, vec![10, 20, 30, 40, 50, 60]);
}
#[test]
fn from_foreign_rejects_null_ptr() {
let err = unsafe { Tensor::<u8>::from_foreign(std::ptr::null_mut(), &[4], None, None) }
.unwrap_err();
assert!(
matches!(err, Error::InvalidArgument(ref m) if m.contains("non-null")),
"expected InvalidArgument(non-null), got {err:?}"
);
}
#[test]
fn from_foreign_rejects_empty_shape() {
let mut dummy: u8 = 0;
let err = unsafe { Tensor::<u8>::from_foreign(&mut dummy, &[], None, None) }.unwrap_err();
assert!(
matches!(err, Error::InvalidSize(0)),
"expected InvalidSize(0) for empty shape, got {err:?}"
);
}
#[test]
fn from_foreign_rejects_overflow_shape() {
let mut dummy: u8 = 0;
let huge = [usize::MAX / 2 + 1, 2];
let err = unsafe { Tensor::<u8>::from_foreign(&mut dummy, &huge, None, None) }.unwrap_err();
assert!(
matches!(err, Error::InvalidArgument(ref m) if m.contains("overflow")),
"expected InvalidArgument(overflow), got {err:?}"
);
}
#[test]
fn from_foreign_owner_keeps_allocation_alive() {
use std::sync::atomic::{AtomicBool, Ordering};
let flag = std::sync::Arc::new(AtomicBool::new(false));
let flag2 = flag.clone();
struct Guard(std::sync::Arc<AtomicBool>);
impl Drop for Guard {
fn drop(&mut self) {
self.0.store(true, Ordering::SeqCst);
}
}
let mut buf: Vec<u32> = vec![42u32; 4];
let ptr = buf.as_mut_ptr();
let owner: ForeignOwner = Box::new(Guard(flag2));
let t = unsafe { Tensor::<u32>::from_foreign(ptr, &[4], Some(owner), None) }.unwrap();
let m = t.map().unwrap();
assert_eq!(m.as_slice()[0], 42);
drop(t); assert!(
!flag.load(Ordering::SeqCst),
"owner must not drop while a map shares the backing"
);
drop(m);
assert!(
flag.load(Ordering::SeqCst),
"owner Drop must fire when the last Arc reference is released"
);
}
}