Skip to main content

edgefirst_tensor/tensor_dyn/
static_backend.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.
36/// Rebuild a [`TensorDyn`] under a different variant of the **same element
37/// width**, moving the storage across untouched.
38///
39/// Every `Tensor<T>` is layout-identical across same-width `T`: it holds
40/// type-erased storage plus a `PhantomData`, with no inline `T` and no
41/// `T`-dependent drop glue. `edgefirst-image` performs the same transmute
42/// at two of its own call sites and pins that with `const` assertions; this
43/// is the same operation, written once where the invariant is documented.
44///
45/// **Contains no panicking operation**, deliberately: [`TensorDyn::set_dtype`]
46/// calls it between a `ptr::read` and a `ptr::write`, and an unwind in that
47/// window would double-drop. The final arm therefore returns its input
48/// unchanged instead of asserting — unreachable via `set_dtype`, which
49/// rejects a width change before calling, and harmless if it ever were
50/// reached (the tensor keeps the dtype it had).
51///
52/// # Safety
53///
54/// `dtype.size()` must equal `t`'s current dtype size.
55// Each width group generates one arm whose target type equals its source --
56// `U8 => u8` reached from a `U8` source, and so on. `transmute`-to-self is a
57// no-op the compiler removes; special-casing it would mean an extra match
58// arm per group and an extra branch to read, for no behavioural difference.
59#[allow(clippy::useless_transmute)]
60unsafe fn retag_same_width(t: TensorDyn, dtype: DType) -> TensorDyn {
61    /// One width group: `$src` is the source variant's element type and
62    /// `$same` its variant, followed by every target variant of that width.
63    ///
64    /// `transmute` names **both** type parameters. `<_, Tensor<$ty>>`
65    /// compiled and worked but tripped
66    /// `clippy::missing_transmute_annotations`, a lint that exists because
67    /// an inferred source type silently follows whatever the call site
68    /// later becomes -- exactly the drift a transmute must not have.
69    macro_rules! group {
70        ($inner:expr, $src:ty, $same:ident, $( $d:ident => $ty:ty ),+ $(,)?) => {
71            match dtype {
72                $(
73                    // SAFETY: the caller guarantees equal element width, and
74                    // same-width `Tensor<T>` are layout-identical.
75                    DType::$d => TensorDyn::$d(unsafe {
76                        std::mem::transmute::<Tensor<$src>, Tensor<$ty>>($inner)
77                    }),
78                )+
79                _ => TensorDyn::$same($inner),
80            }
81        };
82    }
83    match t {
84        TensorDyn::U8(i) => group!(i, u8, U8, U8 => u8, I8 => i8),
85        TensorDyn::I8(i) => group!(i, i8, I8, U8 => u8, I8 => i8),
86        TensorDyn::U16(i) => group!(i, u16, U16, U16 => u16, I16 => i16, F16 => f16),
87        TensorDyn::I16(i) => group!(i, i16, I16, U16 => u16, I16 => i16, F16 => f16),
88        TensorDyn::F16(i) => group!(i, f16, F16, U16 => u16, I16 => i16, F16 => f16),
89        TensorDyn::U32(i) => group!(i, u32, U32, U32 => u32, I32 => i32, F32 => f32),
90        TensorDyn::I32(i) => group!(i, i32, I32, U32 => u32, I32 => i32, F32 => f32),
91        TensorDyn::F32(i) => group!(i, f32, F32, U32 => u32, I32 => i32, F32 => f32),
92        TensorDyn::U64(i) => group!(i, u64, U64, U64 => u64, I64 => i64, F64 => f64),
93        TensorDyn::I64(i) => group!(i, i64, I64, U64 => u64, I64 => i64, F64 => f64),
94        TensorDyn::F64(i) => group!(i, f64, F64, U64 => u64, I64 => i64, F64 => f64),
95    }
96}
97
98macro_rules! dispatch {
99    ($self:expr, $method:ident $(, $arg:expr)*) => {
100        match $self {
101            TensorDyn::U8(t) => t.$method($($arg),*),
102            TensorDyn::I8(t) => t.$method($($arg),*),
103            TensorDyn::U16(t) => t.$method($($arg),*),
104            TensorDyn::I16(t) => t.$method($($arg),*),
105            TensorDyn::U32(t) => t.$method($($arg),*),
106            TensorDyn::I32(t) => t.$method($($arg),*),
107            TensorDyn::U64(t) => t.$method($($arg),*),
108            TensorDyn::I64(t) => t.$method($($arg),*),
109            TensorDyn::F16(t) => t.$method($($arg),*),
110            TensorDyn::F32(t) => t.$method($($arg),*),
111            TensorDyn::F64(t) => t.$method($($arg),*),
112        }
113    };
114}
115
116/// Like [`dispatch!`], but for methods returning `Result<Tensor<T>>`: rewrap the
117/// typed result back into the matching `TensorDyn` variant. Keeps sub-region
118/// fan-out (`batch`, future `view`) to one line instead of an 11-arm match.
119macro_rules! dyn_fanout {
120    ($self:expr, $method:ident $(, $arg:expr)*) => {
121        match $self {
122            TensorDyn::U8(t) => t.$method($($arg),*).map(TensorDyn::U8),
123            TensorDyn::I8(t) => t.$method($($arg),*).map(TensorDyn::I8),
124            TensorDyn::U16(t) => t.$method($($arg),*).map(TensorDyn::U16),
125            TensorDyn::I16(t) => t.$method($($arg),*).map(TensorDyn::I16),
126            TensorDyn::U32(t) => t.$method($($arg),*).map(TensorDyn::U32),
127            TensorDyn::I32(t) => t.$method($($arg),*).map(TensorDyn::I32),
128            TensorDyn::U64(t) => t.$method($($arg),*).map(TensorDyn::U64),
129            TensorDyn::I64(t) => t.$method($($arg),*).map(TensorDyn::I64),
130            TensorDyn::F16(t) => t.$method($($arg),*).map(TensorDyn::F16),
131            TensorDyn::F32(t) => t.$method($($arg),*).map(TensorDyn::F32),
132            TensorDyn::F64(t) => t.$method($($arg),*).map(TensorDyn::F64),
133        }
134    };
135}
136
137/// Generate the three downcast methods (ref, mut ref, owned) for one variant.
138macro_rules! downcast_methods {
139    ($variant:ident, $ty:ty, $as_name:ident, $as_mut_name:ident, $into_name:ident) => {
140        /// Returns a shared reference to the inner tensor if the type matches.
141        pub fn $as_name(&self) -> Option<&Tensor<$ty>> {
142            match self {
143                Self::$variant(t) => Some(t),
144                _ => None,
145            }
146        }
147
148        /// Returns a mutable reference to the inner tensor if the type matches.
149        pub fn $as_mut_name(&mut self) -> Option<&mut Tensor<$ty>> {
150            match self {
151                Self::$variant(t) => Some(t),
152                _ => None,
153            }
154        }
155
156        /// Unwraps the inner tensor if the type matches, otherwise returns `self` as `Err`.
157        /// The Err variant is necessarily large (returns the unconsumed TensorDyn).
158        #[allow(clippy::result_large_err)]
159        pub fn $into_name(self) -> Result<Tensor<$ty>, Self> {
160            match self {
161                Self::$variant(t) => Ok(t),
162                other => Err(other),
163            }
164        }
165    };
166}
167
168impl TensorDyn {
169    /// Return the runtime element type discriminant.
170    pub fn dtype(&self) -> DType {
171        match self {
172            Self::U8(_) => DType::U8,
173            Self::I8(_) => DType::I8,
174            Self::U16(_) => DType::U16,
175            Self::I16(_) => DType::I16,
176            Self::U32(_) => DType::U32,
177            Self::I32(_) => DType::I32,
178            Self::U64(_) => DType::U64,
179            Self::I64(_) => DType::I64,
180            Self::F16(_) => DType::F16,
181            Self::F32(_) => DType::F32,
182            Self::F64(_) => DType::F64,
183        }
184    }
185
186    /// Return the tensor shape.
187    /// Cross-package protocol descriptor (see [`crate::protocol`]).
188    ///
189    /// This is the contract between independently-linked EdgeFirst packages:
190    /// they cannot share a Rust or PyO3 type, so they exchange this instead.
191    /// Pin a stable host address for this tensor's data.
192    ///
193    /// See [`Tensor::pin_host`](crate::Tensor::pin_host). Dispatches to the
194    /// concrete element type behind the erasure.
195    /// Returns a `'static` pin.
196    ///
197    /// `TensorDyn` erases to a fixed set of concrete numeric element types, all
198    /// of which are `'static`, so the keepalive genuinely can outlive any
199    /// borrow — which is what lets a pin cross into a `PyCapsule` handed to
200    /// Python. Eliding to `&self`'s lifetime here would make the pin escape its
201    /// own producer.
202    pub fn pin_host(&self, access: crate::CpuAccess) -> crate::Result<crate::HostPin<'static>> {
203        dispatch!(self, pin_host, access)
204    }
205
206    /// Map this tensor's whole extent for CPU access, type-erased to raw
207    /// bytes. Returns a `'static` view.
208    ///
209    /// `TensorDyn` has no byte-level counterpart to
210    /// [`TensorTrait::map_with`](crate::TensorTrait::map_with) — every typed
211    /// `Tensor<T>` maps to `HostView<'_, T>`, and a type-erased caller (a C
212    /// ABI consumer, which does not know `T`) cannot use that. This dispatches
213    /// exactly the way [`pin_host`](Self::pin_host) and the other `dispatch!`
214    /// accessors above do — one arm per concrete element type, calling the
215    /// same [`TensorTrait::map_with`](crate::TensorTrait::map_with) each typed
216    /// `Tensor<T>` already implements (with its declared-vs-requested
217    /// telemetry, row-stride bookkeeping, and plane-offset handling intact) —
218    /// then re-views the resulting `HostView<'_, T>` as `HostView<'_, u8>` via
219    /// [`HostView::into_bytes`](crate::view::HostView::into_bytes), which
220    /// keeps the same underlying pin (so the same platform sync bracket still
221    /// runs on `Drop`) and just restates element count as byte count.
222    ///
223    /// The `'static` lifetime is the same story as [`pin_host`](Self::pin_host):
224    /// `map_with`'s `'a` is a free parameter bounding how long the backing
225    /// allocation lives, not a borrow of `self` — the returned view shares
226    /// ownership through its pin's keepalive `Arc`, never a reference into
227    /// this tensor — and `TensorDyn` erases to a fixed set of concrete
228    /// element types that are all `'static`, so nothing stops choosing `'a =
229    /// 'static` here. Eliding to `&self`'s lifetime instead would tie the
230    /// view to this call's borrow for no reason, forcing every caller (a C
231    /// ABI entry point chief among them) into an artificial self-referential
232    /// shape to hold onto it past this function returning.
233    ///
234    /// # Errors
235    /// Same as `map_with`: `Error::InvalidArgument` for `CpuAccess::None`,
236    /// plus whatever the concrete backend's map can fail with (capacity,
237    /// stride, or platform-lock errors).
238    pub fn map_bytes(
239        &self,
240        access: crate::CpuAccess,
241    ) -> crate::Result<crate::view::HostView<'static, u8>> {
242        use crate::TensorTrait;
243        match self {
244            Self::U8(t) => t.map_with(access).map(crate::view::HostView::into_bytes),
245            Self::I8(t) => t.map_with(access).map(crate::view::HostView::into_bytes),
246            Self::U16(t) => t.map_with(access).map(crate::view::HostView::into_bytes),
247            Self::I16(t) => t.map_with(access).map(crate::view::HostView::into_bytes),
248            Self::U32(t) => t.map_with(access).map(crate::view::HostView::into_bytes),
249            Self::I32(t) => t.map_with(access).map(crate::view::HostView::into_bytes),
250            Self::U64(t) => t.map_with(access).map(crate::view::HostView::into_bytes),
251            Self::I64(t) => t.map_with(access).map(crate::view::HostView::into_bytes),
252            Self::F16(t) => t.map_with(access).map(crate::view::HostView::into_bytes),
253            Self::F32(t) => t.map_with(access).map(crate::view::HostView::into_bytes),
254            Self::F64(t) => t.map_with(access).map(crate::view::HostView::into_bytes),
255        }
256    }
257
258    /// See [`Tensor::sync_for_cpu`](crate::Tensor::sync_for_cpu).
259    pub fn sync_for_cpu(&self, access: crate::CpuAccess) -> crate::Result<()> {
260        dispatch!(self, sync_for_cpu, access)
261    }
262
263    /// See [`Tensor::sync_for_device`](crate::Tensor::sync_for_device).
264    pub fn sync_for_device(&self, access: crate::CpuAccess) -> crate::Result<()> {
265        dispatch!(self, sync_for_device, access)
266    }
267
268    pub fn descriptor(&self) -> crate::TensorDesc {
269        self.descriptor_pinned(None)
270    }
271
272    /// Descriptor carrying a pinned host address.
273    ///
274    /// A bare [`descriptor`](Self::descriptor) is *descriptive only*: a
275    /// consumer learns the shape, format and backing kind but cannot reach the
276    /// bytes. Supplying a [`HostPin`](crate::HostPin) fills in `ptr`, and the
277    /// caller is responsible for keeping that pin alive at least as long as
278    /// the descriptor is used — which is what the capsule keepalive does.
279    pub fn descriptor_pinned(&self, pin: Option<&crate::HostPin<'_>>) -> crate::TensorDesc {
280        // Native handle: the consumer re-imports zero-copy from this when
281        // present. `-1` (the [`TensorDesc::handle`] "unused" sentinel)
282        // covers every backend/platform combination not listed here (Mem,
283        // Shm, and Dma on platforms without a dedicated arm below).
284        let handle: i64 = match self.memory() {
285            #[cfg(target_os = "linux")]
286            TensorMemory::DmaBuf => {
287                use std::os::fd::AsRawFd;
288                self.dmabuf().map(|fd| fd.as_raw_fd() as i64).unwrap_or(-1)
289            }
290            #[cfg(any(target_os = "macos", target_os = "ios"))]
291            TensorMemory::DmaBuf => self.iosurface_id().map(|id| id as i64).unwrap_or(-1),
292            TensorMemory::Pbo => self.pbo_id().map(|id| id as i64).unwrap_or(-1),
293            _ => -1,
294        };
295        crate::protocol::from_parts(crate::protocol::DescParts {
296            dims: self.shape(),
297            memory: self.memory(),
298            dtype: self.dtype(),
299            fourcc: self.format().map(|f| f.to_fourcc()).unwrap_or(0),
300            format: self.format(),
301            row_stride: self.row_stride(),
302            handle,
303            colorimetry: self.colorimetry().map(|c| c.pack()).unwrap_or(0),
304            capacity: self.capacity_bytes() as u64,
305            pin,
306            pbo_vtable_ptr: self.pbo_vtable_ptr(),
307        })
308    }
309
310    pub fn shape(&self) -> &[usize] {
311        dispatch!(self, shape)
312    }
313
314    /// Set the logical shape to any shape whose bytes fit the allocation,
315    /// without [`reshape`](Self::reshape)'s equal-count constraint. See
316    /// [`crate::TensorTrait::set_logical_shape`].
317    pub fn set_logical_shape(&mut self, shape: &[usize]) -> crate::Result<()> {
318        dispatch!(self, set_logical_shape, shape)
319    }
320
321    /// See [`Tensor::capacity_bytes`](crate::Tensor::capacity_bytes).
322    pub fn capacity_bytes(&self) -> usize {
323        dispatch!(self, capacity_bytes)
324    }
325
326    /// Return the tensor name.
327    pub fn name(&self) -> String {
328        dispatch!(self, name)
329    }
330
331    /// Return the pixel format (None if not an image tensor).
332    pub fn format(&self) -> Option<PixelFormat> {
333        dispatch!(self, format)
334    }
335
336    /// Return the image width (None if not an image tensor).
337    pub fn width(&self) -> Option<usize> {
338        dispatch!(self, width)
339    }
340
341    /// Return the image height (None if not an image tensor).
342    pub fn height(&self) -> Option<usize> {
343        dispatch!(self, height)
344    }
345
346    /// Return the total size of this tensor in bytes.
347    pub fn size(&self) -> usize {
348        dispatch!(self, size)
349    }
350
351    /// Retag this tensor's element type, keeping its bytes untouched.
352    ///
353    /// The recorded dtype is metadata over the same allocation: `U8` and
354    /// `I8` address identical bytes and differ only in how a consumer reads
355    /// them. `edgefirst-image` relies on exactly that -- a PBO or DMA buffer
356    /// is allocated as `u8` and handed back as `i8`, with the int8 shader
357    /// applying an XOR 0x80 bias over the same buffer.
358    ///
359    /// Refuses a dtype of a different width. That is not a retag but a
360    /// reinterpretation: `len()` and `size()` are derived from the shape and
361    /// the element width, so widening or narrowing here would silently
362    /// change how many elements the tensor claims to hold over an allocation
363    /// whose size did not change.
364    ///
365    /// # Errors
366    ///
367    /// [`crate::Error::InvalidArgument`] when `dtype.size()` differs from
368    /// the current dtype's. The tensor is left untouched.
369    pub fn set_dtype(&mut self, dtype: DType) -> crate::Result<()> {
370        let current = self.dtype();
371        if current == dtype {
372            return Ok(());
373        }
374        if current.size() != dtype.size() {
375            return Err(crate::Error::InvalidArgument(format!(
376                "set_dtype: {current:?} and {dtype:?} are different widths \
377                 ({} vs {} bytes); retagging would change the element count \
378                 over an allocation whose size did not change",
379                current.size(),
380                dtype.size()
381            )));
382        }
383        // SAFETY: `read` duplicates the value and `write` stores exactly one
384        // back, so the duplicate is never dropped twice -- provided nothing
385        // between them can unwind. `retag_same_width` is a `match` of
386        // `transmute`s with no panicking operation anywhere in it (its
387        // width-mismatch arm returns its input unchanged rather than
388        // panicking, precisely so this window has no unwind path), and
389        // `self` is not observed in between.
390        unsafe {
391            let taken = std::ptr::read(self);
392            std::ptr::write(self, retag_same_width(taken, dtype));
393        }
394        Ok(())
395    }
396
397    /// Return the memory allocation type.
398    pub fn memory(&self) -> TensorMemory {
399        dispatch!(self, memory)
400    }
401
402    /// The CPU access declared for this tensor at allocation.
403    ///
404    /// See [`Tensor::cpu_access`](crate::Tensor::cpu_access); dispatches to
405    /// the concrete element type behind the erasure, the same way
406    /// [`memory`](Self::memory) and [`dtype`](Self::dtype) do. `.writes()`
407    /// on the result is the declaration query a type-erased caller (a C ABI
408    /// consumer deciding whether a writable map is allowed) needs before
409    /// mapping -- unlike [`TensorMapTrait::is_writable`](crate::TensorMapTrait::is_writable),
410    /// which answers a different question about an *already-open* map guard
411    /// ("was this particular mapping opened writable"), this answers it about
412    /// the tensor itself, before any map exists.
413    pub fn cpu_access(&self) -> crate::CpuAccess {
414        dispatch!(self, cpu_access)
415    }
416
417    /// Reshape this tensor. Total element count must remain the same.
418    pub fn reshape(&mut self, shape: &[usize]) -> crate::Result<()> {
419        dispatch!(self, reshape, shape)
420    }
421
422    /// Attach pixel format metadata to this tensor.
423    ///
424    /// Validates that the tensor's shape is compatible with the format's
425    /// layout (packed, planar, or semi-planar).
426    ///
427    /// # Arguments
428    ///
429    /// * `format` - The pixel format to attach
430    ///
431    /// # Returns
432    ///
433    /// `Ok(())` on success, with the format stored as metadata on the tensor.
434    ///
435    /// # Errors
436    ///
437    /// Returns `Error::InvalidShape` if the tensor shape doesn't match
438    /// the expected layout for the given format.
439    pub fn set_format(&mut self, format: PixelFormat) -> crate::Result<()> {
440        dispatch!(self, set_format, format)
441    }
442
443    /// Attach pixel format metadata, consuming and returning self.
444    ///
445    /// Enables builder-style chaining.
446    ///
447    /// # Arguments
448    ///
449    /// * `format` - The pixel format to attach
450    ///
451    /// # Returns
452    ///
453    /// The tensor with format metadata attached.
454    ///
455    /// # Errors
456    ///
457    /// Returns `Error::InvalidShape` if the tensor shape doesn't match
458    /// the expected layout for the given format.
459    pub fn with_format(mut self, format: PixelFormat) -> crate::Result<Self> {
460        self.set_format(format)?;
461        Ok(self)
462    }
463
464    /// Colorimetry metadata (`None` = undefined; never auto-filled).
465    pub fn colorimetry(&self) -> Option<crate::Colorimetry> {
466        dispatch!(self, colorimetry)
467    }
468
469    /// Attach/clear colorimetry metadata.
470    pub fn set_colorimetry(&mut self, c: Option<crate::Colorimetry>) {
471        dispatch!(self, set_colorimetry, c)
472    }
473
474    /// Builder-style colorimetry attach (consumes and returns self).
475    pub fn with_colorimetry(mut self, c: crate::Colorimetry) -> Self {
476        self.set_colorimetry(Some(c));
477        self
478    }
479
480    /// Row stride in bytes (`None` = tightly packed).
481    pub fn row_stride(&self) -> Option<usize> {
482        dispatch!(self, row_stride)
483    }
484
485    /// Effective row stride: stored stride or computed from format and width.
486    pub fn effective_row_stride(&self) -> Option<usize> {
487        dispatch!(self, effective_row_stride)
488    }
489
490    /// Set logical dimensions + format to a decoded image, reusing the
491    /// allocation. See [`Tensor::configure_image`].
492    pub fn configure_image(
493        &mut self,
494        width: usize,
495        height: usize,
496        format: PixelFormat,
497    ) -> crate::Result<()> {
498        dispatch!(self, configure_image, width, height, format)
499    }
500
501    /// Set the row stride in bytes for externally allocated buffers with
502    /// row padding.
503    ///
504    /// Must be called before the tensor is first used for rendering. The
505    /// format must be set before calling this method.
506    pub fn set_row_stride(&mut self, stride: usize) -> crate::Result<()> {
507        dispatch!(self, set_row_stride, stride)
508    }
509
510    /// Builder-style: set row stride, consuming and returning self.
511    pub fn with_row_stride(mut self, stride: usize) -> crate::Result<Self> {
512        self.set_row_stride(stride)?;
513        Ok(self)
514    }
515
516    /// Set the row stride in bytes without format validation. See
517    /// [`Tensor::set_row_stride_unchecked`]. Added for `tensor-capi`'s
518    /// `ef_tensor_set_row_stride_unchecked` (task 17): the `dynamic` backend
519    /// needs a type-erased entry point the same way [`Self::set_row_stride`]
520    /// already gives it one for the checked setter.
521    pub fn set_row_stride_unchecked(&mut self, stride: usize) {
522        dispatch!(self, set_row_stride_unchecked, stride)
523    }
524
525    /// Byte offset within the DMA-BUF where image data starts (`None` = 0).
526    pub fn plane_offset(&self) -> Option<usize> {
527        dispatch!(self, plane_offset)
528    }
529
530    /// The parent-image snapshot if this tensor is a [`view`](Self::view)/
531    /// [`batch`](Self::batch) sub-region; `None` for a whole tensor. See
532    /// [`Tensor::view_origin`].
533    pub fn view_origin(&self) -> Option<crate::ViewOrigin> {
534        dispatch!(self, view_origin)
535    }
536
537    /// Set the byte offset within the DMA-BUF where image data starts.
538    pub fn set_plane_offset(&mut self, offset: usize) {
539        dispatch!(self, set_plane_offset, offset)
540    }
541
542    /// Borrow batch element `n` of a batched tensor (leading `N` dimension) as a
543    /// zero-copy view sharing this tensor's allocation. See [`Tensor::batch`].
544    pub fn batch(&self, n: usize) -> crate::Result<TensorDyn> {
545        dyn_fanout!(self, batch, n)
546    }
547
548    /// Borrow a rectangular spatial sub-region (the destination/source crop) as
549    /// a zero-copy view sharing this tensor's allocation. See [`Tensor::view`].
550    pub fn view(&self, region: crate::Region) -> crate::Result<TensorDyn> {
551        dyn_fanout!(self, view, region)
552    }
553
554    /// The CUDA registration for this tensor, if any.
555    ///
556    /// Returns `None` when no CUDA handle has been attached (the common non-CUDA case).
557    /// This check is a pure local field read — no thread routing occurs.
558    pub fn cuda(&self) -> Option<&crate::cuda::CudaHandle> {
559        dispatch!(self, cuda)
560    }
561
562    /// Fast-fail CUDA map: `None` when no handle is attached; else maps the
563    /// PBO through the GL worker and returns a scoped device-pointer guard.
564    ///
565    /// The same try-`cuda_map`-then-[`map`](crate::TensorTrait::map) fallback pattern that applies to
566    /// [`Tensor::cuda_map`](crate::Tensor::cuda_map) applies here: call `cuda_map()` first for a
567    /// zero-copy device pointer; when it returns `None` (no CUDA handle attached), fall back to the
568    /// typed host mapping via the inner tensor.
569    ///
570    /// # Example
571    ///
572    /// ```no_run
573    /// use edgefirst_tensor::TensorDyn;
574    /// # fn feed_tensorrt(_dptr: *mut std::ffi::c_void, _bytes: usize) {}
575    /// # fn demo(t: &TensorDyn) {
576    /// if let Some(cuda) = t.cuda_map() {
577    ///     feed_tensorrt(cuda.device_ptr(), cuda.len());
578    /// } else {
579    ///     // No CUDA handle — use the typed inner tensor for host access.
580    ///     // See Tensor::cuda_map for the full fallback example.
581    /// }
582    /// # }
583    /// ```
584    pub fn cuda_map(&self) -> Option<crate::cuda::CudaMap<'_>> {
585        dispatch!(self, cuda_map)
586    }
587
588    /// Quantization metadata. Returns `None` for float variants (F16, F32,
589    /// F64) — quantization does not apply to floating-point tensors.
590    /// Otherwise delegates to the typed `Tensor<T>::quantization()` accessor.
591    pub fn quantization(&self) -> Option<&crate::Quantization> {
592        match self {
593            Self::U8(t) => t.quantization(),
594            Self::I8(t) => t.quantization(),
595            Self::U16(t) => t.quantization(),
596            Self::I16(t) => t.quantization(),
597            Self::U32(t) => t.quantization(),
598            Self::I32(t) => t.quantization(),
599            Self::U64(t) => t.quantization(),
600            Self::I64(t) => t.quantization(),
601            Self::F16(_) | Self::F32(_) | Self::F64(_) => None,
602        }
603    }
604
605    /// Attach quantization metadata. Fails on float variants with
606    /// [`crate::Error::QuantizationInvalid`]; delegates to the typed setter for
607    /// integer variants.
608    pub fn set_quantization(&mut self, q: crate::Quantization) -> crate::Result<()> {
609        match self {
610            Self::U8(t) => t.set_quantization(q),
611            Self::I8(t) => t.set_quantization(q),
612            Self::U16(t) => t.set_quantization(q),
613            Self::I16(t) => t.set_quantization(q),
614            Self::U32(t) => t.set_quantization(q),
615            Self::I32(t) => t.set_quantization(q),
616            Self::U64(t) => t.set_quantization(q),
617            Self::I64(t) => t.set_quantization(q),
618            Self::F16(_) | Self::F32(_) | Self::F64(_) => Err(crate::Error::QuantizationInvalid {
619                field: "dtype_is_integer",
620                expected: "integer tensor dtype (u8/i8/u16/i16/u32/i32/u64/i64)".to_string(),
621                got: format!("{:?}", self.dtype()),
622            }),
623        }
624    }
625
626    /// Builder-style variant of [`Self::set_quantization`]. Consumes self
627    /// and returns it with quantization applied (or the original error).
628    pub fn with_quantization(mut self, q: crate::Quantization) -> crate::Result<Self> {
629        self.set_quantization(q)?;
630        Ok(self)
631    }
632
633    /// Clear any quantization metadata. No-op on float variants.
634    pub fn clear_quantization(&mut self) {
635        match self {
636            Self::U8(t) => t.clear_quantization(),
637            Self::I8(t) => t.clear_quantization(),
638            Self::U16(t) => t.clear_quantization(),
639            Self::I16(t) => t.clear_quantization(),
640            Self::U32(t) => t.clear_quantization(),
641            Self::I32(t) => t.clear_quantization(),
642            Self::U64(t) => t.clear_quantization(),
643            Self::I64(t) => t.clear_quantization(),
644            Self::F16(_) | Self::F32(_) | Self::F64(_) => {}
645        }
646    }
647
648    /// Clone the file descriptor associated with this tensor.
649    #[cfg(unix)]
650    pub fn clone_fd(&self) -> crate::Result<std::os::fd::OwnedFd> {
651        dispatch!(self, clone_fd)
652    }
653
654    /// Clone the DMA-BUF file descriptor backing this tensor (Linux only).
655    ///
656    /// # Returns
657    ///
658    /// An owned duplicate of the DMA-BUF file descriptor.
659    ///
660    /// # Errors
661    ///
662    /// * `Error::NotImplemented` if the tensor is not DMA-backed (Mem/Shm/Pbo)
663    /// * `Error::IoError` if the fd clone syscall fails (e.g., fd limit reached)
664    #[cfg(target_os = "linux")]
665    pub fn dmabuf_clone(&self) -> crate::Result<std::os::fd::OwnedFd> {
666        if self.memory() != TensorMemory::DmaBuf {
667            return Err(crate::Error::NotImplemented(format!(
668                "dmabuf_clone requires DMA-backed tensor, got {:?}",
669                self.memory()
670            )));
671        }
672        self.clone_fd()
673    }
674
675    /// Borrow the DMA-BUF file descriptor backing this tensor (Linux only).
676    ///
677    /// # Returns
678    ///
679    /// A borrowed reference to the DMA-BUF file descriptor, tied to `self`'s
680    /// lifetime.
681    ///
682    /// # Errors
683    ///
684    /// * `Error::NotImplemented` if the tensor is not DMA-backed
685    #[cfg(target_os = "linux")]
686    pub fn dmabuf(&self) -> crate::Result<std::os::fd::BorrowedFd<'_>> {
687        dispatch!(self, dmabuf)
688    }
689
690    /// Return `true` if this tensor uses separate plane allocations.
691    pub fn is_multiplane(&self) -> bool {
692        dispatch!(self, is_multiplane)
693    }
694
695    /// Return the [`BufferIdentity`](crate::BufferIdentity) of the underlying
696    /// allocation.
697    ///
698    /// Two `TensorDyn` values share a [`crate::BufferIdentity::id`] iff they were
699    /// produced by cloning the same allocation (e.g. through
700    /// `DmaTensor::try_clone`). Separate
701    /// imports of the same physical buffer (e.g. two `from_fd` calls on the
702    /// same dmabuf fd) have **distinct** identities — use
703    /// [`aliases`](Self::aliases) if you need to detect that case.
704    pub fn buffer_identity(&self) -> &crate::BufferIdentity {
705        dispatch!(self, buffer_identity)
706    }
707
708    /// Return `true` if `self` and `other` reference the same underlying
709    /// buffer.
710    ///
711    /// This is the correct check for APIs that require distinct input and
712    /// output tensors (e.g. `ImageProcessor::draw_decoded_masks`, where
713    /// aliasing `dst` and `background` would cause the GL backend to read
714    /// and write the same texture — undefined behaviour on most drivers).
715    ///
716    /// Matching is conservative:
717    /// 1. Matching [`crate::BufferIdentity::id`] → same buffer (always).
718    /// 2. Matching backing type + matching dmabuf fd number (Linux, DMA
719    ///    tensors only) → same buffer, even across separate `from_fd`
720    ///    imports in the same process.
721    ///
722    /// Two distinct `dup`'d fds pointing at the same kernel dma-buf are
723    /// **not** detected — there is no cheap way to resolve that without a
724    /// round-trip through the kernel.
725    pub fn aliases(&self, other: &Self) -> bool {
726        if self.buffer_identity().id() == other.buffer_identity().id() {
727            return true;
728        }
729        if self.memory() != other.memory() {
730            return false;
731        }
732        #[cfg(target_os = "linux")]
733        if self.memory() == TensorMemory::DmaBuf {
734            use std::os::fd::AsRawFd;
735            if let (Ok(a), Ok(b)) = (self.dmabuf(), other.dmabuf()) {
736                return a.as_raw_fd() == b.as_raw_fd();
737            }
738        }
739        false
740    }
741
742    // --- Downcasting ---
743
744    downcast_methods!(U8, u8, as_u8, as_u8_mut, into_u8);
745    downcast_methods!(I8, i8, as_i8, as_i8_mut, into_i8);
746    downcast_methods!(U16, u16, as_u16, as_u16_mut, into_u16);
747    downcast_methods!(I16, i16, as_i16, as_i16_mut, into_i16);
748    downcast_methods!(U32, u32, as_u32, as_u32_mut, into_u32);
749    downcast_methods!(I32, i32, as_i32, as_i32_mut, into_i32);
750    downcast_methods!(U64, u64, as_u64, as_u64_mut, into_u64);
751    downcast_methods!(I64, i64, as_i64, as_i64_mut, into_i64);
752    downcast_methods!(F16, f16, as_f16, as_f16_mut, into_f16);
753    downcast_methods!(F32, f32, as_f32, as_f32_mut, into_f32);
754    downcast_methods!(F64, f64, as_f64, as_f64_mut, into_f64);
755
756    /// Create a type-erased tensor with the given shape and element type.
757    pub fn new(
758        shape: &[usize],
759        dtype: DType,
760        memory: Option<TensorMemory>,
761        name: Option<&str>,
762    ) -> crate::Result<Self> {
763        match dtype {
764            DType::U8 => Tensor::<u8>::new(shape, memory, name).map(Self::U8),
765            DType::I8 => Tensor::<i8>::new(shape, memory, name).map(Self::I8),
766            DType::U16 => Tensor::<u16>::new(shape, memory, name).map(Self::U16),
767            DType::I16 => Tensor::<i16>::new(shape, memory, name).map(Self::I16),
768            DType::U32 => Tensor::<u32>::new(shape, memory, name).map(Self::U32),
769            DType::I32 => Tensor::<i32>::new(shape, memory, name).map(Self::I32),
770            DType::U64 => Tensor::<u64>::new(shape, memory, name).map(Self::U64),
771            DType::I64 => Tensor::<i64>::new(shape, memory, name).map(Self::I64),
772            DType::F16 => Tensor::<f16>::new(shape, memory, name).map(Self::F16),
773            DType::F32 => Tensor::<f32>::new(shape, memory, name).map(Self::F32),
774            DType::F64 => Tensor::<f64>::new(shape, memory, name).map(Self::F64),
775        }
776    }
777
778    /// Import an existing buffer as a type-erased tensor, taking ownership
779    /// of its file descriptor. No bytes are copied.
780    ///
781    /// Dispatches to [`Tensor::from_fd`](crate::TensorTrait::from_fd) for
782    /// `dtype` and inherits its contract in full: on Linux the backend is
783    /// detected from the fd's filesystem magic — `DMA_BUF_MAGIC` imports as
784    /// [`TensorMemory::DmaBuf`](crate::TensorMemory::DmaBuf), `TMPFS_MAGIC` (both
785    /// `/dev/shm` and `memfd`) as [`TensorMemory::Shm`](crate::TensorMemory::Shm)
786    /// — and any other filesystem is rejected rather than assumed to be
787    /// shared memory. On non-Linux Unix the fd is always adopted as SHM.
788    ///
789    /// # Errors
790    ///
791    /// * [`Error::UnknownBufferType`](crate::Error::UnknownBufferType) - the
792    ///   fd is neither a DMA-BUF nor tmpfs-backed; carries the observed
793    ///   `fstatfs` magic. Linux only.
794    /// * [`Error::UnknownDeviceType`](crate::Error::UnknownDeviceType) - the
795    ///   fd lives on a real block device. Linux only.
796    /// * [`Error::InvalidSize`](crate::Error::InvalidSize) - `shape` is empty
797    ///   or describes zero elements.
798    /// * [`Error::NixError`](crate::Error::NixError) - a syscall on the
799    ///   descriptor failed.
800    ///
801    /// Callers that require zero-copy must check
802    /// [`memory()`](crate::TensorDyn::memory) on the result rather than
803    /// treating a successful import as proof of DMA backing.
804    #[cfg(unix)]
805    pub fn from_fd(
806        fd: std::os::fd::OwnedFd,
807        shape: &[usize],
808        dtype: DType,
809        name: Option<&str>,
810    ) -> crate::Result<Self> {
811        match dtype {
812            DType::U8 => Tensor::<u8>::from_fd(fd, shape, name).map(Self::U8),
813            DType::I8 => Tensor::<i8>::from_fd(fd, shape, name).map(Self::I8),
814            DType::U16 => Tensor::<u16>::from_fd(fd, shape, name).map(Self::U16),
815            DType::I16 => Tensor::<i16>::from_fd(fd, shape, name).map(Self::I16),
816            DType::U32 => Tensor::<u32>::from_fd(fd, shape, name).map(Self::U32),
817            DType::I32 => Tensor::<i32>::from_fd(fd, shape, name).map(Self::I32),
818            DType::U64 => Tensor::<u64>::from_fd(fd, shape, name).map(Self::U64),
819            DType::I64 => Tensor::<i64>::from_fd(fd, shape, name).map(Self::I64),
820            DType::F16 => Tensor::<f16>::from_fd(fd, shape, name).map(Self::F16),
821            DType::F32 => Tensor::<f32>::from_fd(fd, shape, name).map(Self::F32),
822            DType::F64 => Tensor::<f64>::from_fd(fd, shape, name).map(Self::F64),
823        }
824    }
825
826    /// Rebuild a type-erased PBO tensor from a cross-cdylib `ops` (see
827    /// [`crate::pbo::import_pbo_ops`]) plus the geometry a
828    /// [`crate::TensorDesc`] under [`crate::protocol::kind::PBO`] carries.
829    /// Mirrors [`Self::from_fd`]'s per-dtype dispatch shape exactly; the
830    /// only caller is [`Self::import_descriptor`].
831    pub(crate) fn from_pbo_import(
832        buffer_id: u32,
833        size: usize,
834        shape: &[usize],
835        dtype: DType,
836        ops: std::sync::Arc<dyn crate::PboOps>,
837    ) -> crate::Result<Self> {
838        match dtype {
839            DType::U8 => crate::PboTensor::<u8>::from_pbo(buffer_id, size, shape, None, ops)
840                .and_then(|p| Tensor::from_pbo(p).map(Self::U8)),
841            DType::I8 => crate::PboTensor::<i8>::from_pbo(buffer_id, size, shape, None, ops)
842                .and_then(|p| Tensor::from_pbo(p).map(Self::I8)),
843            DType::U16 => crate::PboTensor::<u16>::from_pbo(buffer_id, size, shape, None, ops)
844                .and_then(|p| Tensor::from_pbo(p).map(Self::U16)),
845            DType::I16 => crate::PboTensor::<i16>::from_pbo(buffer_id, size, shape, None, ops)
846                .and_then(|p| Tensor::from_pbo(p).map(Self::I16)),
847            DType::U32 => crate::PboTensor::<u32>::from_pbo(buffer_id, size, shape, None, ops)
848                .and_then(|p| Tensor::from_pbo(p).map(Self::U32)),
849            DType::I32 => crate::PboTensor::<i32>::from_pbo(buffer_id, size, shape, None, ops)
850                .and_then(|p| Tensor::from_pbo(p).map(Self::I32)),
851            DType::U64 => crate::PboTensor::<u64>::from_pbo(buffer_id, size, shape, None, ops)
852                .and_then(|p| Tensor::from_pbo(p).map(Self::U64)),
853            DType::I64 => crate::PboTensor::<i64>::from_pbo(buffer_id, size, shape, None, ops)
854                .and_then(|p| Tensor::from_pbo(p).map(Self::I64)),
855            DType::F16 => crate::PboTensor::<f16>::from_pbo(buffer_id, size, shape, None, ops)
856                .and_then(|p| Tensor::from_pbo(p).map(Self::F16)),
857            DType::F32 => crate::PboTensor::<f32>::from_pbo(buffer_id, size, shape, None, ops)
858                .and_then(|p| Tensor::from_pbo(p).map(Self::F32)),
859            DType::F64 => crate::PboTensor::<f64>::from_pbo(buffer_id, size, shape, None, ops)
860                .and_then(|p| Tensor::from_pbo(p).map(Self::F64)),
861        }
862    }
863
864    /// Wrap a producer's host pointer as a type-erased tensor without
865    /// copying, aliasing rather than owning it.
866    ///
867    /// This is [`Self::from_foreign_ptr`] with `owner: None` — the consumer
868    /// half of the capsule protocol's `HOST` kind
869    /// ([`crate::protocol::kind::HOST`]). The descriptor's `ptr` is only
870    /// meaningful while the producer's capsule keepalive is alive; nothing
871    /// here takes a reference to extend that lifetime, so the caller (the
872    /// capsule machinery) is responsible for it. See
873    /// [`TensorDyn::import_descriptor`] for the full contract.
874    ///
875    /// # Safety
876    ///
877    /// Same as [`Self::from_foreign_ptr`]: `ptr` must be non-null, aligned to
878    /// `dtype`, and valid for `shape.product()` elements of `dtype` for as
879    /// long as the returned tensor — and every view/map sharing its backing
880    /// — is used.
881    pub unsafe fn from_raw_host(
882        ptr: *mut u8,
883        shape: &[usize],
884        dtype: DType,
885        name: Option<&str>,
886    ) -> crate::Result<Self> {
887        unsafe { Self::from_foreign_ptr(ptr, shape, dtype, None, name) }
888    }
889
890    /// [`Self::from_raw_host`], additionally recording that the producer's
891    /// allocation holds `capacity_bytes` bytes rather than exactly
892    /// `shape.product() * dtype.size()` -- the consumer half of
893    /// [`crate::TensorDesc::capacity`]. See
894    /// [`Tensor::from_foreign_with_capacity`](crate::Tensor::from_foreign_with_capacity)
895    /// for why: a producer's pool tensor (or one padded to a decoder's
896    /// MCU/pitch alignment) is larger than the shape it currently reports,
897    /// and without this the imported alias would be clamped to today's
898    /// shape and unable to grow back into memory the producer actually has.
899    ///
900    /// # Safety
901    ///
902    /// Same as [`Self::from_raw_host`], except `ptr` must be valid for
903    /// `capacity_bytes` bytes (or the tight shape footprint, whichever is
904    /// larger).
905    pub unsafe fn from_raw_host_with_capacity(
906        ptr: *mut u8,
907        shape: &[usize],
908        capacity_bytes: usize,
909        dtype: DType,
910        name: Option<&str>,
911    ) -> crate::Result<Self> {
912        unsafe {
913            Self::from_foreign_ptr_with_capacity(ptr, shape, dtype, capacity_bytes, None, name)
914        }
915    }
916
917    /// The per-kind construction switch [`TensorDyn::import_descriptor`]
918    /// (`derived.rs`) drives, once the descriptor has been validated and
919    /// its `dtype`/`shape` decoded.
920    ///
921    /// Backend-specific because it is the one part of the import that names
922    /// *constructors*; everything around it (validation, and the
923    /// format/stride/colorimetry restore afterward) is shared. See
924    /// `derived.rs` for the whole function's contract -- including the
925    /// borrow semantics every arm here relies on: the result *aliases* the
926    /// producer's memory and nothing here takes a reference to keep it
927    /// alive, except `DMABUF`, which `dup`s.
928    pub(crate) fn import_storage(
929        desc: &crate::TensorDesc,
930        shape: &[usize],
931        dtype: DType,
932    ) -> crate::Result<Self> {
933        match desc.kind {
934            #[cfg(target_os = "linux")]
935            crate::protocol::kind::DMABUF => {
936                // `from_fd` takes ownership of the fd it is given, but the
937                // producer still owns the original and will close it when
938                // its own tensor drops -- `dup_descriptor_fd` gives this
939                // import its own.
940                let owned = crate::protocol::dup_descriptor_fd(desc)?;
941                Self::from_fd(owned, shape, dtype, None)
942            }
943            #[cfg(not(target_os = "linux"))]
944            crate::protocol::kind::DMABUF => Err(crate::Error::NotImplemented(
945                "dma-buf import off Linux".into(),
946            )),
947            #[cfg(any(target_os = "macos", target_os = "ios"))]
948            crate::protocol::kind::IOSURFACE => {
949                let id = crate::protocol::descriptor_surface_id(desc)?;
950                let surface_ref = crate::iosurface::lookup_by_id(id).ok_or_else(|| {
951                    crate::Error::InvalidArgument(format!(
952                        "IOSurface id {id} is not live (the producer's capsule may \
953                         already have been released)"
954                    ))
955                })?;
956                // SAFETY: `lookup_by_id` just returned a live IOSurfaceRef for
957                // this id. `from_iosurface` (`OwnedIoSurface::from_external`)
958                // takes its own independent CFRetain, so the lookup's +1
959                // reference is released right after regardless of outcome.
960                let result = unsafe { Self::from_iosurface(surface_ref, shape, dtype, None) };
961                crate::iosurface::release(surface_ref);
962                result
963            }
964            #[cfg(not(any(target_os = "macos", target_os = "ios")))]
965            crate::protocol::kind::IOSURFACE => Err(crate::Error::NotImplemented(
966                "IOSurface import off Apple platforms".into(),
967            )),
968            crate::protocol::kind::HOST => {
969                crate::protocol::check_descriptor_host_ptr(desc)?;
970                // SAFETY: the caller guarantees the producer's keepalive
971                // outlives the returned tensor -- that is the capsule
972                // contract `import_descriptor` documents. `desc.capacity`
973                // comes from the same trusted producer as `ptr`/`shape`, so
974                // extending trust to it is not a new hazard; see
975                // `from_raw_host_with_capacity`'s docs.
976                unsafe {
977                    Self::from_raw_host_with_capacity(
978                        desc.ptr.0,
979                        shape,
980                        desc.capacity as usize,
981                        dtype,
982                        None,
983                    )
984                }
985            }
986            crate::protocol::kind::PBO => {
987                let buffer_id = crate::protocol::descriptor_pbo_buffer_id(desc)?;
988                // SAFETY: the caller guarantees the producer's keepalive
989                // outlives the returned tensor -- the same capsule contract
990                // `import_descriptor`'s own doc comment already establishes
991                // for `ptr`, extended here to what `ptr` means under
992                // `kind::PBO` (see `TensorDesc::ptr`'s own doc comment).
993                // `desc.ptr.0` came from the same trusted producer as
994                // `desc.handle`, so interpreting it as a `PboOpsVtable*` is
995                // not a new hazard relative to the `HOST` arm's own use of
996                // `desc.ptr`.
997                let ops = unsafe { crate::pbo::import_pbo_ops(desc.ptr.0 as *const _)? };
998                Self::from_pbo_import(buffer_id, desc.capacity as usize, shape, dtype, ops)
999            }
1000            k => Err(crate::Error::NotImplemented(format!(
1001                "tensor interop kind {k} cannot be imported by this build"
1002            ))),
1003        }
1004    }
1005
1006    /// Wrap externally-owned memory as a type-erased tensor without copying.
1007    /// The tensor borrows `[ptr, ptr + shape.product() * dtype.size())` as
1008    /// [`TensorMemory::Mem`]; `owner`, when `Some`, co-owns the source so it
1009    /// outlives the tensor (and all derived views/maps). See
1010    /// [`crate::ForeignOwner`] and [`Tensor::from_foreign`].
1011    ///
1012    /// # Safety
1013    ///
1014    /// `ptr` must be non-null, aligned to the element type, and valid for
1015    /// `shape.product()` elements of `dtype` for as long as the returned
1016    /// tensor — and every view/map sharing its backing — is alive. Pass an
1017    /// `owner` that co-owns the source to uphold that contract.
1018    pub unsafe fn from_foreign_ptr(
1019        ptr: *mut u8,
1020        shape: &[usize],
1021        dtype: DType,
1022        owner: Option<crate::ForeignOwner>,
1023        name: Option<&str>,
1024    ) -> crate::Result<Self> {
1025        // SAFETY: this function's own contract (see its doc comment) is
1026        // exactly `Tensor::from_foreign`'s contract, uniformly across every
1027        // dtype arm below -- `ptr`/`shape`/`owner`/`name` are unchanged, only
1028        // the element type dispatched on `dtype` differs.
1029        unsafe {
1030            match dtype {
1031                DType::U8 => {
1032                    Tensor::<u8>::from_foreign(ptr.cast(), shape, owner, name).map(Self::U8)
1033                }
1034                DType::I8 => {
1035                    Tensor::<i8>::from_foreign(ptr.cast(), shape, owner, name).map(Self::I8)
1036                }
1037                DType::U16 => {
1038                    Tensor::<u16>::from_foreign(ptr.cast(), shape, owner, name).map(Self::U16)
1039                }
1040                DType::I16 => {
1041                    Tensor::<i16>::from_foreign(ptr.cast(), shape, owner, name).map(Self::I16)
1042                }
1043                DType::U32 => {
1044                    Tensor::<u32>::from_foreign(ptr.cast(), shape, owner, name).map(Self::U32)
1045                }
1046                DType::I32 => {
1047                    Tensor::<i32>::from_foreign(ptr.cast(), shape, owner, name).map(Self::I32)
1048                }
1049                DType::U64 => {
1050                    Tensor::<u64>::from_foreign(ptr.cast(), shape, owner, name).map(Self::U64)
1051                }
1052                DType::I64 => {
1053                    Tensor::<i64>::from_foreign(ptr.cast(), shape, owner, name).map(Self::I64)
1054                }
1055                DType::F16 => {
1056                    Tensor::<f16>::from_foreign(ptr.cast(), shape, owner, name).map(Self::F16)
1057                }
1058                DType::F32 => {
1059                    Tensor::<f32>::from_foreign(ptr.cast(), shape, owner, name).map(Self::F32)
1060                }
1061                DType::F64 => {
1062                    Tensor::<f64>::from_foreign(ptr.cast(), shape, owner, name).map(Self::F64)
1063                }
1064            }
1065        }
1066    }
1067
1068    /// [`Self::from_foreign_ptr`], additionally recording that the
1069    /// allocation behind `ptr` holds `capacity_bytes` bytes rather than
1070    /// exactly `shape.product() * dtype.size()`. See
1071    /// [`Tensor::from_foreign_with_capacity`](crate::Tensor::from_foreign_with_capacity).
1072    ///
1073    /// # Safety
1074    ///
1075    /// Same as [`Self::from_foreign_ptr`], except `ptr` must be valid for
1076    /// `capacity_bytes` bytes (or the tight shape footprint, whichever is
1077    /// larger).
1078    pub unsafe fn from_foreign_ptr_with_capacity(
1079        ptr: *mut u8,
1080        shape: &[usize],
1081        dtype: DType,
1082        capacity_bytes: usize,
1083        owner: Option<crate::ForeignOwner>,
1084        name: Option<&str>,
1085    ) -> crate::Result<Self> {
1086        macro_rules! import {
1087            ($t:ty, $variant:ident) => {
1088                // SAFETY: this function's own contract (see its doc comment)
1089                // is exactly `Tensor::from_foreign_with_capacity`'s
1090                // contract, uniformly across every dtype this macro is
1091                // invoked for below.
1092                unsafe {
1093                    Tensor::<$t>::from_foreign_with_capacity(
1094                        ptr.cast(),
1095                        shape,
1096                        capacity_bytes,
1097                        owner,
1098                        name,
1099                    )
1100                }
1101                .map(Self::$variant)
1102            };
1103        }
1104        match dtype {
1105            DType::U8 => import!(u8, U8),
1106            DType::I8 => import!(i8, I8),
1107            DType::U16 => import!(u16, U16),
1108            DType::I16 => import!(i16, I16),
1109            DType::U32 => import!(u32, U32),
1110            DType::I32 => import!(i32, I32),
1111            DType::U64 => import!(u64, U64),
1112            DType::I64 => import!(i64, I64),
1113            DType::F16 => import!(f16, F16),
1114            DType::F32 => import!(f32, F32),
1115            DType::F64 => import!(f64, F64),
1116        }
1117    }
1118
1119    /// Wrap an IOSurface named by its global ID (macOS/iOS only).
1120    ///
1121    /// The IOSurface counterpart to [`from_fd`](Self::from_fd): an ID is the
1122    /// shareable handle on this platform, the way a dma-buf fd is on Linux, so
1123    /// this is what a consumer needs when a tensor arrives from another library
1124    /// or process carrying only the ID.
1125    ///
1126    /// Returns an error when no live surface has that ID — IDs are reused after
1127    /// the surface is freed, so a stale one must fail rather than resolve to an
1128    /// unrelated buffer.
1129    #[cfg(any(target_os = "macos", target_os = "ios"))]
1130    pub fn from_iosurface_id(
1131        id: u32,
1132        shape: &[usize],
1133        dtype: DType,
1134        name: Option<&str>,
1135    ) -> crate::Result<Self> {
1136        let surface = crate::iosurface::lookup_by_id(id).ok_or_else(|| {
1137            crate::Error::InvalidArgument(format!("no live IOSurface with id {id}"))
1138        })?;
1139        // SAFETY: `lookup_by_id` returned a live (+1) reference;
1140        // `from_iosurface` (`OwnedIoSurface::from_external`) takes its own
1141        // independent `CFRetain`, so the lookup's reference is ours to give
1142        // back and must be given back on BOTH paths -- `crate::iosurface`'s
1143        // module docs state the rule without hedging ("every call site pairs
1144        // a successful lookup with `release` once it has taken, or failed to
1145        // take, its own independent retain"), and this function was the one
1146        // call site that did not. The leak was one full surface -- and its
1147        // whole pixel allocation -- per import that never returns to the
1148        // producer's baseline, so a per-frame capsule hand-off leaked a
1149        // frame buffer per frame. Unreached until task P2a routed the
1150        // `dynamic` backend's entire `kind::IOSURFACE` capsule import
1151        // through here (`ef_tensor_from_iosurface_id`); `blob.rs`'s
1152        // deserializer was the only prior caller and leaked the same way.
1153        let result = unsafe { Self::from_iosurface(surface, shape, dtype, name) };
1154        crate::iosurface::release(surface);
1155        result
1156    }
1157
1158    /// Wrap an externally-allocated IOSurface as a type-erased tensor
1159    /// (macOS/iOS only).
1160    ///
1161    /// # Safety
1162    ///
1163    /// `surface_ref` must be a valid live `IOSurfaceRef`. `shape` must
1164    /// match the IOSurface's pixel dimensions and chosen element type.
1165    #[cfg(any(target_os = "macos", target_os = "ios"))]
1166    pub unsafe fn from_iosurface(
1167        surface_ref: *mut std::ffi::c_void,
1168        shape: &[usize],
1169        dtype: DType,
1170        name: Option<&str>,
1171    ) -> crate::Result<Self> {
1172        unsafe {
1173            match dtype {
1174                DType::U8 => Tensor::<u8>::from_iosurface(surface_ref, shape, name).map(Self::U8),
1175                DType::I8 => Tensor::<i8>::from_iosurface(surface_ref, shape, name).map(Self::I8),
1176                DType::U16 => {
1177                    Tensor::<u16>::from_iosurface(surface_ref, shape, name).map(Self::U16)
1178                }
1179                DType::I16 => {
1180                    Tensor::<i16>::from_iosurface(surface_ref, shape, name).map(Self::I16)
1181                }
1182                DType::U32 => {
1183                    Tensor::<u32>::from_iosurface(surface_ref, shape, name).map(Self::U32)
1184                }
1185                DType::I32 => {
1186                    Tensor::<i32>::from_iosurface(surface_ref, shape, name).map(Self::I32)
1187                }
1188                DType::U64 => {
1189                    Tensor::<u64>::from_iosurface(surface_ref, shape, name).map(Self::U64)
1190                }
1191                DType::I64 => {
1192                    Tensor::<i64>::from_iosurface(surface_ref, shape, name).map(Self::I64)
1193                }
1194                DType::F16 => {
1195                    Tensor::<f16>::from_iosurface(surface_ref, shape, name).map(Self::F16)
1196                }
1197                DType::F32 => {
1198                    Tensor::<f32>::from_iosurface(surface_ref, shape, name).map(Self::F32)
1199                }
1200                DType::F64 => {
1201                    Tensor::<f64>::from_iosurface(surface_ref, shape, name).map(Self::F64)
1202                }
1203            }
1204        }
1205    }
1206
1207    /// IOSurfaceID for cross-process surface sharing (macOS/iOS only).
1208    /// Returns `None` when the tensor is not IOSurface-backed.
1209    #[cfg(any(target_os = "macos", target_os = "ios"))]
1210    pub fn iosurface_id(&self) -> Option<u32> {
1211        dispatch!(self, iosurface_id)
1212    }
1213
1214    /// GL buffer ID for this PBO. Returns `None` when the tensor is not
1215    /// PBO-backed.
1216    pub fn pbo_id(&self) -> Option<u32> {
1217        dispatch!(self, pbo_id)
1218    }
1219
1220    /// The C-ABI `PboOpsVtable` address for this PBO, for cross-cdylib
1221    /// export via [`crate::TensorDesc::ptr`] (see that field's doc
1222    /// comment). Returns `None` when the tensor is not PBO-backed.
1223    pub fn pbo_vtable_ptr(&self) -> Option<*const std::ffi::c_void> {
1224        dispatch!(self, pbo_vtable_ptr)
1225    }
1226
1227    /// A type-erased keepalive that must stay alive for at least as long as
1228    /// [`Self::pbo_vtable_ptr`]'s address is used. `None` when the tensor is
1229    /// not PBO-backed. See [`crate::pbo::PboTensor::pbo_keepalive`]'s own
1230    /// doc comment.
1231    pub fn pbo_keepalive(&self) -> Option<std::sync::Arc<dyn Send + Sync>> {
1232        dispatch!(self, pbo_keepalive)
1233    }
1234
1235    /// Borrow the raw `IOSurfaceRef` backing this tensor (macOS/iOS
1236    /// only). Returns `None` when the tensor is not IOSurface-backed.
1237    /// The pointer's lifetime is tied to `self`.
1238    #[cfg(any(target_os = "macos", target_os = "ios"))]
1239    pub fn iosurface_ref(&self) -> Option<*mut std::ffi::c_void> {
1240        dispatch!(self, iosurface_ref)
1241    }
1242
1243    /// Physical IOSurface dimensions in texels, independent of the logical
1244    /// shape (macOS/iOS only). `None` when not IOSurface-backed. The GL
1245    /// backend binds the EGL pbuffer at these dims so one cached pbuffer
1246    /// serves every frame size a reused pool surface holds.
1247    #[cfg(any(target_os = "macos", target_os = "ios"))]
1248    pub fn iosurface_physical_dims(&self) -> Option<(usize, usize)> {
1249        dispatch!(self, iosurface_physical_dims)
1250    }
1251
1252    /// Wrap an externally-allocated AHardwareBuffer as a type-erased
1253    /// tensor (Android only). Used to import buffers from
1254    /// CameraX/ImageReader (via JNI), NNAPI, or cross-process binder
1255    /// transfers.
1256    ///
1257    /// # Safety
1258    ///
1259    /// `buffer_ptr` must be a valid live AHardwareBuffer pointer. `shape`
1260    /// must match the buffer's dimensions and chosen element type.
1261    #[cfg(target_os = "android")]
1262    pub unsafe fn from_hardware_buffer(
1263        buffer_ptr: *mut std::ffi::c_void,
1264        shape: &[usize],
1265        dtype: DType,
1266        name: Option<&str>,
1267    ) -> crate::Result<Self> {
1268        unsafe {
1269            match dtype {
1270                DType::U8 => {
1271                    Tensor::<u8>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::U8)
1272                }
1273                DType::I8 => {
1274                    Tensor::<i8>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::I8)
1275                }
1276                DType::U16 => {
1277                    Tensor::<u16>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::U16)
1278                }
1279                DType::I16 => {
1280                    Tensor::<i16>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::I16)
1281                }
1282                DType::U32 => {
1283                    Tensor::<u32>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::U32)
1284                }
1285                DType::I32 => {
1286                    Tensor::<i32>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::I32)
1287                }
1288                DType::U64 => {
1289                    Tensor::<u64>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::U64)
1290                }
1291                DType::I64 => {
1292                    Tensor::<i64>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::I64)
1293                }
1294                DType::F16 => {
1295                    Tensor::<f16>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::F16)
1296                }
1297                DType::F32 => {
1298                    Tensor::<f32>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::F32)
1299                }
1300                DType::F64 => {
1301                    Tensor::<f64>::from_hardware_buffer(buffer_ptr, shape, name).map(Self::F64)
1302                }
1303            }
1304        }
1305    }
1306
1307    /// Borrow the raw AHardwareBuffer pointer backing this tensor
1308    /// (Android only). Returns `None` when the tensor is not
1309    /// AHardwareBuffer-backed. The pointer's lifetime is tied to `self`.
1310    #[cfg(target_os = "android")]
1311    pub fn hardware_buffer_ptr(&self) -> Option<*mut std::ffi::c_void> {
1312        dispatch!(self, hardware_buffer_ptr)
1313    }
1314
1315    /// Physical AHardwareBuffer dimensions in texels, independent of the
1316    /// logical shape (Android only). `None` when not
1317    /// AHardwareBuffer-backed.
1318    #[cfg(target_os = "android")]
1319    pub fn hardware_buffer_physical_dims(&self) -> Option<(usize, usize)> {
1320        dispatch!(self, hardware_buffer_physical_dims)
1321    }
1322
1323    /// Copy the tensor's logical bytes into `dst`, compacting away any
1324    /// recorded row-stride padding — see [`Tensor::copy_to_flat`] for the
1325    /// full contract. `dst.len()` must equal the tight byte footprint
1326    /// (`shape` product × element size).
1327    pub fn copy_to_flat(&self, dst: &mut [u8]) -> crate::Result<()> {
1328        dispatch!(self, copy_to_flat, dst)
1329    }
1330
1331    /// Create a type-erased image tensor.
1332    ///
1333    /// # Arguments
1334    ///
1335    /// * `width` - Image width in pixels
1336    /// * `height` - Image height in pixels
1337    /// * `format` - Pixel format
1338    /// * `dtype` - Element type discriminant
1339    /// * `memory` - Optional memory backend (None selects the best available)
1340    ///
1341    /// # Returns
1342    ///
1343    /// A new `TensorDyn` wrapping an image tensor of the requested element type.
1344    ///
1345    /// # Errors
1346    ///
1347    /// Returns an error if the underlying `Tensor::image` call fails.
1348    pub fn image(
1349        width: usize,
1350        height: usize,
1351        format: PixelFormat,
1352        dtype: DType,
1353        memory: Option<TensorMemory>,
1354        access: crate::CpuAccess,
1355    ) -> crate::Result<Self> {
1356        match dtype {
1357            DType::U8 => Tensor::<u8>::image(width, height, format, memory, access).map(Self::U8),
1358            DType::I8 => Tensor::<i8>::image(width, height, format, memory, access).map(Self::I8),
1359            DType::U16 => {
1360                Tensor::<u16>::image(width, height, format, memory, access).map(Self::U16)
1361            }
1362            DType::I16 => {
1363                Tensor::<i16>::image(width, height, format, memory, access).map(Self::I16)
1364            }
1365            DType::U32 => {
1366                Tensor::<u32>::image(width, height, format, memory, access).map(Self::U32)
1367            }
1368            DType::I32 => {
1369                Tensor::<i32>::image(width, height, format, memory, access).map(Self::I32)
1370            }
1371            DType::U64 => {
1372                Tensor::<u64>::image(width, height, format, memory, access).map(Self::U64)
1373            }
1374            DType::I64 => {
1375                Tensor::<i64>::image(width, height, format, memory, access).map(Self::I64)
1376            }
1377            DType::F16 => {
1378                Tensor::<f16>::image(width, height, format, memory, access).map(Self::F16)
1379            }
1380            DType::F32 => {
1381                Tensor::<f32>::image(width, height, format, memory, access).map(Self::F32)
1382            }
1383            DType::F64 => {
1384                Tensor::<f64>::image(width, height, format, memory, access).map(Self::F64)
1385            }
1386        }
1387    }
1388
1389    /// Allocate an image tensor from a declarative [`crate::ImageDesc`]
1390    /// request — dispatching on `desc.dtype()`. See
1391    /// [`Tensor::image_desc`] for the compression-request semantics.
1392    pub fn image_desc(desc: &crate::ImageDesc) -> crate::Result<Self> {
1393        match desc.dtype() {
1394            DType::U8 => Tensor::<u8>::image_desc(desc).map(Self::U8),
1395            DType::I8 => Tensor::<i8>::image_desc(desc).map(Self::I8),
1396            DType::U16 => Tensor::<u16>::image_desc(desc).map(Self::U16),
1397            DType::I16 => Tensor::<i16>::image_desc(desc).map(Self::I16),
1398            DType::U32 => Tensor::<u32>::image_desc(desc).map(Self::U32),
1399            DType::I32 => Tensor::<i32>::image_desc(desc).map(Self::I32),
1400            DType::U64 => Tensor::<u64>::image_desc(desc).map(Self::U64),
1401            DType::I64 => Tensor::<i64>::image_desc(desc).map(Self::I64),
1402            DType::F16 => Tensor::<f16>::image_desc(desc).map(Self::F16),
1403            DType::F32 => Tensor::<f32>::image_desc(desc).map(Self::F32),
1404            DType::F64 => Tensor::<f64>::image_desc(desc).map(Self::F64),
1405        }
1406    }
1407
1408    /// The recorded vendor tile-compression scheme (see
1409    /// [`Tensor::compression`]).
1410    pub fn compression(&self) -> Option<crate::CompressionScheme> {
1411        dispatch!(self, compression)
1412    }
1413
1414    /// Create a DMA-backed image tensor with an explicit row stride that
1415    /// may exceed the natural `width * channels * sizeof(T)` pitch.
1416    ///
1417    /// See [`Tensor::image_with_stride`] for the detailed contract and
1418    /// constraints. The TensorDyn wrapper dispatches to the appropriate
1419    /// monomorphised `Tensor<T>` based on `dtype`.
1420    ///
1421    /// # Example
1422    ///
1423    /// ```no_run
1424    /// use edgefirst_tensor::{CpuAccess, TensorDyn, PixelFormat, DType, TensorMemory};
1425    /// # fn main() -> edgefirst_tensor::Result<()> {
1426    /// // Allocate a 3004×1688 RGBA8 canvas with 64-byte pitch alignment
1427    /// // (12032 bytes per row instead of the natural 12016).
1428    /// let img = TensorDyn::image_with_stride(
1429    ///     3004, 1688,
1430    ///     PixelFormat::Rgba, DType::U8,
1431    ///     12032,
1432    ///     Some(TensorMemory::DmaBuf),
1433    ///     CpuAccess::ReadWrite,
1434    /// )?;
1435    /// assert_eq!(img.width(), Some(3004));       // logical, unchanged
1436    /// assert_eq!(img.effective_row_stride(), Some(12032)); // padded
1437    /// # Ok(())
1438    /// # }
1439    /// ```
1440    pub fn image_with_stride(
1441        width: usize,
1442        height: usize,
1443        format: PixelFormat,
1444        dtype: DType,
1445        row_stride_bytes: usize,
1446        memory: Option<TensorMemory>,
1447        access: crate::CpuAccess,
1448    ) -> crate::Result<Self> {
1449        match dtype {
1450            DType::U8 => Tensor::<u8>::image_with_stride(
1451                width,
1452                height,
1453                format,
1454                row_stride_bytes,
1455                memory,
1456                access,
1457            )
1458            .map(Self::U8),
1459            DType::I8 => Tensor::<i8>::image_with_stride(
1460                width,
1461                height,
1462                format,
1463                row_stride_bytes,
1464                memory,
1465                access,
1466            )
1467            .map(Self::I8),
1468            DType::U16 => Tensor::<u16>::image_with_stride(
1469                width,
1470                height,
1471                format,
1472                row_stride_bytes,
1473                memory,
1474                access,
1475            )
1476            .map(Self::U16),
1477            DType::I16 => Tensor::<i16>::image_with_stride(
1478                width,
1479                height,
1480                format,
1481                row_stride_bytes,
1482                memory,
1483                access,
1484            )
1485            .map(Self::I16),
1486            DType::U32 => Tensor::<u32>::image_with_stride(
1487                width,
1488                height,
1489                format,
1490                row_stride_bytes,
1491                memory,
1492                access,
1493            )
1494            .map(Self::U32),
1495            DType::I32 => Tensor::<i32>::image_with_stride(
1496                width,
1497                height,
1498                format,
1499                row_stride_bytes,
1500                memory,
1501                access,
1502            )
1503            .map(Self::I32),
1504            DType::U64 => Tensor::<u64>::image_with_stride(
1505                width,
1506                height,
1507                format,
1508                row_stride_bytes,
1509                memory,
1510                access,
1511            )
1512            .map(Self::U64),
1513            DType::I64 => Tensor::<i64>::image_with_stride(
1514                width,
1515                height,
1516                format,
1517                row_stride_bytes,
1518                memory,
1519                access,
1520            )
1521            .map(Self::I64),
1522            DType::F16 => Tensor::<f16>::image_with_stride(
1523                width,
1524                height,
1525                format,
1526                row_stride_bytes,
1527                memory,
1528                access,
1529            )
1530            .map(Self::F16),
1531            DType::F32 => Tensor::<f32>::image_with_stride(
1532                width,
1533                height,
1534                format,
1535                row_stride_bytes,
1536                memory,
1537                access,
1538            )
1539            .map(Self::F32),
1540            DType::F64 => Tensor::<f64>::image_with_stride(
1541                width,
1542                height,
1543                format,
1544                row_stride_bytes,
1545                memory,
1546                access,
1547            )
1548            .map(Self::F64),
1549        }
1550    }
1551}
1552
1553// --- From impls ---
1554
1555impl From<Tensor<u8>> for TensorDyn {
1556    fn from(t: Tensor<u8>) -> Self {
1557        Self::U8(t)
1558    }
1559}
1560
1561impl From<Tensor<i8>> for TensorDyn {
1562    fn from(t: Tensor<i8>) -> Self {
1563        Self::I8(t)
1564    }
1565}
1566
1567impl From<Tensor<u16>> for TensorDyn {
1568    fn from(t: Tensor<u16>) -> Self {
1569        Self::U16(t)
1570    }
1571}
1572
1573impl From<Tensor<i16>> for TensorDyn {
1574    fn from(t: Tensor<i16>) -> Self {
1575        Self::I16(t)
1576    }
1577}
1578
1579impl From<Tensor<u32>> for TensorDyn {
1580    fn from(t: Tensor<u32>) -> Self {
1581        Self::U32(t)
1582    }
1583}
1584
1585impl From<Tensor<i32>> for TensorDyn {
1586    fn from(t: Tensor<i32>) -> Self {
1587        Self::I32(t)
1588    }
1589}
1590
1591impl From<Tensor<u64>> for TensorDyn {
1592    fn from(t: Tensor<u64>) -> Self {
1593        Self::U64(t)
1594    }
1595}
1596
1597impl From<Tensor<i64>> for TensorDyn {
1598    fn from(t: Tensor<i64>) -> Self {
1599        Self::I64(t)
1600    }
1601}
1602
1603impl From<Tensor<f16>> for TensorDyn {
1604    fn from(t: Tensor<f16>) -> Self {
1605        Self::F16(t)
1606    }
1607}
1608
1609impl From<Tensor<f32>> for TensorDyn {
1610    fn from(t: Tensor<f32>) -> Self {
1611        Self::F32(t)
1612    }
1613}
1614
1615impl From<Tensor<f64>> for TensorDyn {
1616    fn from(t: Tensor<f64>) -> Self {
1617        Self::F64(t)
1618    }
1619}
1620
1621impl fmt::Debug for TensorDyn {
1622    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1623        dispatch!(self, fmt, f)
1624    }
1625}
1626
1627/// The C-facing handle pointer type. Declared in both backends with the same
1628/// name, so a `-capi` crate's signatures do not change with the backend.
1629pub type Raw = *mut std::ffi::c_void;
1630
1631impl TensorDyn {
1632    /// Give up ownership, yielding a raw handle the caller must return to
1633    /// [`TensorDyn::from_raw`] or leak.
1634    pub fn into_raw(self) -> Raw {
1635        Box::into_raw(Box::new(self)) as Raw
1636    }
1637
1638    /// Retake ownership of a handle produced by [`TensorDyn::into_raw`].
1639    ///
1640    /// # Safety
1641    /// `p` must have come from `into_raw` in this build and must not have
1642    /// been passed here before.
1643    pub unsafe fn from_raw(p: Raw) -> TensorDyn {
1644        *unsafe { Box::from_raw(p as *mut TensorDyn) }
1645    }
1646
1647    /// Borrow a `TensorDyn` from a raw handle for the duration of `f`.
1648    /// `ManuallyDrop` prevents ownership — and the destructor — being taken.
1649    ///
1650    /// # Safety
1651    /// `p` must be a live handle from `into_raw` in this build.
1652    pub unsafe fn with_raw<R>(p: Raw, f: impl FnOnce(&mut TensorDyn) -> R) -> R {
1653        let mut t = std::mem::ManuallyDrop::new(unsafe { std::ptr::read(p as *mut TensorDyn) });
1654        f(&mut t)
1655    }
1656}
1657
1658#[cfg(test)]
1659mod tests {
1660    use super::*;
1661
1662    #[test]
1663    fn from_typed_tensor() {
1664        let t = Tensor::<u8>::new(&[10], None, None).unwrap();
1665        let dyn_t: TensorDyn = t.into();
1666        assert_eq!(dyn_t.dtype(), DType::U8);
1667        assert_eq!(dyn_t.shape(), &[10]);
1668    }
1669
1670    #[test]
1671    fn from_foreign_ptr_wraps_borrowed_memory() {
1672        use crate::TensorMapTrait;
1673        // The CUDA zero-copy export shape: wrap an externally-allocated buffer as
1674        // a type-erased Mem tensor, with an owner that frees it on last drop.
1675        let mut vec: Vec<f32> = vec![0.0; 4];
1676        let ptr = vec.as_mut_ptr() as *mut u8;
1677        let owner: crate::ForeignOwner = Box::new(vec);
1678        let t = unsafe {
1679            TensorDyn::from_foreign_ptr(ptr, &[2, 2], DType::F32, Some(owner), Some("trt_output"))
1680        }
1681        .unwrap();
1682        assert_eq!(t.dtype(), DType::F32);
1683        assert_eq!(t.memory(), TensorMemory::Mem);
1684        assert_eq!(t.shape(), &[2, 2]);
1685        {
1686            let mut m = t.as_f32().unwrap().map().unwrap();
1687            m.as_mut_slice().copy_from_slice(&[1.0, 2.0, 3.0, 4.0]);
1688        }
1689        let m = t.as_f32().unwrap().map().unwrap();
1690        assert_eq!(m.as_slice(), &[1.0, 2.0, 3.0, 4.0]);
1691    }
1692
1693    // -------------------------------------------------------------------------
1694    // TensorDyn::from_foreign_ptr guard paths.
1695    //
1696    // The happy path (F32) is covered by `from_foreign_ptr_wraps_borrowed_memory`
1697    // above. These cells add the null-ptr, empty-shape, and overflow guards, plus
1698    // a U8 dtype to confirm the match-arm dispatch is exercised for integer types.
1699    // -------------------------------------------------------------------------
1700
1701    #[test]
1702    fn from_foreign_ptr_rejects_null_ptr() {
1703        let err = unsafe {
1704            TensorDyn::from_foreign_ptr(std::ptr::null_mut(), &[4], DType::U8, None, None)
1705        }
1706        .unwrap_err();
1707        // The null guard fires inside Tensor<u8>::from_foreign.
1708        assert!(
1709            matches!(err, crate::error::Error::InvalidArgument(ref m) if m.contains("non-null")),
1710            "expected InvalidArgument(non-null), got {err:?}"
1711        );
1712    }
1713
1714    #[test]
1715    fn from_foreign_ptr_rejects_empty_shape() {
1716        let mut dummy: u8 = 0;
1717        let err = unsafe {
1718            TensorDyn::from_foreign_ptr(&mut dummy as *mut u8, &[], DType::U8, None, None)
1719        }
1720        .unwrap_err();
1721        assert!(
1722            matches!(err, crate::error::Error::InvalidSize(0)),
1723            "expected InvalidSize(0) for empty shape, got {err:?}"
1724        );
1725    }
1726
1727    #[test]
1728    fn from_foreign_ptr_rejects_overflow_shape() {
1729        let mut dummy: u8 = 0;
1730        let huge = [usize::MAX / 2 + 1, 2];
1731        let err = unsafe { TensorDyn::from_foreign_ptr(&mut dummy, &huge, DType::U8, None, None) }
1732            .unwrap_err();
1733        assert!(
1734            matches!(err, crate::error::Error::InvalidArgument(ref m) if m.contains("overflow")),
1735            "expected InvalidArgument(overflow), got {err:?}"
1736        );
1737    }
1738
1739    #[test]
1740    fn from_foreign_ptr_u8_dtype_dispatch() {
1741        // Exercises the U8 arm of from_foreign_ptr's match, which wraps
1742        // the raw pointer as Tensor<u8> and downcasts correctly.
1743        let mut buf: Vec<u8> = vec![1, 2, 3, 4];
1744        let ptr = buf.as_mut_ptr();
1745        let owner: crate::ForeignOwner = Box::new(buf);
1746        let t = unsafe {
1747            TensorDyn::from_foreign_ptr(ptr, &[4], DType::U8, Some(owner), Some("u8_foreign"))
1748        }
1749        .unwrap();
1750        assert_eq!(t.dtype(), DType::U8);
1751        assert_eq!(t.shape(), &[4]);
1752        let m = t.as_u8().unwrap().map().unwrap();
1753        use crate::TensorMapTrait;
1754        assert_eq!(m.as_slice(), &[1u8, 2, 3, 4]);
1755    }
1756
1757    #[test]
1758    fn map_bytes_roundtrips_and_is_typed_agnostic() {
1759        use crate::TensorMapTrait;
1760        // f32: exercises the byte-length-vs-element-length distinction
1761        // `into_bytes` has to get right (4 elements, but 16 bytes).
1762        let t = Tensor::<f32>::new(&[4], None, None).unwrap();
1763        let dyn_t: TensorDyn = t.into();
1764        {
1765            let mut view = dyn_t.map_bytes(crate::CpuAccess::ReadWrite).unwrap();
1766            assert_eq!(view.len(), 16, "4 x f32 is 16 bytes, not 4");
1767            view.as_mut_slice()
1768                .copy_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
1769        }
1770        let view = dyn_t.map_bytes(crate::CpuAccess::Read).unwrap();
1771        assert_eq!(
1772            view.as_slice(),
1773            &[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
1774        );
1775    }
1776
1777    #[test]
1778    fn map_bytes_none_is_invalid() {
1779        let t = Tensor::<u8>::new(&[4], None, None).unwrap();
1780        let dyn_t: TensorDyn = t.into();
1781        assert!(matches!(
1782            dyn_t.map_bytes(crate::CpuAccess::None),
1783            Err(crate::error::Error::InvalidArgument(_))
1784        ));
1785    }
1786
1787    #[test]
1788    fn downcast_ref() {
1789        let t = Tensor::<u8>::new(&[10], None, None).unwrap();
1790        let dyn_t: TensorDyn = t.into();
1791        assert!(dyn_t.as_u8().is_some());
1792        assert!(dyn_t.as_i8().is_none());
1793    }
1794
1795    #[test]
1796    fn downcast_into() {
1797        let t = Tensor::<u8>::new(&[10], None, None).unwrap();
1798        let dyn_t: TensorDyn = t.into();
1799        let back = dyn_t.into_u8().unwrap();
1800        assert_eq!(back.shape(), &[10]);
1801    }
1802
1803    #[test]
1804    fn image_accessors() {
1805        let t = Tensor::<u8>::image(
1806            640,
1807            480,
1808            PixelFormat::Rgba,
1809            None,
1810            crate::CpuAccess::ReadWrite,
1811        )
1812        .unwrap();
1813        let dyn_t: TensorDyn = t.into();
1814        assert_eq!(dyn_t.format(), Some(PixelFormat::Rgba));
1815        assert_eq!(dyn_t.width(), Some(640));
1816        assert_eq!(dyn_t.height(), Some(480));
1817        assert!(!dyn_t.is_multiplane());
1818    }
1819
1820    #[test]
1821    fn cpu_access_reports_the_declared_access() {
1822        // A Read-declared image tensor reports Read...
1823        let read_t =
1824            Tensor::<u8>::image(4, 4, PixelFormat::Rgba, None, crate::CpuAccess::Read).unwrap();
1825        let dyn_read: TensorDyn = read_t.into();
1826        assert_eq!(dyn_read.cpu_access(), crate::CpuAccess::Read);
1827        assert!(!dyn_read.cpu_access().writes());
1828
1829        // ...and a plain mem tensor reports whatever the typed constructor
1830        // actually defaults to (ReadWrite -- `Tensor::new`'s historical
1831        // implicit behavior, per `CpuAccess::ReadWrite`'s own doc comment).
1832        let mem_t = Tensor::<u8>::new(&[4], None, None).unwrap();
1833        let dyn_mem: TensorDyn = mem_t.into();
1834        assert_eq!(dyn_mem.cpu_access(), crate::CpuAccess::ReadWrite);
1835        assert!(dyn_mem.cpu_access().writes());
1836    }
1837
1838    #[test]
1839    fn image_constructor() {
1840        let dyn_t = TensorDyn::image(
1841            640,
1842            480,
1843            PixelFormat::Rgb,
1844            DType::U8,
1845            None,
1846            crate::CpuAccess::ReadWrite,
1847        )
1848        .unwrap();
1849        assert_eq!(dyn_t.dtype(), DType::U8);
1850        assert_eq!(dyn_t.format(), Some(PixelFormat::Rgb));
1851        assert_eq!(dyn_t.width(), Some(640));
1852    }
1853
1854    #[test]
1855    fn image_constructor_i8() {
1856        let dyn_t = TensorDyn::image(
1857            640,
1858            480,
1859            PixelFormat::Rgb,
1860            DType::I8,
1861            None,
1862            crate::CpuAccess::ReadWrite,
1863        )
1864        .unwrap();
1865        assert_eq!(dyn_t.dtype(), DType::I8);
1866        assert_eq!(dyn_t.format(), Some(PixelFormat::Rgb));
1867    }
1868
1869    #[test]
1870    fn set_format_packed() {
1871        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
1872        assert_eq!(t.format(), None);
1873        t.set_format(PixelFormat::Rgb).unwrap();
1874        assert_eq!(t.format(), Some(PixelFormat::Rgb));
1875        assert_eq!(t.width(), Some(640));
1876        assert_eq!(t.height(), Some(480));
1877    }
1878
1879    #[test]
1880    fn set_format_planar() {
1881        let mut t = TensorDyn::new(&[3, 480, 640], DType::U8, None, None).unwrap();
1882        t.set_format(PixelFormat::PlanarRgb).unwrap();
1883        assert_eq!(t.format(), Some(PixelFormat::PlanarRgb));
1884        assert_eq!(t.width(), Some(640));
1885        assert_eq!(t.height(), Some(480));
1886    }
1887
1888    #[test]
1889    fn set_format_rejects_wrong_shape() {
1890        let mut t = TensorDyn::new(&[480, 640, 4], DType::U8, None, None).unwrap();
1891        assert!(t.set_format(PixelFormat::Rgb).is_err());
1892    }
1893
1894    #[test]
1895    fn with_format_builder() {
1896        let t = TensorDyn::new(&[480, 640, 4], DType::U8, None, None)
1897            .unwrap()
1898            .with_format(PixelFormat::Rgba)
1899            .unwrap();
1900        assert_eq!(t.format(), Some(PixelFormat::Rgba));
1901        assert_eq!(t.width(), Some(640));
1902        assert_eq!(t.height(), Some(480));
1903    }
1904
1905    #[cfg(target_os = "linux")]
1906    #[test]
1907    fn dmabuf_clone_mem_tensor_fails() {
1908        let t = TensorDyn::new(&[480, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
1909        assert_eq!(t.memory(), TensorMemory::Mem);
1910        assert!(t.dmabuf_clone().is_err());
1911    }
1912
1913    #[cfg(target_os = "linux")]
1914    #[test]
1915    fn dmabuf_mem_tensor_fails() {
1916        let t = TensorDyn::new(&[480, 640, 3], DType::U8, Some(TensorMemory::Mem), None).unwrap();
1917        assert!(t.dmabuf().is_err());
1918    }
1919
1920    #[test]
1921    fn set_format_semi_planar_nv12() {
1922        // 720 rows = 480 * 3/2 (NV12: height + height/2 for chroma)
1923        let mut t = TensorDyn::new(&[720, 640], DType::U8, Some(TensorMemory::Mem), None).unwrap();
1924        t.set_format(PixelFormat::Nv12).unwrap();
1925        assert_eq!(t.format(), Some(PixelFormat::Nv12));
1926        assert_eq!(t.width(), Some(640));
1927        assert_eq!(t.height(), Some(480));
1928    }
1929
1930    #[test]
1931    fn set_format_semi_planar_nv16() {
1932        // 960 rows = 480 * 2 (NV16: height + height for chroma)
1933        let mut t = TensorDyn::new(&[960, 640], DType::U8, Some(TensorMemory::Mem), None).unwrap();
1934        t.set_format(PixelFormat::Nv16).unwrap();
1935        assert_eq!(t.format(), Some(PixelFormat::Nv16));
1936        assert_eq!(t.width(), Some(640));
1937        assert_eq!(t.height(), Some(480));
1938    }
1939
1940    #[test]
1941    fn with_format_rejects_wrong_shape() {
1942        let result = TensorDyn::new(&[480, 640, 4], DType::U8, None, None)
1943            .unwrap()
1944            .with_format(PixelFormat::Rgb);
1945        assert!(result.is_err());
1946    }
1947
1948    #[test]
1949    fn set_format_preserved_after_rejection() {
1950        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
1951        t.set_format(PixelFormat::Rgb).unwrap();
1952        assert_eq!(t.format(), Some(PixelFormat::Rgb));
1953
1954        // Rgba requires 4 channels, should fail on a 3-channel tensor
1955        assert!(t.set_format(PixelFormat::Rgba).is_err());
1956
1957        // Original format should be preserved
1958        assert_eq!(t.format(), Some(PixelFormat::Rgb));
1959    }
1960
1961    #[test]
1962    fn set_format_idempotent() {
1963        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
1964        t.set_format(PixelFormat::Rgb).unwrap();
1965        t.set_format(PixelFormat::Rgb).unwrap();
1966        assert_eq!(t.format(), Some(PixelFormat::Rgb));
1967        assert_eq!(t.width(), Some(640));
1968        assert_eq!(t.height(), Some(480));
1969    }
1970
1971    // --- Row stride tests ---
1972
1973    #[test]
1974    fn set_row_stride_valid() {
1975        // RGBA 100px wide: min stride = 400, set 512
1976        let mut t = TensorDyn::image(
1977            100,
1978            100,
1979            PixelFormat::Rgba,
1980            DType::U8,
1981            None,
1982            crate::CpuAccess::ReadWrite,
1983        )
1984        .unwrap();
1985        t.set_row_stride(512).unwrap();
1986        assert_eq!(t.row_stride(), Some(512));
1987        assert_eq!(t.effective_row_stride(), Some(512));
1988    }
1989
1990    #[test]
1991    fn set_row_stride_equals_min() {
1992        // RGB 100px: min stride = 300, set exactly 300
1993        let mut t = TensorDyn::image(
1994            100,
1995            100,
1996            PixelFormat::Rgb,
1997            DType::U8,
1998            None,
1999            crate::CpuAccess::ReadWrite,
2000        )
2001        .unwrap();
2002        t.set_row_stride(300).unwrap();
2003        assert_eq!(t.row_stride(), Some(300));
2004    }
2005
2006    #[test]
2007    fn set_row_stride_too_small() {
2008        // RGBA 64px (a 64-aligned width: 64*4 = 256, already a multiple of 64)
2009        // carries no implicit stride. min stride = 256; setting 200 must error
2010        // and leave row_stride unset. (Non-64-aligned widths now record the
2011        // padded stride at allocation — see `Tensor::image`.)
2012        let mut t = TensorDyn::image(
2013            64,
2014            100,
2015            PixelFormat::Rgba,
2016            DType::U8,
2017            None,
2018            crate::CpuAccess::ReadWrite,
2019        )
2020        .unwrap();
2021        assert!(t.set_row_stride(200).is_err());
2022        assert_eq!(t.row_stride(), None);
2023    }
2024
2025    #[test]
2026    fn set_row_stride_zero() {
2027        let mut t = TensorDyn::image(
2028            100,
2029            100,
2030            PixelFormat::Rgb,
2031            DType::U8,
2032            None,
2033            crate::CpuAccess::ReadWrite,
2034        )
2035        .unwrap();
2036        assert!(t.set_row_stride(0).is_err());
2037    }
2038
2039    #[test]
2040    fn set_row_stride_requires_format() {
2041        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
2042        assert!(t.set_row_stride(2048).is_err());
2043    }
2044
2045    #[test]
2046    fn effective_row_stride_without_stride() {
2047        // A 64-aligned-width packed image carries no explicit stride; the
2048        // effective stride falls back to the computed tight pitch. (Width 64
2049        // RGB → 64*3 = 192, already a multiple of 64, so no padding is added.
2050        // Non-aligned widths now record the padded stride — see `Tensor::image`.)
2051        let t = TensorDyn::image(
2052            64,
2053            100,
2054            PixelFormat::Rgb,
2055            DType::U8,
2056            None,
2057            crate::CpuAccess::ReadWrite,
2058        )
2059        .unwrap();
2060        assert_eq!(t.row_stride(), None);
2061        assert_eq!(t.effective_row_stride(), Some(192)); // 64 * 3
2062    }
2063
2064    #[test]
2065    fn effective_row_stride_padded_packed_dma() {
2066        // A non-64-aligned packed width on a DMA buffer records the 64-aligned
2067        // stride so the EGLImage import is accepted by Mali/Vivante (RGB 100px:
2068        // 100*3 = 300 → padded to 320). This padding is DMA-specific — host-only
2069        // memory keeps the tight pitch — so skip when DMA is unavailable (e.g. CI
2070        // without dma_heap); the behaviour is also validated on-target.
2071        let t = match TensorDyn::image(
2072            100,
2073            100,
2074            PixelFormat::Rgb,
2075            DType::U8,
2076            Some(TensorMemory::DmaBuf),
2077            crate::CpuAccess::ReadWrite,
2078        ) {
2079            Ok(t) if t.memory() == TensorMemory::DmaBuf => t,
2080            _ => return,
2081        };
2082        // 100px * 3 bytes = 300, rounded up to the 64-byte alignment DMA-BUF
2083        // imports require -> 320. That is a DMA-BUF property, not a universal
2084        // one: Android's TensorMemory::DmaBuf is an AHardwareBuffer, whose
2085        // allocator reports no intrinsic pitch here (row_stride() is None), so
2086        // asserting 320 there tests the wrong platform's invariant.
2087        if cfg!(target_os = "android") {
2088            assert!(
2089                t.row_stride().is_none() || t.row_stride() >= Some(300),
2090                "AHardwareBuffer pitch must be absent or at least the tight row"
2091            );
2092            return;
2093        }
2094        assert_eq!(t.row_stride(), Some(320));
2095        assert_eq!(t.effective_row_stride(), Some(320));
2096    }
2097
2098    #[test]
2099    fn effective_row_stride_no_format() {
2100        let t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
2101        assert_eq!(t.effective_row_stride(), None);
2102    }
2103
2104    #[test]
2105    fn with_row_stride_builder() {
2106        let t = TensorDyn::image(
2107            100,
2108            100,
2109            PixelFormat::Rgba,
2110            DType::U8,
2111            None,
2112            crate::CpuAccess::ReadWrite,
2113        )
2114        .unwrap()
2115        .with_row_stride(512)
2116        .unwrap();
2117        assert_eq!(t.row_stride(), Some(512));
2118        assert_eq!(t.effective_row_stride(), Some(512));
2119    }
2120
2121    #[test]
2122    fn with_row_stride_rejects_small() {
2123        let result = TensorDyn::image(
2124            100,
2125            100,
2126            PixelFormat::Rgba,
2127            DType::U8,
2128            None,
2129            crate::CpuAccess::ReadWrite,
2130        )
2131        .unwrap()
2132        .with_row_stride(200);
2133        assert!(result.is_err());
2134    }
2135
2136    #[test]
2137    fn set_format_clears_row_stride() {
2138        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
2139        t.set_format(PixelFormat::Rgb).unwrap();
2140        t.set_row_stride(2048).unwrap();
2141        assert_eq!(t.row_stride(), Some(2048));
2142
2143        // Incompatible format change (4-chan on 3-chan shape) fails — stride preserved
2144        let _ = t.set_format(PixelFormat::Bgra);
2145        assert_eq!(t.row_stride(), Some(2048));
2146
2147        // Re-set to same format — stride preserved
2148        t.set_format(PixelFormat::Rgb).unwrap();
2149        assert_eq!(t.row_stride(), Some(2048));
2150
2151        // Reshape clears format and stride
2152        t.reshape(&[480 * 640 * 3]).unwrap();
2153        assert_eq!(t.row_stride(), None);
2154        assert_eq!(t.format(), None);
2155    }
2156
2157    #[test]
2158    fn set_format_different_compatible_clears_stride() {
2159        // RGBA and BGRA are both 4-channel packed — switching between them
2160        // succeeds and must clear the stored stride.
2161        let mut t = TensorDyn::new(&[480, 640, 4], DType::U8, None, None).unwrap();
2162        t.set_format(PixelFormat::Rgba).unwrap();
2163        t.set_row_stride(4096).unwrap();
2164        assert_eq!(t.row_stride(), Some(4096));
2165
2166        // Successful format change to a different compatible format clears stride
2167        t.set_format(PixelFormat::Bgra).unwrap();
2168        assert_eq!(t.format(), Some(PixelFormat::Bgra));
2169        assert_eq!(t.row_stride(), None);
2170    }
2171
2172    #[test]
2173    fn set_format_same_preserves_stride() {
2174        let mut t = TensorDyn::image(
2175            100,
2176            100,
2177            PixelFormat::Rgb,
2178            DType::U8,
2179            None,
2180            crate::CpuAccess::ReadWrite,
2181        )
2182        .unwrap();
2183        t.set_row_stride(512).unwrap();
2184        // Re-setting the same format should not clear stride
2185        t.set_format(PixelFormat::Rgb).unwrap();
2186        assert_eq!(t.row_stride(), Some(512));
2187    }
2188
2189    #[test]
2190    fn effective_row_stride_planar() {
2191        let t = TensorDyn::image(
2192            640,
2193            480,
2194            PixelFormat::PlanarRgb,
2195            DType::U8,
2196            None,
2197            crate::CpuAccess::ReadWrite,
2198        )
2199        .unwrap();
2200        assert_eq!(t.effective_row_stride(), Some(640)); // planar: width only
2201    }
2202
2203    #[test]
2204    fn effective_row_stride_nv12() {
2205        let t = TensorDyn::image(
2206            640,
2207            480,
2208            PixelFormat::Nv12,
2209            DType::U8,
2210            None,
2211            crate::CpuAccess::ReadWrite,
2212        )
2213        .unwrap();
2214        assert_eq!(t.effective_row_stride(), Some(640)); // semi-planar: width only
2215    }
2216
2217    #[test]
2218    fn map_rejects_strided_tensor() {
2219        let mut t = Tensor::<u8>::image(
2220            100,
2221            100,
2222            PixelFormat::Rgba,
2223            Some(TensorMemory::Mem),
2224            crate::CpuAccess::ReadWrite,
2225        )
2226        .unwrap();
2227        // Map works before stride is set
2228        assert!(t.map().is_ok());
2229        // After setting stride, map should be rejected
2230        t.set_row_stride(512).unwrap();
2231        let err = t.map();
2232        assert!(err.is_err());
2233    }
2234
2235    // ── plane_offset tests ──────────────────────────────────────────
2236
2237    #[test]
2238    fn plane_offset_default_none() {
2239        let t = TensorDyn::image(
2240            100,
2241            100,
2242            PixelFormat::Rgba,
2243            DType::U8,
2244            None,
2245            crate::CpuAccess::ReadWrite,
2246        )
2247        .unwrap();
2248        assert_eq!(t.plane_offset(), None);
2249    }
2250
2251    #[test]
2252    fn set_plane_offset_basic() {
2253        let mut t = TensorDyn::image(
2254            100,
2255            100,
2256            PixelFormat::Rgba,
2257            DType::U8,
2258            None,
2259            crate::CpuAccess::ReadWrite,
2260        )
2261        .unwrap();
2262        t.set_plane_offset(4096);
2263        assert_eq!(t.plane_offset(), Some(4096));
2264    }
2265
2266    #[test]
2267    fn set_plane_offset_zero() {
2268        let mut t = TensorDyn::image(
2269            100,
2270            100,
2271            PixelFormat::Rgb,
2272            DType::U8,
2273            None,
2274            crate::CpuAccess::ReadWrite,
2275        )
2276        .unwrap();
2277        t.set_plane_offset(0);
2278        assert_eq!(t.plane_offset(), Some(0));
2279    }
2280
2281    #[test]
2282    fn set_plane_offset_no_format() {
2283        // plane_offset does not require format (it is format-independent)
2284        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
2285        t.set_plane_offset(4096);
2286        assert_eq!(t.plane_offset(), Some(4096));
2287    }
2288
2289    #[test]
2290    fn set_format_clears_plane_offset() {
2291        let mut t = TensorDyn::new(&[480, 640, 3], DType::U8, None, None).unwrap();
2292        t.set_format(PixelFormat::Rgb).unwrap();
2293        t.set_plane_offset(4096);
2294        assert_eq!(t.plane_offset(), Some(4096));
2295
2296        // Re-set same format — offset preserved
2297        t.set_format(PixelFormat::Rgb).unwrap();
2298        assert_eq!(t.plane_offset(), Some(4096));
2299
2300        // Reshape clears everything
2301        t.reshape(&[480 * 640 * 3]).unwrap();
2302        assert_eq!(t.plane_offset(), None);
2303        assert_eq!(t.format(), None);
2304    }
2305
2306    #[test]
2307    fn map_rejects_out_of_bounds_offset() {
2308        let mut t = Tensor::<u8>::image(
2309            100,
2310            100,
2311            PixelFormat::Rgba,
2312            Some(TensorMemory::Mem),
2313            crate::CpuAccess::ReadWrite,
2314        )
2315        .unwrap();
2316        // Map works before offset is set.
2317        assert!(t.map().is_ok());
2318        // Heap offsets are now honored, but an offset that pushes the full
2319        // logical window (40000 bytes) past the allocation must be rejected.
2320        t.set_plane_offset(4096);
2321        assert!(t.map().is_err());
2322    }
2323
2324    #[test]
2325    fn mem_subview_in_bounds_maps_at_offset() {
2326        // An in-bounds heap sub-view now maps at its offset (previously every
2327        // non-zero heap offset was rejected outright).
2328        let parent = Tensor::<u8>::image(
2329            100,
2330            100,
2331            PixelFormat::Rgba,
2332            Some(TensorMemory::Mem),
2333            crate::CpuAccess::ReadWrite,
2334        )
2335        .unwrap();
2336        // A 10x10 RGBA window (400 bytes) at byte offset 4096 fits in 40000.
2337        let view = parent.subview(4096, &[10, 10, 4]).unwrap();
2338        assert_eq!(view.plane_offset(), Some(4096));
2339        assert!(view.map().is_ok());
2340    }
2341
2342    #[test]
2343    fn dyn_batch_dispatches_every_dtype() {
2344        // `TensorDyn::batch` fans out across all 11 dtype arms via `dyn_fanout!`;
2345        // exercise each so element `n` preserves the element type and shape.
2346        // A `[N=2, 4]` raw parent: element 1 is the contiguous 4-element window.
2347        use DType::*;
2348        for dt in [U8, I8, U16, I16, U32, I32, U64, I64, F16, F32, F64] {
2349            let parent = TensorDyn::new(&[2, 4], dt, Some(TensorMemory::Mem), None).unwrap();
2350            let view = parent.batch(1).unwrap();
2351            assert_eq!(view.dtype(), dt, "batch must preserve dtype {dt:?}");
2352            assert_eq!(view.shape(), &[4], "{dt:?}");
2353        }
2354    }
2355
2356    #[test]
2357    fn map_accepts_zero_offset_tensor() {
2358        let mut t = Tensor::<u8>::image(
2359            100,
2360            100,
2361            PixelFormat::Rgba,
2362            Some(TensorMemory::Mem),
2363            crate::CpuAccess::ReadWrite,
2364        )
2365        .unwrap();
2366        t.set_plane_offset(0);
2367        // Zero offset is fine for CPU mapping
2368        assert!(t.map().is_ok());
2369    }
2370
2371    #[test]
2372    fn dyn_configure_image_nv12() {
2373        let mut t = TensorDyn::image(
2374            640,
2375            480,
2376            PixelFormat::Rgb,
2377            DType::U8,
2378            None,
2379            crate::CpuAccess::ReadWrite,
2380        )
2381        .unwrap();
2382        t.configure_image(320, 240, PixelFormat::Nv12).unwrap();
2383        assert_eq!(t.format(), Some(PixelFormat::Nv12));
2384        assert_eq!((t.width(), t.height()), (Some(320), Some(240)));
2385    }
2386
2387    #[test]
2388    fn tensordyn_colorimetry_roundtrip() {
2389        use crate::{ColorEncoding, Colorimetry, DType, PixelFormat};
2390        let mut t = TensorDyn::image(
2391            1280,
2392            720,
2393            PixelFormat::Nv12,
2394            DType::U8,
2395            None,
2396            crate::CpuAccess::ReadWrite,
2397        )
2398        .unwrap();
2399        assert_eq!(t.colorimetry(), None);
2400        let c = Colorimetry::default().with_encoding(ColorEncoding::Bt709);
2401        t.set_colorimetry(Some(c));
2402        assert_eq!(t.colorimetry(), Some(c));
2403    }
2404
2405    #[test]
2406    fn from_planes_propagates_plane_offset() {
2407        let mut luma =
2408            Tensor::<u8>::new(&[480, 640], Some(TensorMemory::Mem), Some("luma")).unwrap();
2409        luma.set_plane_offset(4096);
2410        let chroma =
2411            Tensor::<u8>::new(&[240, 640], Some(TensorMemory::Mem), Some("chroma")).unwrap();
2412        let combined = Tensor::<u8>::from_planes(luma, chroma, PixelFormat::Nv12).unwrap();
2413        assert_eq!(combined.plane_offset(), Some(4096));
2414    }
2415
2416    #[test]
2417    fn cuda_passthrough_none_for_mem_tensor() {
2418        // Build a Mem-backed dynamic tensor the same way the other tests here do,
2419        // then confirm the CUDA accessors pass through to None (no handle).
2420        let t: TensorDyn = Tensor::<f32>::new(&[10], Some(TensorMemory::Mem), None)
2421            .unwrap()
2422            .into();
2423        assert!(t.cuda().is_none());
2424        assert!(t.cuda_map().is_none());
2425    }
2426}