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