Skip to main content

edgefirst_tensor/
protocol.rs

1// SPDX-FileCopyrightText: Copyright 2026 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! The cross-package tensor protocol.
5//!
6//! Independently-linked EdgeFirst packages cannot share a Rust type: each
7//! Python extension module statically links its own copy of this crate, and
8//! PyO3 caches every `#[pyclass]`'s type object in a static belonging to the
9//! module that defined it. Two modules therefore see two distinct Python
10//! classes even when the Rust types are identical.
11//!
12//! So the contract between packages is a **descriptor**, not a type. A producer
13//! exposes `__edgefirst_tensor__()` returning a `PyCapsule` named
14//! `edgefirst_tensor_v1` wrapping [`TensorDesc`]; a consumer reads the
15//! descriptor and never performs an `isinstance` check. That duck typing is
16//! what makes the protocol survive independent release cadences, and it is the
17//! same pattern numpy, pyarrow and DLPack use.
18//!
19//! DLPack cannot serve this role alone: it has no device type for dma-buf,
20//! IOSurface or PBO, which are precisely the zero-copy cases this library
21//! exists for.
22
23/// Protocol version, checked by `TensorDyn::import_descriptor` after the
24/// payload has already been read as this build's `TensorDesc`.
25///
26/// This is the **second line of defense, not the gate**: it cannot catch a
27/// producer whose `TensorDesc` is a different *size*, since the
28/// out-of-bounds/misaligned read has already happened by the time this
29/// field could be inspected. The capsule name (`edgefirst_tensor_v1`, see
30/// `INTEROP.md`'s Versioning section) is what actually gates that --
31/// checked by `PyCapsule::pointer_checked` before any byte of the payload
32/// is read. DLPack reached the same conclusion after shipping an
33/// unversioned `dltensor` capsule and finding no upgrade path: its v1.0 fix
34/// added both a version field *and* a new capsule name, because the field
35/// alone is unreadable until the layout is already known.
36///
37/// This field stays useful for the case the name rule does not cover on its
38/// own: a future change to what a *same-sized, same-layout* `TensorDesc`'s
39/// fields mean. It has not needed to move past `1`.
40pub const ABI_VERSION: u32 = 1;
41
42/// Element type, as reported in [`TensorDesc::dtype`].
43///
44/// These codes cross a package boundary and are read by value, so they are
45/// part of the ABI: append, never reorder. Re-exported from the wire module
46/// [`crate::DType`] itself generates (via `ef_vocabulary!`, see
47/// `vocabulary.rs`), so this and [`crate::DType::code`] are two views of the
48/// same declared literal per variant, not two hand-kept tables that could
49/// drift apart.
50///
51/// `protocol::dtype` is the canonical, documented path -- the source module,
52/// `crate::dtype_wire`, is `#[doc(hidden)]` because it exists only so this
53/// re-export has something public to alias; it is emission plumbing, not a
54/// second API for the same codes.
55pub use crate::dtype_wire as dtype;
56
57/// Map a [`crate::DType`] to its wire code.
58///
59/// Compiled into both backends: [`from_parts`] is reached from
60/// `TensorDyn::descriptor_pinned`, which now exists on the `dynamic`
61/// backend too (task P2a). Nothing here touches storage -- it is the wire
62/// encoding of facts both backends can already answer -- so there was never
63/// a backend-specific reason for the gate this replaces, only a
64/// dead-code-warning one.
65pub(crate) fn dtype_of(d: crate::DType) -> u32 {
66    d.code()
67}
68
69/// Map a wire [`dtype`] code back to a [`crate::DType`]. Inverse of
70/// [`dtype_of`]. `None` for a code this build does not recognise (e.g. a
71/// dtype added by a newer producer this build predates) — the caller
72/// rejects rather than guessing.
73pub fn dtype_to_dtype(code: u32) -> Option<crate::DType> {
74    crate::DType::from_code(code)
75}
76
77/// HAL pixel format, as reported in [`TensorDesc::format`].
78///
79/// Same ABI concern as [`dtype`]: these codes cross a package boundary, so
80/// they are appended, never reordered. [`crate::PixelFormat`] itself
81/// generates this module's per-variant constants (via `ef_vocabulary!`, see
82/// `vocabulary.rs`), so this and [`crate::PixelFormat::code`] are two views
83/// of the same declared literal per variant, not two hand-kept tables that
84/// could drift apart. [`format::NONE`] is the one constant this module adds
85/// beyond what the macro emits -- see its own doc.
86///
87/// This exists because [`TensorDesc::fourcc`] cannot represent every HAL
88/// format: `PlanarRgb`/`PlanarRgba` have no standard FourCC and both encode
89/// as `fourcc = 0`, the same sentinel a non-image tensor uses, so `fourcc`
90/// alone cannot tell "Planar RGB" apart from "no format". `fourcc` stays for
91/// third-party/DRM interop; a HAL-aware consumer prefers `format` and
92/// falls back to `fourcc` only when `format` is `0`.
93///
94/// `protocol::format` is the canonical, documented path -- the source
95/// module, `crate::pixel_format_wire`, is `#[doc(hidden)]` because it exists
96/// only so this re-export has something public to alias; it is emission
97/// plumbing, not a second API for the same codes.
98pub mod format {
99    /// No format (non-image tensor), or a format this build predates. Not a
100    /// [`crate::PixelFormat`] variant -- every code `1..=11` below is one of
101    /// those, generated by the same `ef_vocabulary!` declaration as
102    /// [`crate::PixelFormat::code`]; `0` is a sentinel this module alone
103    /// defines, since "no format" has no corresponding Rust variant.
104    pub const NONE: u32 = 0;
105    pub use crate::format::pixel_format_wire::*;
106}
107
108/// Map a [`crate::PixelFormat`] to its [`mod@format`] wire code.
109///
110/// Compiled into both backends: same reason as [`dtype_of`].
111pub(crate) fn format_of(f: crate::PixelFormat) -> u32 {
112    f.code()
113}
114
115/// Map a [`mod@format`] wire code back to a [`crate::PixelFormat`]. `None` for
116/// [`format::NONE`] or a code this build does not recognise (e.g. a format
117/// added by a newer producer).
118pub fn format_from_code(code: u32) -> Option<crate::PixelFormat> {
119    crate::PixelFormat::from_code(code)
120}
121
122/// Bit flags, as reported in [`TensorDesc::flags`].
123///
124/// A bitfield rather than more discrete fields: the additions this protocol
125/// is most likely to need are booleans, and each one spent as its own `u32`
126/// would grow the descriptor -- which after release costs a capsule-name
127/// bump. Spending one `u32` now buys 32 of them for free.
128pub mod flags {
129    /// [`TensorDesc::sync`] carries a real handle. When clear, `sync` is
130    /// meaningless and must not be waited on.
131    ///
132    /// A flag rather than a sentinel because every plausible sentinel is a
133    /// legal handle somewhere: `0` is a valid `GLsync`-adjacent value and a
134    /// valid fd number is never negative, so `-1` and `0` cannot both be
135    /// spare across the kinds this field spans.
136    pub const SYNC_PRESENT: u32 = 1 << 0;
137}
138
139/// Backing store kind, as reported in [`TensorDesc::kind`].
140pub mod kind {
141    /// Host memory: `Mem` or `Shm`. `ptr` is valid; `handle` is -1.
142    pub const HOST: u32 = 0;
143    /// Linux dma-buf. `handle` is the fd.
144    pub const DMABUF: u32 = 1;
145    /// Apple IOSurface. `handle` is the surface id.
146    pub const IOSURFACE: u32 = 2;
147    /// OpenGL pixel buffer object. `handle` is the buffer id.
148    pub const PBO: u32 = 3;
149    /// CUDA device memory. `ptr` is a device pointer, not host-addressable.
150    pub const CUDA_DEVICE: u32 = 4;
151}
152
153/// A raw pointer that may cross a `PyCapsule`.
154///
155/// `PyCapsule::new` requires `T: Send`, which `*mut u8` is not. The producer
156/// discharges that obligation by keeping the owning tensor alive for the
157/// capsule's lifetime via the capsule's keepalive; see the module docs.
158#[repr(transparent)]
159#[derive(Clone, Copy, Debug)]
160pub struct SendPtr(pub *mut u8);
161
162// SAFETY: the pointer is only dereferenced by a consumer that received it
163// through a capsule whose keepalive holds the producing tensor alive, so the
164// memory outlives every use.
165unsafe impl Send for SendPtr {}
166unsafe impl Sync for SendPtr {}
167
168impl SendPtr {
169    /// A null pointer, for descriptors whose memory is not host-addressable.
170    pub const fn null() -> Self {
171        SendPtr(std::ptr::null_mut())
172    }
173    /// True when the descriptor carries no host address.
174    pub fn is_null(&self) -> bool {
175        self.0.is_null()
176    }
177}
178
179/// The cross-package descriptor. Layout is part of the contract: it is
180/// `#[repr(C)]` and its size is pinned by test.
181#[repr(C)]
182#[derive(Clone, Copy, Debug)]
183pub struct TensorDesc {
184    /// [`ABI_VERSION`]. Checked, never assumed.
185    pub version: u32,
186    /// One of the [`kind`] constants.
187    pub kind: u32,
188    /// dma-buf fd, IOSurface id or GL buffer id; `-1` when unused.
189    pub handle: i64,
190    /// Host pointer when addressable, else null -- **except under
191    /// [`kind::PBO`]**, where it instead carries a `*const
192    /// crate::pbo::PboOpsVtable`, or null if the producer attached none.
193    /// Interpretation is keyed by [`Self::kind`], the same established
194    /// pattern [`Self::sync`]'s own doc comment already documents for that
195    /// field: a PBO tensor never takes a `HostPin` (`PboTensor` refuses
196    /// `pin_host` — the address cannot outlive the map), so this field is
197    /// otherwise permanently unused for that kind, which is exactly why it
198    /// is the field that carries the vtable instead of a new one requiring
199    /// a layout change. Valid under the exact same borrow contract as the
200    /// host-pointer case: only while the producer's own keepalive (for the
201    /// capsule protocol, the capsule's) keeps the owning tensor alive.
202    pub ptr: SendPtr,
203    /// Number of significant entries in `shape` and `strides`.
204    pub ndim: u32,
205    /// Element type, one of the [`dtype`] constants.
206    ///
207    /// Deliberately placed here: `ndim` leaves a 4-byte hole before the
208    /// 8-aligned `shape`, so this field costs nothing. Without it `strides`
209    /// — documented in *elements* — cannot be converted to bytes by a
210    /// consumer, and a `Tensor<f32>` is indistinguishable from a
211    /// `Tensor<u8>` of the same shape.
212    pub dtype: u32,
213    /// Logical shape, most-significant dimension first.
214    pub shape: [u64; 8],
215    /// Strides **in bytes**, signed so a consumer can express flips.
216    ///
217    /// Bytes rather than elements so a hardware row pitch is always
218    /// representable: a pitch need not be a whole number of `dtype` elements
219    /// (and a sub-byte dtype has no element size to divide by). Multiply a
220    /// coordinate by the stride directly; do not scale by `dtype` size.
221    pub strides: [i64; 8],
222    /// DRM FourCC of the pixel format, or 0 for a non-image tensor. Kept for
223    /// third-party/DRM interop; a HAL-aware consumer should prefer
224    /// [`Self::format`] and fall back to this only when `format` is 0
225    /// (see [`mod@format`]'s doc for why `fourcc` alone is insufficient).
226    pub fourcc: u32,
227    /// One of the [`mod@format`] constants, or 0 for a non-image tensor.
228    pub format: u32,
229    /// Packed colorimetry, or 0 when unspecified.
230    pub colorimetry: u32,
231    /// Bit flags; see [`mod@flags`]. Zero when nothing applies.
232    ///
233    /// Occupies what was a 4-byte alignment hole before `capacity`, so it
234    /// cost nothing to add.
235    pub flags: u32,
236    /// Bytes available in the producer's underlying allocation, from this
237    /// tensor's window start (its [`TensorTrait::capacity_bytes`
238    /// ](crate::TensorTrait::capacity_bytes)) -- may exceed the byte size
239    /// `shape`/`strides` imply when the producer over-allocated: a pool
240    /// tensor sized for its largest expected image but holding a smaller
241    /// one today, or row-pitch/MCU-alignment padding a decoder needs beyond
242    /// the logical width. Without this, a consumer importing a `HOST`
243    /// descriptor has no way to learn that headroom exists and must treat
244    /// the declared shape as the allocation's exact size -- which starves a
245    /// later `configure_image`/`set_logical_shape` of capacity the producer
246    /// actually has. `DMABUF`/`IOSURFACE` consumers query their real
247    /// capacity from the handle itself and do not depend on this field, but
248    /// it is populated for every kind for consistency.
249    pub capacity: u64,
250    /// Completion handle for work still in flight against this tensor, or 0
251    /// when [`flags::SYNC_PRESENT`] is clear.
252    ///
253    /// **Reserved: producers in this build always leave it clear.** It is
254    /// declared now because the field has to exist before the first release
255    /// or adding it later costs a capsule-name bump, and it is the one gap
256    /// the synchronous-at-the-boundary contract cannot close: a *foreign*
257    /// producer with queued GPU work has no other way to say "wait on this
258    /// first".
259    ///
260    /// Interpretation is keyed by [`Self::kind`], since each backing already
261    /// implies its own fence flavour and a separate discriminant would be
262    /// redundant:
263    ///
264    /// * [`kind::DMABUF`] -- a `sync_file` fd from
265    ///   `DMA_BUF_IOCTL_EXPORT_SYNC_FILE`. The consumer owns it and must
266    ///   close it.
267    /// * [`kind::PBO`] -- a `GLsync` from `glFenceSync`, valid only in the
268    ///   producer's share group.
269    /// * [`kind::CUDA_DEVICE`] -- a `cudaEvent_t`.
270    /// * [`kind::HOST`], [`kind::IOSURFACE`] -- no fence flavour defined;
271    ///   `SYNC_PRESENT` must be clear.
272    pub sync: u64,
273}
274
275impl TensorDesc {
276    /// Maximum rank the fixed-size `shape`/`strides` arrays can carry.
277    pub const MAX_NDIM: usize = 8;
278
279    /// The logical shape as a slice.
280    pub fn shape(&self) -> &[u64] {
281        &self.shape[..(self.ndim as usize).min(Self::MAX_NDIM)]
282    }
283
284    /// The strides as a slice, in bytes.
285    pub fn strides(&self) -> &[i64] {
286        &self.strides[..(self.ndim as usize).min(Self::MAX_NDIM)]
287    }
288
289    /// Total element count.
290    pub fn len(&self) -> u64 {
291        self.shape().iter().product()
292    }
293
294    /// True when the descriptor addresses no elements.
295    pub fn is_empty(&self) -> bool {
296        self.len() == 0
297    }
298}
299
300// ---------------------------------------------------------------------------
301// Producer side
302// ---------------------------------------------------------------------------
303
304use crate::TensorMemory;
305
306/// Map a backing store to its [`kind`] constant.
307///
308/// `TensorMemory::DmaBuf` is a deliberately shared discriminant covering DMA-BUF,
309/// IOSurface and AHardwareBuffer (it is ABI-stable across platforms), so the
310/// platform decides which protocol kind it reports.
311pub(crate) fn kind_of(memory: TensorMemory) -> u32 {
312    match memory {
313        // Both are a host-addressable pointer with no handle, which is all
314        // `kind` records -- it is a narrower vocabulary than `TensorMemory`
315        // on purpose, not a copy of it that lost a variant.
316        TensorMemory::Mem | TensorMemory::Shm => kind::HOST,
317        TensorMemory::Pbo => kind::PBO,
318        TensorMemory::Cuda => kind::CUDA_DEVICE,
319        // Named specifically rather than through `DmaBuf`. Unreachable
320        // today (no backend reports it) but correct the day one does, which
321        // is cheaper than the panic the obvious `unreachable!()` becomes.
322        TensorMemory::IoSurface => kind::IOSURFACE,
323        TensorMemory::DmaBuf => {
324            #[cfg(any(target_os = "macos", target_os = "ios"))]
325            {
326                kind::IOSURFACE
327            }
328            #[cfg(not(any(target_os = "macos", target_os = "ios")))]
329            {
330                kind::DMABUF
331            }
332        }
333    }
334}
335
336// ---------------------------------------------------------------------------
337// Consumer side: the per-kind field checks `TensorDyn::import_storage` needs
338// ---------------------------------------------------------------------------
339//
340// Each of these validates one kind's handle/pointer field and produces the
341// value a constructor takes. They live here, next to the descriptor they
342// read, rather than in either backend: a descriptor is an *untrusted*
343// cross-package payload, and two copies of "is this fd representable" or
344// "is this surface id in range" are two places for the refusals to drift.
345
346/// `dup` a `kind::DMABUF` descriptor's fd into one this process owns.
347///
348/// The producer still owns the original and will close it when its own
349/// tensor drops, so an import must have its own -- `from_fd` adopts what it
350/// is given.
351///
352/// # Errors
353///
354/// [`crate::Error::InvalidArgument`] when the descriptor carries no fd, or
355/// one not representable as a file descriptor. `as i32` would wrap a handle
356/// above `i32::MAX` onto an unrelated -- possibly live -- descriptor, which
357/// the `dup` would then adopt as the buffer.
358#[cfg(target_os = "linux")]
359pub(crate) fn dup_descriptor_fd(desc: &TensorDesc) -> crate::Result<std::os::fd::OwnedFd> {
360    use std::os::fd::BorrowedFd;
361    if desc.handle < 0 {
362        return Err(crate::Error::InvalidArgument(
363            "dma-buf descriptor carries no fd".into(),
364        ));
365    }
366    let raw_fd = i32::try_from(desc.handle).map_err(|_| {
367        crate::Error::InvalidArgument(format!(
368            "dma-buf descriptor fd {} is not representable as a file descriptor",
369            desc.handle
370        ))
371    })?;
372    // SAFETY: the producer owns `desc.handle` and the capsule keepalive
373    // holds it open for the duration of this call, so it is a valid fd to
374    // borrow (not adopt) for the dup below.
375    let borrowed = unsafe { BorrowedFd::borrow_raw(raw_fd) };
376    Ok(nix::unistd::dup(borrowed)?)
377}
378
379/// Read a `kind::IOSURFACE` descriptor's surface id.
380///
381/// # Errors
382///
383/// [`crate::Error::InvalidArgument`] when the descriptor carries no id, or
384/// one out of `u32` range. Liveness is *not* checked here -- that belongs
385/// with the lookup, which happens inside the constructor (and, under the
386/// `dynamic` backend, inside `libedgefirst_tensor.so`) so no window exists
387/// between "found it" and "retained it".
388#[cfg(any(target_os = "macos", target_os = "ios"))]
389pub(crate) fn descriptor_surface_id(desc: &TensorDesc) -> crate::Result<u32> {
390    if desc.handle < 0 {
391        return Err(crate::Error::InvalidArgument(
392            "IOSurface descriptor carries no surface id".into(),
393        ));
394    }
395    u32::try_from(desc.handle).map_err(|_| {
396        crate::Error::InvalidArgument(format!(
397            "IOSurface descriptor id {} is out of range",
398            desc.handle
399        ))
400    })
401}
402
403/// Refuse a `kind::HOST` descriptor that carries no address.
404///
405/// # Errors
406///
407/// [`crate::Error::InvalidArgument`], naming the producer-side fix: a
408/// descriptive-only capsule was requested, so no address was pinned.
409pub(crate) fn check_descriptor_host_ptr(desc: &TensorDesc) -> crate::Result<()> {
410    if desc.ptr.is_null() {
411        return Err(crate::Error::InvalidArgument(
412            "host descriptor has no address: the producer was asked \
413             for a descriptive-only capsule (request access=\"read\" \
414             or \"readwrite\")"
415                .into(),
416        ));
417    }
418    Ok(())
419}
420
421/// Read a `kind::PBO` descriptor's GL buffer id, and refuse one carrying no
422/// ops vtable.
423///
424/// Both checks together, unlike the other kinds' one apiece, because the
425/// `PBO` kind is the one that needs *two* of the descriptor's fields to be
426/// present before a constructor can be called at all -- `handle` names the
427/// buffer, `ptr` carries the vtable that can reach it, and either alone is
428/// useless.
429///
430/// # Errors
431///
432/// [`crate::Error::InvalidArgument`] when the buffer id is absent or out of
433/// range, or when no vtable address is carried.
434pub(crate) fn descriptor_pbo_buffer_id(desc: &TensorDesc) -> crate::Result<u32> {
435    if desc.handle < 0 {
436        return Err(crate::Error::InvalidArgument(
437            "PBO descriptor carries no buffer id".into(),
438        ));
439    }
440    let buffer_id = u32::try_from(desc.handle).map_err(|_| {
441        crate::Error::InvalidArgument(format!(
442            "PBO descriptor buffer id {} is out of range",
443            desc.handle
444        ))
445    })?;
446    if desc.ptr.is_null() {
447        return Err(crate::Error::InvalidArgument(
448            "PBO descriptor carries no ops vtable -- the producer either \
449             predates this build's cross-cdylib PBO support or its own \
450             PboTensor could not build one"
451                .into(),
452        ));
453    }
454    Ok(buffer_id)
455}
456
457/// Inputs to [`from_parts`], grouped into one named-field struct rather than
458/// a positional argument list.
459///
460/// This crossed nine parameters once the pixel-format and capacity inputs
461/// were added, several of them same-typed (`fourcc: u32`, a `u32`
462/// handle-adjacent value, `colorimetry: u32`) — a positional call at that
463/// width compiles happily with two arguments transposed and silently
464/// corrupts the descriptor. Named fields make that a compile error instead. Internal to
465/// this crate: `TensorDyn::descriptor_pinned` is presently the only caller --
466/// on both backends now, not just `static` (task P2a).
467pub(crate) struct DescParts<'a, 'p> {
468    pub dims: &'a [usize],
469    pub memory: TensorMemory,
470    pub dtype: crate::DType,
471    /// DRM FourCC for [`TensorDesc::fourcc`]; 0 when the format has none.
472    pub fourcc: u32,
473    /// The HAL format, source for both the row-dimension decision below and
474    /// [`TensorDesc::format`] (via [`format_of`]).
475    pub format: Option<crate::PixelFormat>,
476    /// Row pitch in bytes; see [`from_parts`]'s doc for how it maps to a
477    /// stride index.
478    pub row_stride: Option<usize>,
479    pub handle: i64,
480    pub colorimetry: u32,
481    /// Producer's [`crate::TensorTrait::capacity_bytes`] for
482    /// [`TensorDesc::capacity`]; see that field's docs.
483    pub capacity: u64,
484    pub pin: Option<&'a crate::HostPin<'p>>,
485    /// The producer's `PboOpsVtable` address, for [`TensorDesc::ptr`] under
486    /// [`kind::PBO`] — see that field's own doc comment. `None` for every
487    /// other kind (mutually exclusive with `pin`: a PBO tensor never has
488    /// one, since `PboTensor` refuses `pin_host`).
489    pub pbo_vtable_ptr: Option<*const std::ffi::c_void>,
490}
491
492/// Build a descriptor from its parts.
493///
494/// The single construction site: `TensorDyn::descriptor` and any future
495/// `Tensor<T>::descriptor` both route here, so the layout and the stride
496/// convention are defined once.
497///
498/// `ptr` is null unless a [`HostPin`](crate::HostPin) is supplied, or the
499/// tensor is PBO-backed and `pbo_vtable_ptr` is supplied instead — see
500/// [`TensorDesc::ptr`]'s own doc comment for why that one field carries two
501/// different meanings by [`TensorDesc::kind`]. A raw address is only
502/// meaningful while something guarantees the memory (or, for PBO, the
503/// `PboHandle` the vtable addresses) stays put, so the descriptor carries
504/// one only when the producer has pinned or built a vtable — and the
505/// capsule's keepalive then holds the relevant tensor alive for the
506/// consumer's benefit either way.
507///
508/// `row_stride` is the producer's [`TensorDyn::row_stride`](crate::TensorDyn::row_stride)
509/// **in bytes**, or `None` for a tightly-packed tensor. A pitch-aligned image
510/// carries padding between rows that the shape alone cannot express, so when
511/// present it overrides the stride of the dimension that is actually the row
512/// dimension for `format`'s layout. `format` is also the source of
513/// [`TensorDesc::format`] (via [`format_of`]) and is taken separately
514/// from `fourcc` rather than derived from it: `PlanarRgb`/`PlanarRgba` both
515/// encode as `fourcc = 0` (no standard FourCC exists for them), the same
516/// sentinel a non-image tensor uses, so `fourcc` alone cannot tell Planar
517/// apart from "no format" — for either purpose.
518///
519/// * Packed (`[H, W, C]`) / SemiPlanar (`[combined_H, W]`): dimension 0.
520/// * Planar (`[C, H, W]`, e.g. `PlanarRgb`): dimension 1 — dimension 0 there
521///   is the *plane* count, and clobbering it with the row pitch would
522///   corrupt the plane stride while leaving the true per-row stride
523///   (dimension 1) unfixed. `Tensor` tracks only one row-pitch scalar (no
524///   separate per-plane stride), so dimension 0 keeps its packed value here
525///   exactly as it did before any padding was known — unchanged, not
526///   recomputed.
527///
528/// `row_stride` is a byte pitch and is carried verbatim. It need not be a
529/// whole number of `dtype` elements: `set_row_stride` validates only
530/// `stride >= minimum`, not element alignment, and since strides are bytes
531/// there is no division to lose a remainder to. The element-stride
532/// representation this replaced could not express such a pitch at all — it
533/// fell back to the packed stride and reported a pitch the buffer does not
534/// have, across a package/ABI boundary.
535///
536/// `handle`, `colorimetry` and `capacity` are carried through verbatim; `-1`
537/// and `0` are the "unused"/"undefined" sentinels for the first two.
538pub(crate) fn from_parts(parts: DescParts) -> TensorDesc {
539    let DescParts {
540        dims,
541        memory,
542        dtype,
543        fourcc,
544        format,
545        row_stride,
546        handle,
547        colorimetry,
548        capacity,
549        pin,
550        pbo_vtable_ptr,
551    } = parts;
552    // `ndim` carries the TRUE rank even when it exceeds the eight slots the
553    // descriptor can hold, so a consumer can detect the case and refuse.
554    // Reporting the clamped value instead would ship a descriptor whose
555    // `ndim` disagrees with its own `dims`, and the consumer would address
556    // `product(dims[..8])` elements of a larger allocation -- silently wrong
557    // data across a package boundary. This mirrors the `row_stride` override
558    // below, which likewise refuses to truncate quietly.
559    let ndim = dims.len();
560    let filled = ndim.min(TensorDesc::MAX_NDIM);
561    if ndim > TensorDesc::MAX_NDIM {
562        log::warn!(
563            "tensor of rank {ndim} exceeds the descriptor's {} shape slots; \
564             the descriptor will be rejected on import",
565            TensorDesc::MAX_NDIM
566        );
567    }
568    let mut shape = [0u64; TensorDesc::MAX_NDIM];
569    let mut strides = [0i64; TensorDesc::MAX_NDIM];
570    for (i, d) in dims.iter().take(filled).enumerate() {
571        shape[i] = *d as u64;
572    }
573    // Row-major and contiguous, in BYTES: stride[i] = dtype size * product of
574    // trailing dims. Bytes, not elements, because a hardware pitch need not be
575    // a whole number of elements (and sub-byte dtypes have no element size at
576    // all) -- see the `row_stride` override below.
577    let mut acc: i64 = dtype.size() as i64;
578    // `filled`, not `ndim`: the arrays hold MAX_NDIM slots and `ndim` may now
579    // legitimately exceed that.
580    for i in (0..filled).rev() {
581        strides[i] = acc;
582        acc *= shape[i] as i64;
583    }
584    if ndim >= 2 {
585        if let Some(rs) = row_stride {
586            // Planar stacks channels ahead of rows ([C, H, W]): the row
587            // dimension is 1, not 0. Every other layout this protocol
588            // carries (Packed [H, W, C], SemiPlanar [combined_H, W]) has the
589            // row dimension at 0. `format` is `None` for a non-image tensor,
590            // in which case `row_stride` should not have been supplied in
591            // the first place; dimension 0 is a harmless default for that
592            // case since it matches the pre-padding behaviour.
593            let row_dim = match format.map(|f| f.layout()) {
594                Some(crate::PixelLayout::Planar) if ndim >= 3 => 1,
595                _ => 0,
596            };
597            // Bytes in, bytes out. The element-stride representation had to
598            // divide here, and a remainder made the pitch unrepresentable --
599            // it fell back to the packed stride and reported a pitch the
600            // buffer does not have. There is nothing left to get wrong.
601            strides[row_dim] = rs as i64;
602        }
603    }
604    TensorDesc {
605        version: ABI_VERSION,
606        kind: kind_of(memory),
607        handle,
608        // `pin` and `pbo_vtable_ptr` are mutually exclusive by construction
609        // (see `DescParts::pbo_vtable_ptr`'s own doc comment) -- `pin`
610        // takes precedence purely because it is the older, more general
611        // case; a real caller only ever supplies one.
612        ptr: pin
613            .map(|p| SendPtr(p.as_mut_ptr()))
614            .or_else(|| pbo_vtable_ptr.map(|p| SendPtr(p as *mut u8)))
615            .unwrap_or_else(SendPtr::null),
616        ndim: ndim as u32,
617        dtype: dtype_of(dtype),
618        shape,
619        strides,
620        fourcc,
621        format: format.map(format_of).unwrap_or(format::NONE),
622        colorimetry,
623        // Reserved; no producer in this build sets either. See the field docs.
624        flags: 0,
625        capacity,
626        sync: 0,
627    }
628}