Skip to main content

edgefirst_tensor/
tensor_dyn.rs

1// SPDX-FileCopyrightText: Copyright 2025 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::{DType, PixelFormat, Tensor, TensorMemory, TensorTrait};
5use half::f16;
6use std::fmt;
7
8/// Type-erased tensor. Wraps a `Tensor<T>` with runtime element type.
9#[non_exhaustive]
10pub enum TensorDyn {
11    /// Unsigned 8-bit integer tensor.
12    U8(Tensor<u8>),
13    /// Signed 8-bit integer tensor.
14    I8(Tensor<i8>),
15    /// Unsigned 16-bit integer tensor.
16    U16(Tensor<u16>),
17    /// Signed 16-bit integer tensor.
18    I16(Tensor<i16>),
19    /// Unsigned 32-bit integer tensor.
20    U32(Tensor<u32>),
21    /// Signed 32-bit integer tensor.
22    I32(Tensor<i32>),
23    /// Unsigned 64-bit integer tensor.
24    U64(Tensor<u64>),
25    /// Signed 64-bit integer tensor.
26    I64(Tensor<i64>),
27    /// 16-bit floating-point tensor.
28    F16(Tensor<f16>),
29    /// 32-bit floating-point tensor.
30    F32(Tensor<f32>),
31    /// 64-bit floating-point tensor.
32    F64(Tensor<f64>),
33}
34
35/// Dispatch a method call across all TensorDyn variants.
36macro_rules! dispatch {
37    ($self:expr, $method:ident $(, $arg:expr)*) => {
38        match $self {
39            TensorDyn::U8(t) => t.$method($($arg),*),
40            TensorDyn::I8(t) => t.$method($($arg),*),
41            TensorDyn::U16(t) => t.$method($($arg),*),
42            TensorDyn::I16(t) => t.$method($($arg),*),
43            TensorDyn::U32(t) => t.$method($($arg),*),
44            TensorDyn::I32(t) => t.$method($($arg),*),
45            TensorDyn::U64(t) => t.$method($($arg),*),
46            TensorDyn::I64(t) => t.$method($($arg),*),
47            TensorDyn::F16(t) => t.$method($($arg),*),
48            TensorDyn::F32(t) => t.$method($($arg),*),
49            TensorDyn::F64(t) => t.$method($($arg),*),
50        }
51    };
52}
53
54/// Like [`dispatch!`], but for methods returning `Result<Tensor<T>>`: rewrap the
55/// typed result back into the matching `TensorDyn` variant. Keeps sub-region
56/// fan-out (`batch`, future `view`) to one line instead of an 11-arm match.
57macro_rules! dyn_fanout {
58    ($self:expr, $method:ident $(, $arg:expr)*) => {
59        match $self {
60            TensorDyn::U8(t) => t.$method($($arg),*).map(TensorDyn::U8),
61            TensorDyn::I8(t) => t.$method($($arg),*).map(TensorDyn::I8),
62            TensorDyn::U16(t) => t.$method($($arg),*).map(TensorDyn::U16),
63            TensorDyn::I16(t) => t.$method($($arg),*).map(TensorDyn::I16),
64            TensorDyn::U32(t) => t.$method($($arg),*).map(TensorDyn::U32),
65            TensorDyn::I32(t) => t.$method($($arg),*).map(TensorDyn::I32),
66            TensorDyn::U64(t) => t.$method($($arg),*).map(TensorDyn::U64),
67            TensorDyn::I64(t) => t.$method($($arg),*).map(TensorDyn::I64),
68            TensorDyn::F16(t) => t.$method($($arg),*).map(TensorDyn::F16),
69            TensorDyn::F32(t) => t.$method($($arg),*).map(TensorDyn::F32),
70            TensorDyn::F64(t) => t.$method($($arg),*).map(TensorDyn::F64),
71        }
72    };
73}
74
75/// Generate the three downcast methods (ref, mut ref, owned) for one variant.
76macro_rules! downcast_methods {
77    ($variant:ident, $ty:ty, $as_name:ident, $as_mut_name:ident, $into_name:ident) => {
78        /// Returns a shared reference to the inner tensor if the type matches.
79        pub fn $as_name(&self) -> Option<&Tensor<$ty>> {
80            match self {
81                Self::$variant(t) => Some(t),
82                _ => None,
83            }
84        }
85
86        /// Returns a mutable reference to the inner tensor if the type matches.
87        pub fn $as_mut_name(&mut self) -> Option<&mut Tensor<$ty>> {
88            match self {
89                Self::$variant(t) => Some(t),
90                _ => None,
91            }
92        }
93
94        /// Unwraps the inner tensor if the type matches, otherwise returns `self` as `Err`.
95        /// The Err variant is necessarily large (returns the unconsumed TensorDyn).
96        #[allow(clippy::result_large_err)]
97        pub fn $into_name(self) -> Result<Tensor<$ty>, Self> {
98            match self {
99                Self::$variant(t) => Ok(t),
100                other => Err(other),
101            }
102        }
103    };
104}
105
106impl TensorDyn {
107    /// Return the runtime element type discriminant.
108    pub fn dtype(&self) -> DType {
109        match self {
110            Self::U8(_) => DType::U8,
111            Self::I8(_) => DType::I8,
112            Self::U16(_) => DType::U16,
113            Self::I16(_) => DType::I16,
114            Self::U32(_) => DType::U32,
115            Self::I32(_) => DType::I32,
116            Self::U64(_) => DType::U64,
117            Self::I64(_) => DType::I64,
118            Self::F16(_) => DType::F16,
119            Self::F32(_) => DType::F32,
120            Self::F64(_) => DType::F64,
121        }
122    }
123
124    /// Return the tensor shape.
125    pub fn shape(&self) -> &[usize] {
126        dispatch!(self, shape)
127    }
128
129    /// Return the tensor name.
130    pub fn name(&self) -> String {
131        dispatch!(self, name)
132    }
133
134    /// Return the pixel format (None if not an image tensor).
135    pub fn format(&self) -> Option<PixelFormat> {
136        dispatch!(self, format)
137    }
138
139    /// Return the image width (None if not an image tensor).
140    pub fn width(&self) -> Option<usize> {
141        dispatch!(self, width)
142    }
143
144    /// Return the image height (None if not an image tensor).
145    pub fn height(&self) -> Option<usize> {
146        dispatch!(self, height)
147    }
148
149    /// Return the total size of this tensor in bytes.
150    pub fn size(&self) -> usize {
151        dispatch!(self, size)
152    }
153
154    /// Return the memory allocation type.
155    pub fn memory(&self) -> TensorMemory {
156        dispatch!(self, memory)
157    }
158
159    /// Reshape this tensor. Total element count must remain the same.
160    pub fn reshape(&mut self, shape: &[usize]) -> crate::Result<()> {
161        dispatch!(self, reshape, shape)
162    }
163
164    /// Attach pixel format metadata to this tensor.
165    ///
166    /// Validates that the tensor's shape is compatible with the format's
167    /// layout (packed, planar, or semi-planar).
168    ///
169    /// # Arguments
170    ///
171    /// * `format` - The pixel format to attach
172    ///
173    /// # Returns
174    ///
175    /// `Ok(())` on success, with the format stored as metadata on the tensor.
176    ///
177    /// # Errors
178    ///
179    /// Returns `Error::InvalidShape` if the tensor shape doesn't match
180    /// the expected layout for the given format.
181    pub fn set_format(&mut self, format: PixelFormat) -> crate::Result<()> {
182        dispatch!(self, set_format, format)
183    }
184
185    /// Attach pixel format metadata, consuming and returning self.
186    ///
187    /// Enables builder-style chaining.
188    ///
189    /// # Arguments
190    ///
191    /// * `format` - The pixel format to attach
192    ///
193    /// # Returns
194    ///
195    /// The tensor with format metadata attached.
196    ///
197    /// # Errors
198    ///
199    /// Returns `Error::InvalidShape` if the tensor shape doesn't match
200    /// the expected layout for the given format.
201    pub fn with_format(mut self, format: PixelFormat) -> crate::Result<Self> {
202        self.set_format(format)?;
203        Ok(self)
204    }
205
206    /// Colorimetry metadata (`None` = undefined; never auto-filled).
207    pub fn colorimetry(&self) -> Option<crate::Colorimetry> {
208        dispatch!(self, colorimetry)
209    }
210
211    /// Attach/clear colorimetry metadata.
212    pub fn set_colorimetry(&mut self, c: Option<crate::Colorimetry>) {
213        dispatch!(self, set_colorimetry, c)
214    }
215
216    /// Builder-style colorimetry attach (consumes and returns self).
217    pub fn with_colorimetry(mut self, c: crate::Colorimetry) -> Self {
218        self.set_colorimetry(Some(c));
219        self
220    }
221
222    /// Row stride in bytes (`None` = tightly packed).
223    pub fn row_stride(&self) -> Option<usize> {
224        dispatch!(self, row_stride)
225    }
226
227    /// Effective row stride: stored stride or computed from format and width.
228    pub fn effective_row_stride(&self) -> Option<usize> {
229        dispatch!(self, effective_row_stride)
230    }
231
232    /// Set logical dimensions + format to a decoded image, reusing the
233    /// allocation. See [`Tensor::configure_image`].
234    pub fn configure_image(
235        &mut self,
236        width: usize,
237        height: usize,
238        format: PixelFormat,
239    ) -> crate::Result<()> {
240        dispatch!(self, configure_image, width, height, format)
241    }
242
243    /// Set the row stride in bytes for externally allocated buffers with
244    /// row padding.
245    ///
246    /// Must be called before the tensor is first used for rendering. The
247    /// format must be set before calling this method.
248    pub fn set_row_stride(&mut self, stride: usize) -> crate::Result<()> {
249        dispatch!(self, set_row_stride, stride)
250    }
251
252    /// Builder-style: set row stride, consuming and returning self.
253    pub fn with_row_stride(mut self, stride: usize) -> crate::Result<Self> {
254        self.set_row_stride(stride)?;
255        Ok(self)
256    }
257
258    /// Byte offset within the DMA-BUF where image data starts (`None` = 0).
259    pub fn plane_offset(&self) -> Option<usize> {
260        dispatch!(self, plane_offset)
261    }
262
263    /// The parent-image snapshot if this tensor is a [`view`](Self::view)/
264    /// [`batch`](Self::batch) sub-region; `None` for a whole tensor. See
265    /// [`Tensor::view_origin`].
266    pub fn view_origin(&self) -> Option<crate::ViewOrigin> {
267        dispatch!(self, view_origin)
268    }
269
270    /// Set the byte offset within the DMA-BUF where image data starts.
271    pub fn set_plane_offset(&mut self, offset: usize) {
272        dispatch!(self, set_plane_offset, offset)
273    }
274
275    /// Borrow batch element `n` of a batched tensor (leading `N` dimension) as a
276    /// zero-copy view sharing this tensor's allocation. See [`Tensor::batch`].
277    pub fn batch(&self, n: usize) -> crate::Result<TensorDyn> {
278        dyn_fanout!(self, batch, n)
279    }
280
281    /// Borrow a rectangular spatial sub-region (the destination/source crop) as
282    /// a zero-copy view sharing this tensor's allocation. See [`Tensor::view`].
283    pub fn view(&self, region: crate::Region) -> crate::Result<TensorDyn> {
284        dyn_fanout!(self, view, region)
285    }
286
287    /// The CUDA registration for this tensor, if any.
288    ///
289    /// Returns `None` when no CUDA handle has been attached (the common non-CUDA case).
290    /// This check is a pure local field read — no thread routing occurs.
291    pub fn cuda(&self) -> Option<&crate::cuda::CudaHandle> {
292        dispatch!(self, cuda)
293    }
294
295    /// Fast-fail CUDA map: `None` when no handle is attached; else maps the
296    /// PBO through the GL worker and returns a scoped device-pointer guard.
297    ///
298    /// The same try-`cuda_map`-then-[`map`](crate::TensorTrait::map) fallback pattern that applies to
299    /// [`Tensor::cuda_map`](crate::Tensor::cuda_map) applies here: call `cuda_map()` first for a
300    /// zero-copy device pointer; when it returns `None` (no CUDA handle attached), fall back to the
301    /// typed host mapping via the inner tensor.
302    ///
303    /// # Example
304    ///
305    /// ```no_run
306    /// use edgefirst_tensor::TensorDyn;
307    /// # fn feed_tensorrt(_dptr: *mut std::ffi::c_void, _bytes: usize) {}
308    /// # fn demo(t: &TensorDyn) {
309    /// if let Some(cuda) = t.cuda_map() {
310    ///     feed_tensorrt(cuda.device_ptr(), cuda.len());
311    /// } else {
312    ///     // No CUDA handle — use the typed inner tensor for host access.
313    ///     // See Tensor::cuda_map for the full fallback example.
314    /// }
315    /// # }
316    /// ```
317    pub fn cuda_map(&self) -> Option<crate::cuda::CudaMap<'_>> {
318        dispatch!(self, cuda_map)
319    }
320
321    /// Quantization metadata. Returns `None` for float variants (F16, F32,
322    /// F64) — quantization does not apply to floating-point tensors.
323    /// Otherwise delegates to the typed `Tensor<T>::quantization()` accessor.
324    pub fn quantization(&self) -> Option<&crate::Quantization> {
325        match self {
326            Self::U8(t) => t.quantization(),
327            Self::I8(t) => t.quantization(),
328            Self::U16(t) => t.quantization(),
329            Self::I16(t) => t.quantization(),
330            Self::U32(t) => t.quantization(),
331            Self::I32(t) => t.quantization(),
332            Self::U64(t) => t.quantization(),
333            Self::I64(t) => t.quantization(),
334            Self::F16(_) | Self::F32(_) | Self::F64(_) => None,
335        }
336    }
337
338    /// Attach quantization metadata. Fails on float variants with
339    /// [`crate::Error::QuantizationInvalid`]; delegates to the typed setter for
340    /// integer variants.
341    pub fn set_quantization(&mut self, q: crate::Quantization) -> crate::Result<()> {
342        match self {
343            Self::U8(t) => t.set_quantization(q),
344            Self::I8(t) => t.set_quantization(q),
345            Self::U16(t) => t.set_quantization(q),
346            Self::I16(t) => t.set_quantization(q),
347            Self::U32(t) => t.set_quantization(q),
348            Self::I32(t) => t.set_quantization(q),
349            Self::U64(t) => t.set_quantization(q),
350            Self::I64(t) => t.set_quantization(q),
351            Self::F16(_) | Self::F32(_) | Self::F64(_) => Err(crate::Error::QuantizationInvalid {
352                field: "dtype_is_integer",
353                expected: "integer tensor dtype (u8/i8/u16/i16/u32/i32/u64/i64)".to_string(),
354                got: format!("{:?}", self.dtype()),
355            }),
356        }
357    }
358
359    /// Builder-style variant of [`Self::set_quantization`]. Consumes self
360    /// and returns it with quantization applied (or the original error).
361    pub fn with_quantization(mut self, q: crate::Quantization) -> crate::Result<Self> {
362        self.set_quantization(q)?;
363        Ok(self)
364    }
365
366    /// Clear any quantization metadata. No-op on float variants.
367    pub fn clear_quantization(&mut self) {
368        match self {
369            Self::U8(t) => t.clear_quantization(),
370            Self::I8(t) => t.clear_quantization(),
371            Self::U16(t) => t.clear_quantization(),
372            Self::I16(t) => t.clear_quantization(),
373            Self::U32(t) => t.clear_quantization(),
374            Self::I32(t) => t.clear_quantization(),
375            Self::U64(t) => t.clear_quantization(),
376            Self::I64(t) => t.clear_quantization(),
377            Self::F16(_) | Self::F32(_) | Self::F64(_) => {}
378        }
379    }
380
381    /// Clone the file descriptor associated with this tensor.
382    #[cfg(unix)]
383    pub fn clone_fd(&self) -> crate::Result<std::os::fd::OwnedFd> {
384        dispatch!(self, clone_fd)
385    }
386
387    /// Clone the DMA-BUF file descriptor backing this tensor (Linux only).
388    ///
389    /// # Returns
390    ///
391    /// An owned duplicate of the DMA-BUF file descriptor.
392    ///
393    /// # Errors
394    ///
395    /// * `Error::NotImplemented` if the tensor is not DMA-backed (Mem/Shm/Pbo)
396    /// * `Error::IoError` if the fd clone syscall fails (e.g., fd limit reached)
397    #[cfg(target_os = "linux")]
398    pub fn dmabuf_clone(&self) -> crate::Result<std::os::fd::OwnedFd> {
399        if self.memory() != TensorMemory::Dma {
400            return Err(crate::Error::NotImplemented(format!(
401                "dmabuf_clone requires DMA-backed tensor, got {:?}",
402                self.memory()
403            )));
404        }
405        self.clone_fd()
406    }
407
408    /// Borrow the DMA-BUF file descriptor backing this tensor (Linux only).
409    ///
410    /// # Returns
411    ///
412    /// A borrowed reference to the DMA-BUF file descriptor, tied to `self`'s
413    /// lifetime.
414    ///
415    /// # Errors
416    ///
417    /// * `Error::NotImplemented` if the tensor is not DMA-backed
418    #[cfg(target_os = "linux")]
419    pub fn dmabuf(&self) -> crate::Result<std::os::fd::BorrowedFd<'_>> {
420        dispatch!(self, dmabuf)
421    }
422
423    /// Return `true` if this tensor uses separate plane allocations.
424    pub fn is_multiplane(&self) -> bool {
425        dispatch!(self, is_multiplane)
426    }
427
428    /// Return the [`BufferIdentity`](crate::BufferIdentity) of the underlying
429    /// allocation.
430    ///
431    /// Two `TensorDyn` values share a [`crate::BufferIdentity::id`] iff they were
432    /// produced by cloning the same allocation (e.g. through
433    /// `DmaTensor::try_clone`). Separate
434    /// imports of the same physical buffer (e.g. two `from_fd` calls on the
435    /// same dmabuf fd) have **distinct** identities — use
436    /// [`aliases`](Self::aliases) if you need to detect that case.
437    pub fn buffer_identity(&self) -> &crate::BufferIdentity {
438        dispatch!(self, buffer_identity)
439    }
440
441    /// Return `true` if `self` and `other` reference the same underlying
442    /// buffer.
443    ///
444    /// This is the correct check for APIs that require distinct input and
445    /// output tensors (e.g. `ImageProcessor::draw_decoded_masks`, where
446    /// aliasing `dst` and `background` would cause the GL backend to read
447    /// and write the same texture — undefined behaviour on most drivers).
448    ///
449    /// Matching is conservative:
450    /// 1. Matching [`crate::BufferIdentity::id`] → same buffer (always).
451    /// 2. Matching backing type + matching dmabuf fd number (Linux, DMA
452    ///    tensors only) → same buffer, even across separate `from_fd`
453    ///    imports in the same process.
454    ///
455    /// Two distinct `dup`'d fds pointing at the same kernel dma-buf are
456    /// **not** detected — there is no cheap way to resolve that without a
457    /// round-trip through the kernel.
458    pub fn aliases(&self, other: &Self) -> bool {
459        if self.buffer_identity().id() == other.buffer_identity().id() {
460            return true;
461        }
462        if self.memory() != other.memory() {
463            return false;
464        }
465        #[cfg(target_os = "linux")]
466        if self.memory() == TensorMemory::Dma {
467            use std::os::fd::AsRawFd;
468            if let (Ok(a), Ok(b)) = (self.dmabuf(), other.dmabuf()) {
469                return a.as_raw_fd() == b.as_raw_fd();
470            }
471        }
472        false
473    }
474
475    // --- Downcasting ---
476
477    downcast_methods!(U8, u8, as_u8, as_u8_mut, into_u8);
478    downcast_methods!(I8, i8, as_i8, as_i8_mut, into_i8);
479    downcast_methods!(U16, u16, as_u16, as_u16_mut, into_u16);
480    downcast_methods!(I16, i16, as_i16, as_i16_mut, into_i16);
481    downcast_methods!(U32, u32, as_u32, as_u32_mut, into_u32);
482    downcast_methods!(I32, i32, as_i32, as_i32_mut, into_i32);
483    downcast_methods!(U64, u64, as_u64, as_u64_mut, into_u64);
484    downcast_methods!(I64, i64, as_i64, as_i64_mut, into_i64);
485    downcast_methods!(F16, f16, as_f16, as_f16_mut, into_f16);
486    downcast_methods!(F32, f32, as_f32, as_f32_mut, into_f32);
487    downcast_methods!(F64, f64, as_f64, as_f64_mut, into_f64);
488
489    /// Create a type-erased tensor with the given shape and element type.
490    pub fn new(
491        shape: &[usize],
492        dtype: DType,
493        memory: Option<TensorMemory>,
494        name: Option<&str>,
495    ) -> crate::Result<Self> {
496        match dtype {
497            DType::U8 => Tensor::<u8>::new(shape, memory, name).map(Self::U8),
498            DType::I8 => Tensor::<i8>::new(shape, memory, name).map(Self::I8),
499            DType::U16 => Tensor::<u16>::new(shape, memory, name).map(Self::U16),
500            DType::I16 => Tensor::<i16>::new(shape, memory, name).map(Self::I16),
501            DType::U32 => Tensor::<u32>::new(shape, memory, name).map(Self::U32),
502            DType::I32 => Tensor::<i32>::new(shape, memory, name).map(Self::I32),
503            DType::U64 => Tensor::<u64>::new(shape, memory, name).map(Self::U64),
504            DType::I64 => Tensor::<i64>::new(shape, memory, name).map(Self::I64),
505            DType::F16 => Tensor::<f16>::new(shape, memory, name).map(Self::F16),
506            DType::F32 => Tensor::<f32>::new(shape, memory, name).map(Self::F32),
507            DType::F64 => Tensor::<f64>::new(shape, memory, name).map(Self::F64),
508        }
509    }
510
511    /// Import an existing buffer as a type-erased tensor, taking ownership
512    /// of its file descriptor. No bytes are copied.
513    ///
514    /// Dispatches to [`Tensor::from_fd`](crate::TensorTrait::from_fd) for
515    /// `dtype` and inherits its contract in full: on Linux the backend is
516    /// detected from the fd's filesystem magic — `DMA_BUF_MAGIC` imports as
517    /// [`TensorMemory::Dma`](crate::TensorMemory::Dma), `TMPFS_MAGIC` (both
518    /// `/dev/shm` and `memfd`) as [`TensorMemory::Shm`](crate::TensorMemory::Shm)
519    /// — and any other filesystem is rejected rather than assumed to be
520    /// shared memory. On non-Linux Unix the fd is always adopted as SHM.
521    ///
522    /// # Errors
523    ///
524    /// * [`Error::UnknownBufferType`](crate::Error::UnknownBufferType) - the
525    ///   fd is neither a DMA-BUF nor tmpfs-backed; carries the observed
526    ///   `fstatfs` magic. Linux only.
527    /// * [`Error::UnknownDeviceType`](crate::Error::UnknownDeviceType) - the
528    ///   fd lives on a real block device. Linux only.
529    /// * [`Error::InvalidSize`](crate::Error::InvalidSize) - `shape` is empty
530    ///   or describes zero elements.
531    /// * [`Error::NixError`](crate::Error::NixError) - a syscall on the
532    ///   descriptor failed.
533    ///
534    /// Callers that require zero-copy must check
535    /// [`memory()`](crate::TensorDyn::memory) on the result rather than
536    /// treating a successful import as proof of DMA backing.
537    #[cfg(unix)]
538    pub fn from_fd(
539        fd: std::os::fd::OwnedFd,
540        shape: &[usize],
541        dtype: DType,
542        name: Option<&str>,
543    ) -> crate::Result<Self> {
544        match dtype {
545            DType::U8 => Tensor::<u8>::from_fd(fd, shape, name).map(Self::U8),
546            DType::I8 => Tensor::<i8>::from_fd(fd, shape, name).map(Self::I8),
547            DType::U16 => Tensor::<u16>::from_fd(fd, shape, name).map(Self::U16),
548            DType::I16 => Tensor::<i16>::from_fd(fd, shape, name).map(Self::I16),
549            DType::U32 => Tensor::<u32>::from_fd(fd, shape, name).map(Self::U32),
550            DType::I32 => Tensor::<i32>::from_fd(fd, shape, name).map(Self::I32),
551            DType::U64 => Tensor::<u64>::from_fd(fd, shape, name).map(Self::U64),
552            DType::I64 => Tensor::<i64>::from_fd(fd, shape, name).map(Self::I64),
553            DType::F16 => Tensor::<f16>::from_fd(fd, shape, name).map(Self::F16),
554            DType::F32 => Tensor::<f32>::from_fd(fd, shape, name).map(Self::F32),
555            DType::F64 => Tensor::<f64>::from_fd(fd, shape, name).map(Self::F64),
556        }
557    }
558
559    /// Wrap externally-owned memory as a type-erased tensor without copying.
560    /// The tensor borrows `[ptr, ptr + shape.product() * dtype.size())` as
561    /// [`TensorMemory::Mem`]; `owner`, when `Some`, co-owns the source so it
562    /// outlives the tensor (and all derived views/maps). See
563    /// [`crate::ForeignOwner`] and [`Tensor::from_foreign`].
564    ///
565    /// # Safety
566    ///
567    /// `ptr` must be non-null, aligned to the element type, and valid for
568    /// `shape.product()` elements of `dtype` for as long as the returned
569    /// tensor — and every view/map sharing its backing — is alive. Pass an
570    /// `owner` that co-owns the source to uphold that contract.
571    pub unsafe fn from_foreign_ptr(
572        ptr: *mut u8,
573        shape: &[usize],
574        dtype: DType,
575        owner: Option<crate::ForeignOwner>,
576        name: Option<&str>,
577    ) -> crate::Result<Self> {
578        match dtype {
579            DType::U8 => Tensor::<u8>::from_foreign(ptr.cast(), shape, owner, name).map(Self::U8),
580            DType::I8 => Tensor::<i8>::from_foreign(ptr.cast(), shape, owner, name).map(Self::I8),
581            DType::U16 => {
582                Tensor::<u16>::from_foreign(ptr.cast(), shape, owner, name).map(Self::U16)
583            }
584            DType::I16 => {
585                Tensor::<i16>::from_foreign(ptr.cast(), shape, owner, name).map(Self::I16)
586            }
587            DType::U32 => {
588                Tensor::<u32>::from_foreign(ptr.cast(), shape, owner, name).map(Self::U32)
589            }
590            DType::I32 => {
591                Tensor::<i32>::from_foreign(ptr.cast(), shape, owner, name).map(Self::I32)
592            }
593            DType::U64 => {
594                Tensor::<u64>::from_foreign(ptr.cast(), shape, owner, name).map(Self::U64)
595            }
596            DType::I64 => {
597                Tensor::<i64>::from_foreign(ptr.cast(), shape, owner, name).map(Self::I64)
598            }
599            DType::F16 => {
600                Tensor::<f16>::from_foreign(ptr.cast(), shape, owner, name).map(Self::F16)
601            }
602            DType::F32 => {
603                Tensor::<f32>::from_foreign(ptr.cast(), shape, owner, name).map(Self::F32)
604            }
605            DType::F64 => {
606                Tensor::<f64>::from_foreign(ptr.cast(), shape, owner, name).map(Self::F64)
607            }
608        }
609    }
610
611    /// Wrap an externally-allocated IOSurface as a type-erased tensor
612    /// (macOS/iOS only).
613    ///
614    /// # Safety
615    ///
616    /// `surface_ref` must be a valid live `IOSurfaceRef`. `shape` must
617    /// match the IOSurface's pixel dimensions and chosen element type.
618    #[cfg(any(target_os = "macos", target_os = "ios"))]
619    pub unsafe fn from_iosurface(
620        surface_ref: *mut std::ffi::c_void,
621        shape: &[usize],
622        dtype: DType,
623        name: Option<&str>,
624    ) -> crate::Result<Self> {
625        unsafe {
626            match dtype {
627                DType::U8 => Tensor::<u8>::from_iosurface(surface_ref, shape, name).map(Self::U8),
628                DType::I8 => Tensor::<i8>::from_iosurface(surface_ref, shape, name).map(Self::I8),
629                DType::U16 => {
630                    Tensor::<u16>::from_iosurface(surface_ref, shape, name).map(Self::U16)
631                }
632                DType::I16 => {
633                    Tensor::<i16>::from_iosurface(surface_ref, shape, name).map(Self::I16)
634                }
635                DType::U32 => {
636                    Tensor::<u32>::from_iosurface(surface_ref, shape, name).map(Self::U32)
637                }
638                DType::I32 => {
639                    Tensor::<i32>::from_iosurface(surface_ref, shape, name).map(Self::I32)
640                }
641                DType::U64 => {
642                    Tensor::<u64>::from_iosurface(surface_ref, shape, name).map(Self::U64)
643                }
644                DType::I64 => {
645                    Tensor::<i64>::from_iosurface(surface_ref, shape, name).map(Self::I64)
646                }
647                DType::F16 => {
648                    Tensor::<f16>::from_iosurface(surface_ref, shape, name).map(Self::F16)
649                }
650                DType::F32 => {
651                    Tensor::<f32>::from_iosurface(surface_ref, shape, name).map(Self::F32)
652                }
653                DType::F64 => {
654                    Tensor::<f64>::from_iosurface(surface_ref, shape, name).map(Self::F64)
655                }
656            }
657        }
658    }
659
660    /// IOSurfaceID for cross-process surface sharing (macOS/iOS only).
661    /// Returns `None` when the tensor is not IOSurface-backed.
662    #[cfg(any(target_os = "macos", target_os = "ios"))]
663    pub fn iosurface_id(&self) -> Option<u32> {
664        dispatch!(self, iosurface_id)
665    }
666
667    /// Borrow the raw `IOSurfaceRef` backing this tensor (macOS/iOS
668    /// only). Returns `None` when the tensor is not IOSurface-backed.
669    /// The pointer's lifetime is tied to `self`.
670    #[cfg(any(target_os = "macos", target_os = "ios"))]
671    pub fn iosurface_ref(&self) -> Option<*mut std::ffi::c_void> {
672        dispatch!(self, iosurface_ref)
673    }
674
675    /// Physical IOSurface dimensions in texels, independent of the logical
676    /// shape (macOS/iOS only). `None` when not IOSurface-backed. The GL
677    /// backend binds the EGL pbuffer at these dims so one cached pbuffer
678    /// serves every frame size a reused pool surface holds.
679    #[cfg(any(target_os = "macos", target_os = "ios"))]
680    pub fn iosurface_physical_dims(&self) -> Option<(usize, usize)> {
681        dispatch!(self, iosurface_physical_dims)
682    }
683
684    /// Wrap an externally-allocated AHardwareBuffer as a type-erased
685    /// tensor (Android only). Used to import buffers from
686    /// CameraX/ImageReader (via JNI), NNAPI, or cross-process binder
687    /// transfers.
688    ///
689    /// # Safety
690    ///
691    /// `buffer_ptr` must be a valid live AHardwareBuffer pointer. `shape`
692    /// must match the buffer's dimensions and chosen element type.
693    #[cfg(target_os = "android")]
694    pub unsafe fn from_hardware_buffer(
695        buffer_ptr: *mut std::ffi::c_void,
696        shape: &[usize],
697        dtype: DType,
698        name: Option<&str>,
699    ) -> crate::Result<Self> {
700        unsafe {
701            match dtype {
702                DType::U8 => {
703                    Tensor::<u8>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::U8)
704                }
705                DType::I8 => {
706                    Tensor::<i8>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::I8)
707                }
708                DType::U16 => {
709                    Tensor::<u16>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::U16)
710                }
711                DType::I16 => {
712                    Tensor::<i16>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::I16)
713                }
714                DType::U32 => {
715                    Tensor::<u32>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::U32)
716                }
717                DType::I32 => {
718                    Tensor::<i32>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::I32)
719                }
720                DType::U64 => {
721                    Tensor::<u64>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::U64)
722                }
723                DType::I64 => {
724                    Tensor::<i64>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::I64)
725                }
726                DType::F16 => {
727                    Tensor::<f16>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::F16)
728                }
729                DType::F32 => {
730                    Tensor::<f32>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::F32)
731                }
732                DType::F64 => {
733                    Tensor::<f64>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::F64)
734                }
735            }
736        }
737    }
738
739    /// Borrow the raw AHardwareBuffer pointer backing this tensor
740    /// (Android only). Returns `None` when the tensor is not
741    /// AHardwareBuffer-backed. The pointer's lifetime is tied to `self`.
742    #[cfg(target_os = "android")]
743    pub fn hardware_buffer_ptr(&self) -> Option<*mut std::ffi::c_void> {
744        dispatch!(self, hardware_buffer_ptr)
745    }
746
747    /// Physical AHardwareBuffer dimensions in texels, independent of the
748    /// logical shape (Android only). `None` when not
749    /// AHardwareBuffer-backed.
750    #[cfg(target_os = "android")]
751    pub fn hardware_buffer_physical_dims(&self) -> Option<(usize, usize)> {
752        dispatch!(self, hardware_buffer_physical_dims)
753    }
754
755    /// Copy the tensor's logical bytes into `dst`, compacting away any
756    /// recorded row-stride padding — see [`Tensor::copy_to_flat`] for the
757    /// full contract. `dst.len()` must equal the tight byte footprint
758    /// (`shape` product × element size).
759    pub fn copy_to_flat(&self, dst: &mut [u8]) -> crate::Result<()> {
760        dispatch!(self, copy_to_flat, dst)
761    }
762
763    /// Create a type-erased image tensor.
764    ///
765    /// # Arguments
766    ///
767    /// * `width` - Image width in pixels
768    /// * `height` - Image height in pixels
769    /// * `format` - Pixel format
770    /// * `dtype` - Element type discriminant
771    /// * `memory` - Optional memory backend (None selects the best available)
772    ///
773    /// # Returns
774    ///
775    /// A new `TensorDyn` wrapping an image tensor of the requested element type.
776    ///
777    /// # Errors
778    ///
779    /// Returns an error if the underlying `Tensor::image` call fails.
780    pub fn image(
781        width: usize,
782        height: usize,
783        format: PixelFormat,
784        dtype: DType,
785        memory: Option<TensorMemory>,
786        access: crate::CpuAccess,
787    ) -> crate::Result<Self> {
788        match dtype {
789            DType::U8 => Tensor::<u8>::image(width, height, format, memory, access).map(Self::U8),
790            DType::I8 => Tensor::<i8>::image(width, height, format, memory, access).map(Self::I8),
791            DType::U16 => {
792                Tensor::<u16>::image(width, height, format, memory, access).map(Self::U16)
793            }
794            DType::I16 => {
795                Tensor::<i16>::image(width, height, format, memory, access).map(Self::I16)
796            }
797            DType::U32 => {
798                Tensor::<u32>::image(width, height, format, memory, access).map(Self::U32)
799            }
800            DType::I32 => {
801                Tensor::<i32>::image(width, height, format, memory, access).map(Self::I32)
802            }
803            DType::U64 => {
804                Tensor::<u64>::image(width, height, format, memory, access).map(Self::U64)
805            }
806            DType::I64 => {
807                Tensor::<i64>::image(width, height, format, memory, access).map(Self::I64)
808            }
809            DType::F16 => {
810                Tensor::<f16>::image(width, height, format, memory, access).map(Self::F16)
811            }
812            DType::F32 => {
813                Tensor::<f32>::image(width, height, format, memory, access).map(Self::F32)
814            }
815            DType::F64 => {
816                Tensor::<f64>::image(width, height, format, memory, access).map(Self::F64)
817            }
818        }
819    }
820
821    /// Allocate an image tensor from a declarative [`crate::ImageDesc`]
822    /// request — dispatching on `desc.dtype()`. See
823    /// [`Tensor::image_desc`] for the compression-request semantics.
824    pub fn image_desc(desc: &crate::ImageDesc) -> crate::Result<Self> {
825        match desc.dtype() {
826            DType::U8 => Tensor::<u8>::image_desc(desc).map(Self::U8),
827            DType::I8 => Tensor::<i8>::image_desc(desc).map(Self::I8),
828            DType::U16 => Tensor::<u16>::image_desc(desc).map(Self::U16),
829            DType::I16 => Tensor::<i16>::image_desc(desc).map(Self::I16),
830            DType::U32 => Tensor::<u32>::image_desc(desc).map(Self::U32),
831            DType::I32 => Tensor::<i32>::image_desc(desc).map(Self::I32),
832            DType::U64 => Tensor::<u64>::image_desc(desc).map(Self::U64),
833            DType::I64 => Tensor::<i64>::image_desc(desc).map(Self::I64),
834            DType::F16 => Tensor::<f16>::image_desc(desc).map(Self::F16),
835            DType::F32 => Tensor::<f32>::image_desc(desc).map(Self::F32),
836            DType::F64 => Tensor::<f64>::image_desc(desc).map(Self::F64),
837        }
838    }
839
840    /// The recorded vendor tile-compression scheme (see
841    /// [`Tensor::compression`]).
842    pub fn compression(&self) -> Option<crate::CompressionScheme> {
843        dispatch!(self, compression)
844    }
845
846    /// Create a DMA-backed image tensor with an explicit row stride that
847    /// may exceed the natural `width * channels * sizeof(T)` pitch.
848    ///
849    /// See [`Tensor::image_with_stride`] for the detailed contract and
850    /// constraints. The TensorDyn wrapper dispatches to the appropriate
851    /// monomorphised `Tensor<T>` based on `dtype`.
852    ///
853    /// # Example
854    ///
855    /// ```no_run
856    /// use edgefirst_tensor::{CpuAccess, TensorDyn, PixelFormat, DType, TensorMemory};
857    /// # fn main() -> edgefirst_tensor::Result<()> {
858    /// // Allocate a 3004×1688 RGBA8 canvas with 64-byte pitch alignment
859    /// // (12032 bytes per row instead of the natural 12016).
860    /// let img = TensorDyn::image_with_stride(
861    ///     3004, 1688,
862    ///     PixelFormat::Rgba, DType::U8,
863    ///     12032,
864    ///     Some(TensorMemory::Dma),
865    ///     CpuAccess::ReadWrite,
866    /// )?;
867    /// assert_eq!(img.width(), Some(3004));       // logical, unchanged
868    /// assert_eq!(img.effective_row_stride(), Some(12032)); // padded
869    /// # Ok(())
870    /// # }
871    /// ```
872    pub fn image_with_stride(
873        width: usize,
874        height: usize,
875        format: PixelFormat,
876        dtype: DType,
877        row_stride_bytes: usize,
878        memory: Option<TensorMemory>,
879        access: crate::CpuAccess,
880    ) -> crate::Result<Self> {
881        match dtype {
882            DType::U8 => Tensor::<u8>::image_with_stride(
883                width,
884                height,
885                format,
886                row_stride_bytes,
887                memory,
888                access,
889            )
890            .map(Self::U8),
891            DType::I8 => Tensor::<i8>::image_with_stride(
892                width,
893                height,
894                format,
895                row_stride_bytes,
896                memory,
897                access,
898            )
899            .map(Self::I8),
900            DType::U16 => Tensor::<u16>::image_with_stride(
901                width,
902                height,
903                format,
904                row_stride_bytes,
905                memory,
906                access,
907            )
908            .map(Self::U16),
909            DType::I16 => Tensor::<i16>::image_with_stride(
910                width,
911                height,
912                format,
913                row_stride_bytes,
914                memory,
915                access,
916            )
917            .map(Self::I16),
918            DType::U32 => Tensor::<u32>::image_with_stride(
919                width,
920                height,
921                format,
922                row_stride_bytes,
923                memory,
924                access,
925            )
926            .map(Self::U32),
927            DType::I32 => Tensor::<i32>::image_with_stride(
928                width,
929                height,
930                format,
931                row_stride_bytes,
932                memory,
933                access,
934            )
935            .map(Self::I32),
936            DType::U64 => Tensor::<u64>::image_with_stride(
937                width,
938                height,
939                format,
940                row_stride_bytes,
941                memory,
942                access,
943            )
944            .map(Self::U64),
945            DType::I64 => Tensor::<i64>::image_with_stride(
946                width,
947                height,
948                format,
949                row_stride_bytes,
950                memory,
951                access,
952            )
953            .map(Self::I64),
954            DType::F16 => Tensor::<f16>::image_with_stride(
955                width,
956                height,
957                format,
958                row_stride_bytes,
959                memory,
960                access,
961            )
962            .map(Self::F16),
963            DType::F32 => Tensor::<f32>::image_with_stride(
964                width,
965                height,
966                format,
967                row_stride_bytes,
968                memory,
969                access,
970            )
971            .map(Self::F32),
972            DType::F64 => Tensor::<f64>::image_with_stride(
973                width,
974                height,
975                format,
976                row_stride_bytes,
977                memory,
978                access,
979            )
980            .map(Self::F64),
981        }
982    }
983}
984
985// --- From impls ---
986
987impl From<Tensor<u8>> for TensorDyn {
988    fn from(t: Tensor<u8>) -> Self {
989        Self::U8(t)
990    }
991}
992
993impl From<Tensor<i8>> for TensorDyn {
994    fn from(t: Tensor<i8>) -> Self {
995        Self::I8(t)
996    }
997}
998
999impl From<Tensor<u16>> for TensorDyn {
1000    fn from(t: Tensor<u16>) -> Self {
1001        Self::U16(t)
1002    }
1003}
1004
1005impl From<Tensor<i16>> for TensorDyn {
1006    fn from(t: Tensor<i16>) -> Self {
1007        Self::I16(t)
1008    }
1009}
1010
1011impl From<Tensor<u32>> for TensorDyn {
1012    fn from(t: Tensor<u32>) -> Self {
1013        Self::U32(t)
1014    }
1015}
1016
1017impl From<Tensor<i32>> for TensorDyn {
1018    fn from(t: Tensor<i32>) -> Self {
1019        Self::I32(t)
1020    }
1021}
1022
1023impl From<Tensor<u64>> for TensorDyn {
1024    fn from(t: Tensor<u64>) -> Self {
1025        Self::U64(t)
1026    }
1027}
1028
1029impl From<Tensor<i64>> for TensorDyn {
1030    fn from(t: Tensor<i64>) -> Self {
1031        Self::I64(t)
1032    }
1033}
1034
1035impl From<Tensor<f16>> for TensorDyn {
1036    fn from(t: Tensor<f16>) -> Self {
1037        Self::F16(t)
1038    }
1039}
1040
1041impl From<Tensor<f32>> for TensorDyn {
1042    fn from(t: Tensor<f32>) -> Self {
1043        Self::F32(t)
1044    }
1045}
1046
1047impl From<Tensor<f64>> for TensorDyn {
1048    fn from(t: Tensor<f64>) -> Self {
1049        Self::F64(t)
1050    }
1051}
1052
1053impl fmt::Debug for TensorDyn {
1054    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1055        dispatch!(self, fmt, f)
1056    }
1057}
1058
1059#[cfg(test)]
1060mod tests {
1061    use super::*;
1062
1063    #[test]
1064    fn from_typed_tensor() {
1065        let t = Tensor::<u8>::new(&[10], None, None).unwrap();
1066        let dyn_t: TensorDyn = t.into();
1067        assert_eq!(dyn_t.dtype(), DType::U8);
1068        assert_eq!(dyn_t.shape(), &[10]);
1069    }
1070
1071    #[test]
1072    fn from_foreign_ptr_wraps_borrowed_memory() {
1073        use crate::TensorMapTrait;
1074        // The CUDA zero-copy export shape: wrap an externally-allocated buffer as
1075        // a type-erased Mem tensor, with an owner that frees it on last drop.
1076        let mut vec: Vec<f32> = vec![0.0; 4];
1077        let ptr = vec.as_mut_ptr() as *mut u8;
1078        let owner: crate::ForeignOwner = Box::new(vec);
1079        let t = unsafe {
1080            TensorDyn::from_foreign_ptr(ptr, &[2, 2], DType::F32, Some(owner), Some("trt_output"))
1081        }
1082        .unwrap();
1083        assert_eq!(t.dtype(), DType::F32);
1084        assert_eq!(t.memory(), TensorMemory::Mem);
1085        assert_eq!(t.shape(), &[2, 2]);
1086        {
1087            let mut m = t.as_f32().unwrap().map().unwrap();
1088            m.as_mut_slice().copy_from_slice(&[1.0, 2.0, 3.0, 4.0]);
1089        }
1090        let m = t.as_f32().unwrap().map().unwrap();
1091        assert_eq!(m.as_slice(), &[1.0, 2.0, 3.0, 4.0]);
1092    }
1093
1094    // -------------------------------------------------------------------------
1095    // TensorDyn::from_foreign_ptr guard paths.
1096    //
1097    // The happy path (F32) is covered by `from_foreign_ptr_wraps_borrowed_memory`
1098    // above. These cells add the null-ptr, empty-shape, and overflow guards, plus
1099    // a U8 dtype to confirm the match-arm dispatch is exercised for integer types.
1100    // -------------------------------------------------------------------------
1101
1102    #[test]
1103    fn from_foreign_ptr_rejects_null_ptr() {
1104        let err = unsafe {
1105            TensorDyn::from_foreign_ptr(std::ptr::null_mut(), &[4], DType::U8, None, None)
1106        }
1107        .unwrap_err();
1108        // The null guard fires inside Tensor<u8>::from_foreign.
1109        assert!(
1110            matches!(err, crate::error::Error::InvalidArgument(ref m) if m.contains("non-null")),
1111            "expected InvalidArgument(non-null), got {err:?}"
1112        );
1113    }
1114
1115    #[test]
1116    fn from_foreign_ptr_rejects_empty_shape() {
1117        let mut dummy: u8 = 0;
1118        let err = unsafe {
1119            TensorDyn::from_foreign_ptr(&mut dummy as *mut u8, &[], DType::U8, None, None)
1120        }
1121        .unwrap_err();
1122        assert!(
1123            matches!(err, crate::error::Error::InvalidSize(0)),
1124            "expected InvalidSize(0) for empty shape, got {err:?}"
1125        );
1126    }
1127
1128    #[test]
1129    fn from_foreign_ptr_rejects_overflow_shape() {
1130        let mut dummy: u8 = 0;
1131        let huge = [usize::MAX / 2 + 1, 2];
1132        let err = unsafe { TensorDyn::from_foreign_ptr(&mut dummy, &huge, DType::U8, None, None) }
1133            .unwrap_err();
1134        assert!(
1135            matches!(err, crate::error::Error::InvalidArgument(ref m) if m.contains("overflow")),
1136            "expected InvalidArgument(overflow), got {err:?}"
1137        );
1138    }
1139
1140    #[test]
1141    fn from_foreign_ptr_u8_dtype_dispatch() {
1142        // Exercises the U8 arm of from_foreign_ptr's match, which wraps
1143        // the raw pointer as Tensor<u8> and downcasts correctly.
1144        let mut buf: Vec<u8> = vec![1, 2, 3, 4];
1145        let ptr = buf.as_mut_ptr();
1146        let owner: crate::ForeignOwner = Box::new(buf);
1147        let t = unsafe {
1148            TensorDyn::from_foreign_ptr(ptr, &[4], DType::U8, Some(owner), Some("u8_foreign"))
1149        }
1150        .unwrap();
1151        assert_eq!(t.dtype(), DType::U8);
1152        assert_eq!(t.shape(), &[4]);
1153        let m = t.as_u8().unwrap().map().unwrap();
1154        use crate::TensorMapTrait;
1155        assert_eq!(m.as_slice(), &[1u8, 2, 3, 4]);
1156    }
1157
1158    #[test]
1159    fn downcast_ref() {
1160        let t = Tensor::<u8>::new(&[10], None, None).unwrap();
1161        let dyn_t: TensorDyn = t.into();
1162        assert!(dyn_t.as_u8().is_some());
1163        assert!(dyn_t.as_i8().is_none());
1164    }
1165
1166    #[test]
1167    fn downcast_into() {
1168        let t = Tensor::<u8>::new(&[10], None, None).unwrap();
1169        let dyn_t: TensorDyn = t.into();
1170        let back = dyn_t.into_u8().unwrap();
1171        assert_eq!(back.shape(), &[10]);
1172    }
1173
1174    #[test]
1175    fn image_accessors() {
1176        let t = Tensor::<u8>::image(
1177            640,
1178            480,
1179            PixelFormat::Rgba,
1180            None,
1181            crate::CpuAccess::ReadWrite,
1182        )
1183        .unwrap();
1184        let dyn_t: TensorDyn = t.into();
1185        assert_eq!(dyn_t.format(), Some(PixelFormat::Rgba));
1186        assert_eq!(dyn_t.width(), Some(640));
1187        assert_eq!(dyn_t.height(), Some(480));
1188        assert!(!dyn_t.is_multiplane());
1189    }
1190
1191    #[test]
1192    fn image_constructor() {
1193        let dyn_t = TensorDyn::image(
1194            640,
1195            480,
1196            PixelFormat::Rgb,
1197            DType::U8,
1198            None,
1199            crate::CpuAccess::ReadWrite,
1200        )
1201        .unwrap();
1202        assert_eq!(dyn_t.dtype(), DType::U8);
1203        assert_eq!(dyn_t.format(), Some(PixelFormat::Rgb));
1204        assert_eq!(dyn_t.width(), Some(640));
1205    }
1206
1207    #[test]
1208    fn image_constructor_i8() {
1209        let dyn_t = TensorDyn::image(
1210            640,
1211            480,
1212            PixelFormat::Rgb,
1213            DType::I8,
1214            None,
1215            crate::CpuAccess::ReadWrite,
1216        )
1217        .unwrap();
1218        assert_eq!(dyn_t.dtype(), DType::I8);
1219        assert_eq!(dyn_t.format(), Some(PixelFormat::Rgb));
1220    }
1221
1222    #[test]
1223    fn set_format_packed() {
1224        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
1225        assert_eq!(t.format(), None);
1226        t.set_format(PixelFormat::Rgb).unwrap();
1227        assert_eq!(t.format(), Some(PixelFormat::Rgb));
1228        assert_eq!(t.width(), Some(640));
1229        assert_eq!(t.height(), Some(480));
1230    }
1231
1232    #[test]
1233    fn set_format_planar() {
1234        let mut t = TensorDyn::new(&[3, 480, 640], DType::U8, None, None).unwrap();
1235        t.set_format(PixelFormat::PlanarRgb).unwrap();
1236        assert_eq!(t.format(), Some(PixelFormat::PlanarRgb));
1237        assert_eq!(t.width(), Some(640));
1238        assert_eq!(t.height(), Some(480));
1239    }
1240
1241    #[test]
1242    fn set_format_rejects_wrong_shape() {
1243        let mut t = TensorDyn::new(&[480, 640, 4], DType::U8, None, None).unwrap();
1244        assert!(t.set_format(PixelFormat::Rgb).is_err());
1245    }
1246
1247    #[test]
1248    fn with_format_builder() {
1249        let t = TensorDyn::new(&[480, 640, 4], DType::U8, None, None)
1250            .unwrap()
1251            .with_format(PixelFormat::Rgba)
1252            .unwrap();
1253        assert_eq!(t.format(), Some(PixelFormat::Rgba));
1254        assert_eq!(t.width(), Some(640));
1255        assert_eq!(t.height(), Some(480));
1256    }
1257
1258    #[cfg(target_os = "linux")]
1259    #[test]
1260    fn dmabuf_clone_mem_tensor_fails() {
1261        let t = TensorDyn::new(&[480, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
1262        assert_eq!(t.memory(), TensorMemory::Mem);
1263        assert!(t.dmabuf_clone().is_err());
1264    }
1265
1266    #[cfg(target_os = "linux")]
1267    #[test]
1268    fn dmabuf_mem_tensor_fails() {
1269        let t = TensorDyn::new(&[480, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
1270        assert!(t.dmabuf().is_err());
1271    }
1272
1273    #[test]
1274    fn set_format_semi_planar_nv12() {
1275        // 720 rows = 480 * 3/2 (NV12: height + height/2 for chroma)
1276        let mut t = TensorDyn::new(&[720, 640], DType::U8, Some(TensorMemory::Mem), None).unwrap();
1277        t.set_format(PixelFormat::Nv12).unwrap();
1278        assert_eq!(t.format(), Some(PixelFormat::Nv12));
1279        assert_eq!(t.width(), Some(640));
1280        assert_eq!(t.height(), Some(480));
1281    }
1282
1283    #[test]
1284    fn set_format_semi_planar_nv16() {
1285        // 960 rows = 480 * 2 (NV16: height + height for chroma)
1286        let mut t = TensorDyn::new(&[960, 640], DType::U8, Some(TensorMemory::Mem), None).unwrap();
1287        t.set_format(PixelFormat::Nv16).unwrap();
1288        assert_eq!(t.format(), Some(PixelFormat::Nv16));
1289        assert_eq!(t.width(), Some(640));
1290        assert_eq!(t.height(), Some(480));
1291    }
1292
1293    #[test]
1294    fn with_format_rejects_wrong_shape() {
1295        let result = TensorDyn::new(&[480, 640, 4], DType::U8, None, None)
1296            .unwrap()
1297            .with_format(PixelFormat::Rgb);
1298        assert!(result.is_err());
1299    }
1300
1301    #[test]
1302    fn set_format_preserved_after_rejection() {
1303        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
1304        t.set_format(PixelFormat::Rgb).unwrap();
1305        assert_eq!(t.format(), Some(PixelFormat::Rgb));
1306
1307        // Rgba requires 4 channels, should fail on a 3-channel tensor
1308        assert!(t.set_format(PixelFormat::Rgba).is_err());
1309
1310        // Original format should be preserved
1311        assert_eq!(t.format(), Some(PixelFormat::Rgb));
1312    }
1313
1314    #[test]
1315    fn set_format_idempotent() {
1316        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
1317        t.set_format(PixelFormat::Rgb).unwrap();
1318        t.set_format(PixelFormat::Rgb).unwrap();
1319        assert_eq!(t.format(), Some(PixelFormat::Rgb));
1320        assert_eq!(t.width(), Some(640));
1321        assert_eq!(t.height(), Some(480));
1322    }
1323
1324    // --- Row stride tests ---
1325
1326    #[test]
1327    fn set_row_stride_valid() {
1328        // RGBA 100px wide: min stride = 400, set 512
1329        let mut t = TensorDyn::image(
1330            100,
1331            100,
1332            PixelFormat::Rgba,
1333            DType::U8,
1334            None,
1335            crate::CpuAccess::ReadWrite,
1336        )
1337        .unwrap();
1338        t.set_row_stride(512).unwrap();
1339        assert_eq!(t.row_stride(), Some(512));
1340        assert_eq!(t.effective_row_stride(), Some(512));
1341    }
1342
1343    #[test]
1344    fn set_row_stride_equals_min() {
1345        // RGB 100px: min stride = 300, set exactly 300
1346        let mut t = TensorDyn::image(
1347            100,
1348            100,
1349            PixelFormat::Rgb,
1350            DType::U8,
1351            None,
1352            crate::CpuAccess::ReadWrite,
1353        )
1354        .unwrap();
1355        t.set_row_stride(300).unwrap();
1356        assert_eq!(t.row_stride(), Some(300));
1357    }
1358
1359    #[test]
1360    fn set_row_stride_too_small() {
1361        // RGBA 64px (a 64-aligned width: 64*4 = 256, already a multiple of 64)
1362        // carries no implicit stride. min stride = 256; setting 200 must error
1363        // and leave row_stride unset. (Non-64-aligned widths now record the
1364        // padded stride at allocation — see `Tensor::image`.)
1365        let mut t = TensorDyn::image(
1366            64,
1367            100,
1368            PixelFormat::Rgba,
1369            DType::U8,
1370            None,
1371            crate::CpuAccess::ReadWrite,
1372        )
1373        .unwrap();
1374        assert!(t.set_row_stride(200).is_err());
1375        assert_eq!(t.row_stride(), None);
1376    }
1377
1378    #[test]
1379    fn set_row_stride_zero() {
1380        let mut t = TensorDyn::image(
1381            100,
1382            100,
1383            PixelFormat::Rgb,
1384            DType::U8,
1385            None,
1386            crate::CpuAccess::ReadWrite,
1387        )
1388        .unwrap();
1389        assert!(t.set_row_stride(0).is_err());
1390    }
1391
1392    #[test]
1393    fn set_row_stride_requires_format() {
1394        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
1395        assert!(t.set_row_stride(2048).is_err());
1396    }
1397
1398    #[test]
1399    fn effective_row_stride_without_stride() {
1400        // A 64-aligned-width packed image carries no explicit stride; the
1401        // effective stride falls back to the computed tight pitch. (Width 64
1402        // RGB → 64*3 = 192, already a multiple of 64, so no padding is added.
1403        // Non-aligned widths now record the padded stride — see `Tensor::image`.)
1404        let t = TensorDyn::image(
1405            64,
1406            100,
1407            PixelFormat::Rgb,
1408            DType::U8,
1409            None,
1410            crate::CpuAccess::ReadWrite,
1411        )
1412        .unwrap();
1413        assert_eq!(t.row_stride(), None);
1414        assert_eq!(t.effective_row_stride(), Some(192)); // 64 * 3
1415    }
1416
1417    #[test]
1418    fn effective_row_stride_padded_packed_dma() {
1419        // A non-64-aligned packed width on a DMA buffer records the 64-aligned
1420        // stride so the EGLImage import is accepted by Mali/Vivante (RGB 100px:
1421        // 100*3 = 300 → padded to 320). This padding is DMA-specific — host-only
1422        // memory keeps the tight pitch — so skip when DMA is unavailable (e.g. CI
1423        // without dma_heap); the behaviour is also validated on-target.
1424        let t = match TensorDyn::image(
1425            100,
1426            100,
1427            PixelFormat::Rgb,
1428            DType::U8,
1429            Some(TensorMemory::Dma),
1430            crate::CpuAccess::ReadWrite,
1431        ) {
1432            Ok(t) if t.memory() == TensorMemory::Dma => t,
1433            _ => return,
1434        };
1435        assert_eq!(t.row_stride(), Some(320));
1436        assert_eq!(t.effective_row_stride(), Some(320));
1437    }
1438
1439    #[test]
1440    fn effective_row_stride_no_format() {
1441        let t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
1442        assert_eq!(t.effective_row_stride(), None);
1443    }
1444
1445    #[test]
1446    fn with_row_stride_builder() {
1447        let t = TensorDyn::image(
1448            100,
1449            100,
1450            PixelFormat::Rgba,
1451            DType::U8,
1452            None,
1453            crate::CpuAccess::ReadWrite,
1454        )
1455        .unwrap()
1456        .with_row_stride(512)
1457        .unwrap();
1458        assert_eq!(t.row_stride(), Some(512));
1459        assert_eq!(t.effective_row_stride(), Some(512));
1460    }
1461
1462    #[test]
1463    fn with_row_stride_rejects_small() {
1464        let result = TensorDyn::image(
1465            100,
1466            100,
1467            PixelFormat::Rgba,
1468            DType::U8,
1469            None,
1470            crate::CpuAccess::ReadWrite,
1471        )
1472        .unwrap()
1473        .with_row_stride(200);
1474        assert!(result.is_err());
1475    }
1476
1477    #[test]
1478    fn set_format_clears_row_stride() {
1479        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
1480        t.set_format(PixelFormat::Rgb).unwrap();
1481        t.set_row_stride(2048).unwrap();
1482        assert_eq!(t.row_stride(), Some(2048));
1483
1484        // Incompatible format change (4-chan on 3-chan shape) fails — stride preserved
1485        let _ = t.set_format(PixelFormat::Bgra);
1486        assert_eq!(t.row_stride(), Some(2048));
1487
1488        // Re-set to same format — stride preserved
1489        t.set_format(PixelFormat::Rgb).unwrap();
1490        assert_eq!(t.row_stride(), Some(2048));
1491
1492        // Reshape clears format and stride
1493        t.reshape(&[480 * 640 * 3]).unwrap();
1494        assert_eq!(t.row_stride(), None);
1495        assert_eq!(t.format(), None);
1496    }
1497
1498    #[test]
1499    fn set_format_different_compatible_clears_stride() {
1500        // RGBA and BGRA are both 4-channel packed — switching between them
1501        // succeeds and must clear the stored stride.
1502        let mut t = TensorDyn::new(&[480, 640, 4], DType::U8, None, None).unwrap();
1503        t.set_format(PixelFormat::Rgba).unwrap();
1504        t.set_row_stride(4096).unwrap();
1505        assert_eq!(t.row_stride(), Some(4096));
1506
1507        // Successful format change to a different compatible format clears stride
1508        t.set_format(PixelFormat::Bgra).unwrap();
1509        assert_eq!(t.format(), Some(PixelFormat::Bgra));
1510        assert_eq!(t.row_stride(), None);
1511    }
1512
1513    #[test]
1514    fn set_format_same_preserves_stride() {
1515        let mut t = TensorDyn::image(
1516            100,
1517            100,
1518            PixelFormat::Rgb,
1519            DType::U8,
1520            None,
1521            crate::CpuAccess::ReadWrite,
1522        )
1523        .unwrap();
1524        t.set_row_stride(512).unwrap();
1525        // Re-setting the same format should not clear stride
1526        t.set_format(PixelFormat::Rgb).unwrap();
1527        assert_eq!(t.row_stride(), Some(512));
1528    }
1529
1530    #[test]
1531    fn effective_row_stride_planar() {
1532        let t = TensorDyn::image(
1533            640,
1534            480,
1535            PixelFormat::PlanarRgb,
1536            DType::U8,
1537            None,
1538            crate::CpuAccess::ReadWrite,
1539        )
1540        .unwrap();
1541        assert_eq!(t.effective_row_stride(), Some(640)); // planar: width only
1542    }
1543
1544    #[test]
1545    fn effective_row_stride_nv12() {
1546        let t = TensorDyn::image(
1547            640,
1548            480,
1549            PixelFormat::Nv12,
1550            DType::U8,
1551            None,
1552            crate::CpuAccess::ReadWrite,
1553        )
1554        .unwrap();
1555        assert_eq!(t.effective_row_stride(), Some(640)); // semi-planar: width only
1556    }
1557
1558    #[test]
1559    fn map_rejects_strided_tensor() {
1560        let mut t = Tensor::<u8>::image(
1561            100,
1562            100,
1563            PixelFormat::Rgba,
1564            Some(TensorMemory::Mem),
1565            crate::CpuAccess::ReadWrite,
1566        )
1567        .unwrap();
1568        // Map works before stride is set
1569        assert!(t.map().is_ok());
1570        // After setting stride, map should be rejected
1571        t.set_row_stride(512).unwrap();
1572        let err = t.map();
1573        assert!(err.is_err());
1574    }
1575
1576    // ── plane_offset tests ──────────────────────────────────────────
1577
1578    #[test]
1579    fn plane_offset_default_none() {
1580        let t = TensorDyn::image(
1581            100,
1582            100,
1583            PixelFormat::Rgba,
1584            DType::U8,
1585            None,
1586            crate::CpuAccess::ReadWrite,
1587        )
1588        .unwrap();
1589        assert_eq!(t.plane_offset(), None);
1590    }
1591
1592    #[test]
1593    fn set_plane_offset_basic() {
1594        let mut t = TensorDyn::image(
1595            100,
1596            100,
1597            PixelFormat::Rgba,
1598            DType::U8,
1599            None,
1600            crate::CpuAccess::ReadWrite,
1601        )
1602        .unwrap();
1603        t.set_plane_offset(4096);
1604        assert_eq!(t.plane_offset(), Some(4096));
1605    }
1606
1607    #[test]
1608    fn set_plane_offset_zero() {
1609        let mut t = TensorDyn::image(
1610            100,
1611            100,
1612            PixelFormat::Rgb,
1613            DType::U8,
1614            None,
1615            crate::CpuAccess::ReadWrite,
1616        )
1617        .unwrap();
1618        t.set_plane_offset(0);
1619        assert_eq!(t.plane_offset(), Some(0));
1620    }
1621
1622    #[test]
1623    fn set_plane_offset_no_format() {
1624        // plane_offset does not require format (it is format-independent)
1625        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
1626        t.set_plane_offset(4096);
1627        assert_eq!(t.plane_offset(), Some(4096));
1628    }
1629
1630    #[test]
1631    fn set_format_clears_plane_offset() {
1632        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
1633        t.set_format(PixelFormat::Rgb).unwrap();
1634        t.set_plane_offset(4096);
1635        assert_eq!(t.plane_offset(), Some(4096));
1636
1637        // Re-set same format — offset preserved
1638        t.set_format(PixelFormat::Rgb).unwrap();
1639        assert_eq!(t.plane_offset(), Some(4096));
1640
1641        // Reshape clears everything
1642        t.reshape(&[480 * 640 * 3]).unwrap();
1643        assert_eq!(t.plane_offset(), None);
1644        assert_eq!(t.format(), None);
1645    }
1646
1647    #[test]
1648    fn map_rejects_out_of_bounds_offset() {
1649        let mut t = Tensor::<u8>::image(
1650            100,
1651            100,
1652            PixelFormat::Rgba,
1653            Some(TensorMemory::Mem),
1654            crate::CpuAccess::ReadWrite,
1655        )
1656        .unwrap();
1657        // Map works before offset is set.
1658        assert!(t.map().is_ok());
1659        // Heap offsets are now honored, but an offset that pushes the full
1660        // logical window (40000 bytes) past the allocation must be rejected.
1661        t.set_plane_offset(4096);
1662        assert!(t.map().is_err());
1663    }
1664
1665    #[test]
1666    fn mem_subview_in_bounds_maps_at_offset() {
1667        // An in-bounds heap sub-view now maps at its offset (previously every
1668        // non-zero heap offset was rejected outright).
1669        let parent = Tensor::<u8>::image(
1670            100,
1671            100,
1672            PixelFormat::Rgba,
1673            Some(TensorMemory::Mem),
1674            crate::CpuAccess::ReadWrite,
1675        )
1676        .unwrap();
1677        // A 10x10 RGBA window (400 bytes) at byte offset 4096 fits in 40000.
1678        let view = parent.subview(4096, &[10, 10, 4]).unwrap();
1679        assert_eq!(view.plane_offset(), Some(4096));
1680        assert!(view.map().is_ok());
1681    }
1682
1683    #[test]
1684    fn dyn_batch_dispatches_every_dtype() {
1685        // `TensorDyn::batch` fans out across all 11 dtype arms via `dyn_fanout!`;
1686        // exercise each so element `n` preserves the element type and shape.
1687        // A `[N=2, 4]` raw parent: element 1 is the contiguous 4-element window.
1688        use DType::*;
1689        for dt in [U8, I8, U16, I16, U32, I32, U64, I64, F16, F32, F64] {
1690            let parent = TensorDyn::new(&[2, 4], dt, Some(TensorMemory::Mem), None).unwrap();
1691            let view = parent.batch(1).unwrap();
1692            assert_eq!(view.dtype(), dt, "batch must preserve dtype {dt:?}");
1693            assert_eq!(view.shape(), &[4], "{dt:?}");
1694        }
1695    }
1696
1697    #[test]
1698    fn map_accepts_zero_offset_tensor() {
1699        let mut t = Tensor::<u8>::image(
1700            100,
1701            100,
1702            PixelFormat::Rgba,
1703            Some(TensorMemory::Mem),
1704            crate::CpuAccess::ReadWrite,
1705        )
1706        .unwrap();
1707        t.set_plane_offset(0);
1708        // Zero offset is fine for CPU mapping
1709        assert!(t.map().is_ok());
1710    }
1711
1712    #[test]
1713    fn dyn_configure_image_nv12() {
1714        let mut t = TensorDyn::image(
1715            640,
1716            480,
1717            PixelFormat::Rgb,
1718            DType::U8,
1719            None,
1720            crate::CpuAccess::ReadWrite,
1721        )
1722        .unwrap();
1723        t.configure_image(320, 240, PixelFormat::Nv12).unwrap();
1724        assert_eq!(t.format(), Some(PixelFormat::Nv12));
1725        assert_eq!((t.width(), t.height()), (Some(320), Some(240)));
1726    }
1727
1728    #[test]
1729    fn tensordyn_colorimetry_roundtrip() {
1730        use crate::{ColorEncoding, Colorimetry, DType, PixelFormat};
1731        let mut t = TensorDyn::image(
1732            1280,
1733            720,
1734            PixelFormat::Nv12,
1735            DType::U8,
1736            None,
1737            crate::CpuAccess::ReadWrite,
1738        )
1739        .unwrap();
1740        assert_eq!(t.colorimetry(), None);
1741        let c = Colorimetry::default().with_encoding(ColorEncoding::Bt709);
1742        t.set_colorimetry(Some(c));
1743        assert_eq!(t.colorimetry(), Some(c));
1744    }
1745
1746    #[test]
1747    fn from_planes_propagates_plane_offset() {
1748        let mut luma =
1749            Tensor::<u8>::new(&[480, 640], Some(TensorMemory::Mem), Some("luma")).unwrap();
1750        luma.set_plane_offset(4096);
1751        let chroma =
1752            Tensor::<u8>::new(&[240, 640], Some(TensorMemory::Mem), Some("chroma")).unwrap();
1753        let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
1754        assert_eq!(combined.plane_offset(), Some(4096));
1755    }
1756
1757    #[test]
1758    fn cuda_passthrough_none_for_mem_tensor() {
1759        // Build a Mem-backed dynamic tensor the same way the other tests here do,
1760        // then confirm the CUDA accessors pass through to None (no handle).
1761        let t: TensorDyn = Tensor::<f32>::new(&[10], Some(TensorMemory::Mem), None)
1762            .unwrap()
1763            .into();
1764        assert!(t.cuda().is_none());
1765        assert!(t.cuda_map().is_none());
1766    }
1767}