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. A consumer needing to survive
36 /// past the capsule for any other kind (`HOST`, `IOSURFACE`) must take
37 /// its own reference explicitly — this function does not.
38 ///
39 /// # Errors
40 ///
41 /// * [`Error::NotImplemented`](crate::Error::NotImplemented) — an ABI
42 /// major this build does not understand ([`desc.version`](crate::TensorDesc::version)),
43 /// a dtype/kind code this build does not recognise, or a kind this
44 /// platform cannot import (e.g. dma-buf off Linux).
45 /// * [`Error::InvalidArgument`](crate::Error::InvalidArgument) — a
46 /// malformed descriptor: a missing fd/surface id/host address for the
47 /// kind it claims to be, or (macOS/iOS) a surface id that no longer
48 /// resolves to a live `IOSurface`.
49 pub fn import_descriptor(desc: &TensorDesc) -> Result<Self> {
50 let (dtype, shape) = validate_descriptor(desc)?;
51 let mut t = Self::import_storage(desc, &shape, dtype)?;
52 restore_descriptor_metadata(&mut t, desc, &shape)?;
53 Ok(t)
54 }
55}
56
57/// Refuse a descriptor this build cannot honour, and decode the two facts
58/// every `import_storage` arm needs from one that it can.
59///
60/// Separate from the construction switch so the refusals apply identically
61/// on both backends -- a descriptor is untrusted cross-package input, and
62/// "which producers do we refuse" is exactly the decision that must not
63/// have two implementations.
64fn validate_descriptor(desc: &TensorDesc) -> Result<(DType, Vec<usize>)> {
65 if desc.version != crate::ABI_VERSION {
66 return Err(crate::Error::NotImplemented(format!(
67 "tensor interop ABI v{} (this build understands v{})",
68 desc.version,
69 crate::ABI_VERSION
70 )));
71 }
72
73 // A producer advertising a fence we cannot wait on is a correctness
74 // problem, not a compatibility nicety: importing anyway would alias
75 // memory whose contents are still being written by the producer's
76 // device, and the corruption would be timing-dependent. `sync` is
77 // reserved in this build, so the only honest answer is to refuse.
78 if desc.ndim as usize > crate::TensorDesc::MAX_NDIM {
79 return Err(crate::Error::NotImplemented(format!(
80 "descriptor declares rank {}, but this build's descriptor \
81 carries only {} shape slots; importing would address a \
82 prefix of a larger allocation and silently read wrong data",
83 desc.ndim,
84 crate::TensorDesc::MAX_NDIM
85 )));
86 }
87 if desc.flags & crate::protocol::flags::SYNC_PRESENT != 0 {
88 return Err(crate::Error::NotImplemented(
89 "descriptor advertises SYNC_PRESENT, but waiting on a \
90 producer fence is not implemented in this build; importing \
91 would alias memory with device work still in flight"
92 .to_owned(),
93 ));
94 }
95 let dtype = crate::protocol::dtype_to_dtype(desc.dtype).ok_or_else(|| {
96 crate::Error::NotImplemented(format!("tensor interop dtype code {}", desc.dtype))
97 })?;
98 let shape: Vec<usize> = desc.shape().iter().map(|d| *d as usize).collect();
99 Ok((dtype, shape))
100}
101
102/// Restore the producer's format, row pitch and colorimetry onto a
103/// freshly-imported tensor.
104///
105/// Runs after `import_storage` on both backends, over the public setters
106/// (`set_format`, `set_row_stride`, `set_colorimetry`) both already have --
107/// which is what makes it shareable at all.
108fn restore_descriptor_metadata(
109 t: &mut TensorDyn,
110 desc: &TensorDesc,
111 shape: &[usize],
112) -> Result<()> {
113 // `format` is the HAL-aware format code; it can represent every
114 // `PixelFormat` (including Planar, which has no FourCC). Fall back
115 // to `fourcc` only when the producer left `format` at NONE, for
116 // third-party/DRM-only producers that never set it.
117 let format = if desc.format != crate::protocol::format::NONE {
118 crate::protocol::format_from_code(desc.format)
119 } else if desc.fourcc != 0 {
120 crate::PixelFormat::from_fourcc(desc.fourcc)
121 } else {
122 None
123 };
124 if let Some(fmt) = format {
125 t.set_format(fmt)?;
126 restore_imported_row_stride(t, desc, shape, fmt);
127 }
128
129 // Colorimetry does not participate in `set_format`'s validation, so
130 // this can be unconditional: `0` unpacks to all-`None`, matching a
131 // freshly-imported tensor's default and costing nothing extra.
132 t.set_colorimetry(if desc.colorimetry == 0 {
133 None
134 } else {
135 Some(crate::Colorimetry::unpack(desc.colorimetry))
136 });
137
138 Ok(())
139}
140
141/// Restore the producer's physical row pitch (the row dimension
142/// convention documented on `protocol::from_parts`) so a decode
143/// that reconfigures this import within its allocation --
144/// `configure_image`'s pool-reuse path -- keeps writing at the
145/// producer's real stride instead of recomputing a tighter one
146/// that only fits today's logical shape. Without this, `capacity`
147/// alone is not enough: `configure_image` only *prefers* a prior
148/// stride when one is recorded, so a fresh import with no stride
149/// still picks a freshly-computed (and possibly narrower) pitch.
150///
151/// `HOST` and `DMABUF`. Historical note: restoring a stride for an
152/// imported `DMABUF` used to be actively harmful -- `Tensor::map`'s
153/// strided path rejected any imported (non-self-allocated) DMA-BUF
154/// outright, so recording one here would have made a foreign
155/// DMA-BUF's CPU map fail with `InvalidOperation`. That was fixed
156/// at the map site instead (a strided imported DMA-BUF is now
157/// accepted there, bounds-checked against `buf_size` exactly like
158/// a self-allocated one is -- `fstat`-derived kernel truth when
159/// the kernel reports a usable size, the declared logical size in
160/// the rare fallback, and either way an out-of-range mmap is still
161/// rejected by the kernel), which is what makes restoring it here
162/// safe: the same argument that justified loosening `Tensor::map`
163/// applies verbatim to the value being restored here -- it is
164/// bounds-checked again by `set_row_stride`'s caller
165/// (`configure_image`, later) and by `Tensor::map` itself at write
166/// time, so an oversized or malformed producer-reported stride
167/// cannot cause an OOB write even though it is trusted without
168/// independent verification.
169/// Confirmed load-bearing, not just anticipated: a pool-sized
170/// (oversized) DMA-BUF-backed destination decoding a smaller
171/// image lost its real pitch without this, recomputing a
172/// tighter one instead -- silent misalignment for any GPU
173/// consumer reading at the true (wider) physical pitch.
174///
175/// `IOSURFACE` is not included: nothing has reported this gap for
176/// it, and unlike `DMABUF` its CPU-mapping was never restricted by
177/// `is_imported` in the first place, so there is no known-broken
178/// case pulling it in yet. The same argument would apply if one
179/// surfaces.
180fn restore_imported_row_stride(
181 t: &mut TensorDyn,
182 desc: &TensorDesc,
183 shape: &[usize],
184 fmt: crate::PixelFormat,
185) {
186 if !matches!(
187 desc.kind,
188 crate::protocol::kind::HOST | crate::protocol::kind::DMABUF
189 ) {
190 return;
191 }
192 let row_dim = match fmt.layout() {
193 crate::PixelLayout::Planar if desc.ndim as usize >= 3 => 1,
194 _ => 0,
195 };
196 if (desc.ndim as usize) < 2 {
197 return;
198 }
199 // `strides` is signed to leave room for a future flip
200 // (negative stride); nothing produces one today, and
201 // `set_row_stride` has no way to express it, so a
202 // negative value here is simply skipped rather than
203 // reinterpreted as a huge unsigned pitch.
204 let Some(&stride_bytes) = desc.strides().get(row_dim) else {
205 return;
206 };
207 let Ok(stride_bytes) = usize::try_from(stride_bytes) else {
208 return;
209 };
210 // `set_row_stride` enforces only a minimum
211 // (`stride >= min_stride`); it explicitly does no
212 // size validation, by design (it is pure layout
213 // metadata, reused by `strides_follow_row_stride
214 // _not_shape`-style tests that never touch a real
215 // allocation). Import is not that case: `desc` is
216 // an untrusted cross-package payload, and an
217 // oversized stride here would otherwise be
218 // caught only downstream, by whichever consumer
219 // happens to multiply it out first
220 // (`Tensor::map`'s strided path, or
221 // `configure_image`'s own capacity check). The
222 // multiply below is checked -- the descriptor's
223 // stride is untrusted input, so it must not wrap
224 // into a small-looking value that passes a
225 // capacity comparison it should fail.
226 //
227 // `desc.strides()` is in bytes, so there is no
228 // element->byte conversion here (and no second
229 // multiply that could overflow) any more.
230 let rows: usize = match fmt.layout() {
231 crate::PixelLayout::Planar if shape.len() >= 2 => shape[0].saturating_mul(shape[1]),
232 _ => shape.first().copied().unwrap_or(0),
233 };
234 apply_imported_row_stride(t, fmt, stride_bytes, rows);
235}
236
237fn apply_imported_row_stride(
238 t: &mut TensorDyn,
239 fmt: crate::PixelFormat,
240 stride_bytes: usize,
241 rows: usize,
242) {
243 let capacity = t.capacity_bytes();
244 match stride_bytes.checked_mul(rows) {
245 Some(needed) if needed <= capacity => {
246 if let Err(e) = t.set_row_stride(stride_bytes) {
247 log::warn!(
248 "import_descriptor: producer row_stride \
249 {stride_bytes} bytes rejected for {fmt:?} \
250 ({e}); keeping the freshly-imported tight \
251 pitch instead"
252 );
253 }
254 }
255 Some(needed) => {
256 log::warn!(
257 "import_descriptor: producer row_stride \
258 {stride_bytes} bytes × {rows} rows needs {needed} \
259 bytes, exceeding this import's capacity \
260 {capacity}; keeping the freshly-imported tight \
261 pitch instead of trusting an oversized descriptor"
262 );
263 }
264 None => {
265 log::warn!(
266 "import_descriptor: producer row_stride \
267 {stride_bytes} bytes × {rows} rows overflows \
268 usize computing bytes needed; keeping the \
269 freshly-imported tight pitch instead of trusting \
270 a malformed descriptor"
271 );
272 }
273 }
274}