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