Skip to main content

edgefirst_tensor/tensor_dyn/
derived.rs

1// SPDX-FileCopyrightText: Copyright 2026 Au-Zone Technologies
2// SPDX-License-Identifier: Apache-2.0
3
4//! Methods expressible purely over the primitive `TensorDyn` API, written
5//! once and compiled into both the `static` and `dynamic` backends.
6//!
7//! [`TensorDyn::import_descriptor`] is the first resident: the consumer
8//! half of the cross-package tensor protocol. It was `static`-only until
9//! `edgefirst-python-common` -- the first consumer to call `TensorDyn` at
10//! the Rust level under `dynamic` -- needed it, and almost all of it is
11//! backend-independent. Only the per-kind *construction* switch names
12//! constructors and therefore differs; that stays in each backend as
13//! `import_storage`, and the validation before it and the metadata restore
14//! after it live here, once. Writing the whole function twice was the
15//! alternative and it is the shape this branch exists to delete: the
16//! stride-restore reasoning below is forty lines of load-bearing
17//! commentary that a second copy would eventually contradict.
18
19use crate::{DType, Result, TensorDesc, TensorDyn};
20
21impl TensorDyn {
22    /// Rebuild a tensor addressing the memory an [`crate::TensorDesc`]
23    /// describes — the consumer half of the cross-package tensor protocol
24    /// (see [`crate::protocol`]'s module docs).
25    ///
26    /// **Borrow semantics.** The result *aliases* the producer's memory; it
27    /// is valid only while the producer keeps that memory alive. For the
28    /// capsule protocol, that is the capsule's own lifetime: the capsule's
29    /// keepalive holds the producing tensor (and, for a `HOST` descriptor,
30    /// its pin) for as long as the capsule itself is alive. A `DMABUF`
31    /// import is the one exception with its own, narrower guarantee — this
32    /// function `dup`s the fd, so the imported tensor's fd stays valid even
33    /// after the producer's tensor drops (closing its own copy); the
34    /// underlying dma-buf allocation itself is still only alive as long as
35    /// *some* fd or GPU import references it. `D3D11_TEXTURE` behaves the
36    /// same way for the same reason: the import duplicates the texture and
37    /// fence NT handles it keeps, so the result outlives the producer's
38    /// tensor while the texture itself lives as long as some handle or view
39    /// references it. Both of the descriptor's handle values must be valid
40    /// **in this process** — the protocol duplicates, it does not reach into
41    /// another process's handle table (the blob transport does; see
42    /// [`crate::blob::import`]). A consumer needing to survive past the
43    /// capsule for any other kind (`HOST`, `IOSURFACE`) must take its own
44    /// reference explicitly — this function does not.
45    ///
46    /// # Errors
47    ///
48    /// * [`Error::NotImplemented`](crate::Error::NotImplemented) — an ABI
49    ///   major this build does not understand ([`desc.version`](crate::TensorDesc::version)),
50    ///   a dtype/kind code this build does not recognise, or a kind this
51    ///   platform cannot import (e.g. dma-buf off Linux).
52    /// * [`Error::InvalidArgument`](crate::Error::InvalidArgument) — a
53    ///   malformed descriptor: a missing fd/surface id/host address for the
54    ///   kind it claims to be, or (macOS/iOS) a surface id that no longer
55    ///   resolves to a live `IOSurface`.
56    pub fn import_descriptor(desc: &TensorDesc) -> Result<Self> {
57        let (dtype, shape) = validate_descriptor(desc)?;
58        let mut t = Self::import_storage(desc, &shape, dtype)?;
59        restore_descriptor_metadata(&mut t, desc, &shape)?;
60        Ok(t)
61    }
62}
63
64/// Refuse a descriptor this build cannot honour, and decode the two facts
65/// every `import_storage` arm needs from one that it can.
66///
67/// Separate from the construction switch so the refusals apply identically
68/// on both backends -- a descriptor is untrusted cross-package input, and
69/// "which producers do we refuse" is exactly the decision that must not
70/// have two implementations.
71fn validate_descriptor(desc: &TensorDesc) -> Result<(DType, Vec<usize>)> {
72    if desc.version != crate::ABI_VERSION {
73        return Err(crate::Error::NotImplemented(format!(
74            "tensor interop ABI v{} (this build understands v{})",
75            desc.version,
76            crate::ABI_VERSION
77        )));
78    }
79
80    if desc.ndim as usize > crate::TensorDesc::MAX_NDIM {
81        return Err(crate::Error::NotImplemented(format!(
82            "descriptor declares rank {}, but this build's descriptor \
83             carries only {} shape slots; importing would address a \
84             prefix of a larger allocation and silently read wrong data",
85            desc.ndim,
86            crate::TensorDesc::MAX_NDIM
87        )));
88    }
89    // A producer advertising a fence we cannot wait on is a correctness
90    // problem, not a compatibility nicety: importing anyway would alias
91    // memory whose contents are still being written by the producer's
92    // device, and the corruption would be timing-dependent.
93    //
94    // `D3D11_TEXTURE` is the one kind with a wait path: its descriptor names
95    // an `ID3D11Fence` plus a value, and `from_d3d11_shared_handle` issues
96    // the GPU-side wait before the import is usable. Every other kind still
97    // has nowhere to wait, so a producer advertising a fence is refused.
98    if desc.flags & crate::protocol::flags::SYNC_PRESENT != 0
99        && desc.kind != crate::protocol::kind::D3D11_TEXTURE
100    {
101        return Err(crate::Error::NotImplemented(
102            "descriptor advertises SYNC_PRESENT, but waiting on a \
103             producer fence is not implemented in this build; importing \
104             would alias memory with device work still in flight"
105                .to_owned(),
106        ));
107    }
108    // The same refusal for a `D3D11_TEXTURE` descriptor that advertises a
109    // fence value with no fence to read it on: `ptr` is where that kind
110    // carries the fence handle, so a null one leaves a completion nobody can
111    // wait on, which is the in-flight-write hazard again rather than a
112    // missing feature.
113    if desc.flags & crate::protocol::flags::SYNC_PRESENT != 0
114        && desc.kind == crate::protocol::kind::D3D11_TEXTURE
115        && desc.ptr.is_null()
116    {
117        return Err(crate::Error::InvalidArgument(
118            "D3D11_TEXTURE descriptor advertises SYNC_PRESENT but carries no \
119             fence handle in `ptr`; the completion it names cannot be waited on"
120                .to_owned(),
121        ));
122    }
123    let dtype = crate::protocol::dtype_to_dtype(desc.dtype).ok_or_else(|| {
124        crate::Error::NotImplemented(format!("tensor interop dtype code {}", desc.dtype))
125    })?;
126    let shape: Vec<usize> = desc.shape().iter().map(|d| *d as usize).collect();
127    Ok((dtype, shape))
128}
129
130/// Restore the producer's format, row pitch and colorimetry onto a
131/// freshly-imported tensor.
132///
133/// Runs after `import_storage` on both backends, over the public setters
134/// (`set_format`, `set_row_stride`, `set_colorimetry`) both already have --
135/// which is what makes it shareable at all.
136fn restore_descriptor_metadata(
137    t: &mut TensorDyn,
138    desc: &TensorDesc,
139    shape: &[usize],
140) -> Result<()> {
141    // `format` is the HAL-aware format code; it can represent every
142    // `PixelFormat` (including Planar, which has no FourCC). Fall back
143    // to `fourcc` only when the producer left `format` at NONE, for
144    // third-party/DRM-only producers that never set it.
145    let format = if desc.format != crate::protocol::format::NONE {
146        crate::protocol::format_from_code(desc.format)
147    } else if desc.fourcc != 0 {
148        crate::PixelFormat::from_fourcc(desc.fourcc)
149    } else {
150        None
151    };
152    if let Some(fmt) = format {
153        t.set_format(fmt)?;
154        restore_imported_row_stride(t, desc, shape, fmt);
155    }
156
157    // Colorimetry does not participate in `set_format`'s validation, so
158    // this can be unconditional: `0` unpacks to all-`None`, matching a
159    // freshly-imported tensor's default and costing nothing extra.
160    t.set_colorimetry(if desc.colorimetry == 0 {
161        None
162    } else {
163        Some(crate::Colorimetry::unpack(desc.colorimetry))
164    });
165
166    restore_d3d11_logical_shape(t, desc, shape)?;
167
168    Ok(())
169}
170
171/// Give a `kind::D3D11_TEXTURE` import the shape its producer had.
172///
173/// The import opens the texture at the format's *allocation* shape, because
174/// that is what `from_d3d11_shared_handle` builds from width and height. A
175/// producer that called `set_logical_shape` was carrying the *addressing*
176/// shape instead (`[h, w]` for a semi-planar image rather than
177/// `[combined_h, w]`), and the descriptor faithfully reported it; without
178/// this the consumer would see a shape its producer did not have. The
179/// import arm has already checked `shape` is one of those two spellings of
180/// the geometry the texture itself reports, so nothing untrusted reaches
181/// `set_logical_shape` here.
182///
183/// After the format restore, not before: `set_format` validates the shape it
184/// finds, and an addressing shape is not one it accepts for a semi-planar
185/// format (`[481, 640]` is an unreachable combined-plane height).
186///
187/// No other kind needs this. Every one of them is imported at exactly the
188/// shape the descriptor carries.
189fn restore_d3d11_logical_shape(
190    t: &mut TensorDyn,
191    desc: &TensorDesc,
192    shape: &[usize],
193) -> Result<()> {
194    if desc.kind != crate::protocol::kind::D3D11_TEXTURE || t.shape() == shape {
195        return Ok(());
196    }
197    t.set_logical_shape(shape)
198}
199
200/// Restore the producer's physical row pitch (the row dimension
201/// convention documented on `protocol::from_parts`) so a decode
202/// that reconfigures this import within its allocation --
203/// `configure_image`'s pool-reuse path -- keeps writing at the
204/// producer's real stride instead of recomputing a tighter one
205/// that only fits today's logical shape. Without this, `capacity`
206/// alone is not enough: `configure_image` only *prefers* a prior
207/// stride when one is recorded, so a fresh import with no stride
208/// still picks a freshly-computed (and possibly narrower) pitch.
209///
210/// `HOST` and `DMABUF`. Historical note: restoring a stride for an
211/// imported `DMABUF` used to be actively harmful -- `Tensor::map`'s
212/// strided path rejected any imported (non-self-allocated) DMA-BUF
213/// outright, so recording one here would have made a foreign
214/// DMA-BUF's CPU map fail with `InvalidOperation`. That was fixed
215/// at the map site instead (a strided imported DMA-BUF is now
216/// accepted there, bounds-checked against `buf_size` exactly like
217/// a self-allocated one is -- `fstat`-derived kernel truth when
218/// the kernel reports a usable size, the declared logical size in
219/// the rare fallback, and either way an out-of-range mmap is still
220/// rejected by the kernel), which is what makes restoring it here
221/// safe: the same argument that justified loosening `Tensor::map`
222/// applies verbatim to the value being restored here -- it is
223/// bounds-checked again by `set_row_stride`'s caller
224/// (`configure_image`, later) and by `Tensor::map` itself at write
225/// time, so an oversized or malformed producer-reported stride
226/// cannot cause an OOB write even though it is trusted without
227/// independent verification.
228/// Confirmed load-bearing, not just anticipated: a pool-sized
229/// (oversized) DMA-BUF-backed destination decoding a smaller
230/// image lost its real pitch without this, recomputing a
231/// tighter one instead -- silent misalignment for any GPU
232/// consumer reading at the true (wider) physical pitch.
233///
234/// `IOSURFACE` is not included: nothing has reported this gap for
235/// it, and unlike `DMABUF` its CPU-mapping was never restricted by
236/// `is_imported` in the first place, so there is no known-broken
237/// case pulling it in yet. The same argument would apply if one
238/// surfaces.
239fn restore_imported_row_stride(
240    t: &mut TensorDyn,
241    desc: &TensorDesc,
242    shape: &[usize],
243    fmt: crate::PixelFormat,
244) {
245    if !matches!(
246        desc.kind,
247        crate::protocol::kind::HOST | crate::protocol::kind::DMABUF
248    ) {
249        return;
250    }
251    let row_dim = match fmt.layout() {
252        crate::PixelLayout::Planar if desc.ndim as usize >= 3 => 1,
253        _ => 0,
254    };
255    if (desc.ndim as usize) < 2 {
256        return;
257    }
258    // `strides` is signed to leave room for a future flip
259    // (negative stride); nothing produces one today, and
260    // `set_row_stride` has no way to express it, so a
261    // negative value here is simply skipped rather than
262    // reinterpreted as a huge unsigned pitch.
263    let Some(&stride_bytes) = desc.strides().get(row_dim) else {
264        return;
265    };
266    let Ok(stride_bytes) = usize::try_from(stride_bytes) else {
267        return;
268    };
269    // `set_row_stride` enforces only a minimum
270    // (`stride >= min_stride`); it explicitly does no
271    // size validation, by design (it is pure layout
272    // metadata, reused by `strides_follow_row_stride
273    // _not_shape`-style tests that never touch a real
274    // allocation). Import is not that case: `desc` is
275    // an untrusted cross-package payload, and an
276    // oversized stride here would otherwise be
277    // caught only downstream, by whichever consumer
278    // happens to multiply it out first
279    // (`Tensor::map`'s strided path, or
280    // `configure_image`'s own capacity check). The
281    // multiply below is checked -- the descriptor's
282    // stride is untrusted input, so it must not wrap
283    // into a small-looking value that passes a
284    // capacity comparison it should fail.
285    //
286    // `desc.strides()` is in bytes, so there is no
287    // element->byte conversion here (and no second
288    // multiply that could overflow) any more.
289    let rows: usize = match fmt.layout() {
290        crate::PixelLayout::Planar if shape.len() >= 2 => shape[0].saturating_mul(shape[1]),
291        _ => shape.first().copied().unwrap_or(0),
292    };
293    apply_imported_row_stride(t, fmt, stride_bytes, rows);
294}
295
296fn apply_imported_row_stride(
297    t: &mut TensorDyn,
298    fmt: crate::PixelFormat,
299    stride_bytes: usize,
300    rows: usize,
301) {
302    let capacity = t.capacity_bytes();
303    match stride_bytes.checked_mul(rows) {
304        Some(needed) if needed <= capacity => {
305            if let Err(e) = t.set_row_stride(stride_bytes) {
306                log::warn!(
307                    "import_descriptor: producer row_stride \
308                         {stride_bytes} bytes rejected for {fmt:?} \
309                         ({e}); keeping the freshly-imported tight \
310                         pitch instead"
311                );
312            }
313        }
314        Some(needed) => {
315            log::warn!(
316                "import_descriptor: producer row_stride \
317                     {stride_bytes} bytes × {rows} rows needs {needed} \
318                     bytes, exceeding this import's capacity \
319                     {capacity}; keeping the freshly-imported tight \
320                     pitch instead of trusting an oversized descriptor"
321            );
322        }
323        None => {
324            log::warn!(
325                "import_descriptor: producer row_stride \
326                     {stride_bytes} bytes × {rows} rows overflows \
327                     usize computing bytes needed; keeping the \
328                     freshly-imported tight pitch instead of trusting \
329                     a malformed descriptor"
330            );
331        }
332    }
333}