1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
// SPDX-FileCopyrightText: Copyright 2026 Au-Zone Technologies
// SPDX-License-Identifier: Apache-2.0
//! The cross-package tensor protocol.
//!
//! Independently-linked EdgeFirst packages cannot share a Rust type: each
//! Python extension module statically links its own copy of this crate, and
//! PyO3 caches every `#[pyclass]`'s type object in a static belonging to the
//! module that defined it. Two modules therefore see two distinct Python
//! classes even when the Rust types are identical.
//!
//! So the contract between packages is a **descriptor**, not a type. A producer
//! exposes `__edgefirst_tensor__()` returning a `PyCapsule` named
//! `edgefirst_tensor_v1` wrapping [`TensorDesc`]; a consumer reads the
//! descriptor and never performs an `isinstance` check. That duck typing is
//! what makes the protocol survive independent release cadences, and it is the
//! same pattern numpy, pyarrow and DLPack use.
//!
//! DLPack cannot serve this role alone: it has no device type for dma-buf,
//! IOSurface or PBO, which are precisely the zero-copy cases this library
//! exists for.
/// Protocol version, checked by `TensorDyn::import_descriptor` after the
/// payload has already been read as this build's `TensorDesc`.
///
/// This is the **second line of defense, not the gate**: it cannot catch a
/// producer whose `TensorDesc` is a different *size*, since the
/// out-of-bounds/misaligned read has already happened by the time this
/// field could be inspected. The capsule name (`edgefirst_tensor_v1`, see
/// `INTEROP.md`'s Versioning section) is what actually gates that --
/// checked by `PyCapsule::pointer_checked` before any byte of the payload
/// is read. DLPack reached the same conclusion after shipping an
/// unversioned `dltensor` capsule and finding no upgrade path: its v1.0 fix
/// added both a version field *and* a new capsule name, because the field
/// alone is unreadable until the layout is already known.
///
/// This field stays useful for the case the name rule does not cover on its
/// own: a future change to what a *same-sized, same-layout* `TensorDesc`'s
/// fields mean. It has not needed to move past `1`.
pub const ABI_VERSION: u32 = 1;
/// Element type, as reported in [`TensorDesc::dtype`].
///
/// These codes cross a package boundary and are read by value, so they are
/// part of the ABI: append, never reorder. Re-exported from the wire module
/// [`crate::DType`] itself generates (via `ef_vocabulary!`, see
/// `vocabulary.rs`), so this and [`crate::DType::code`] are two views of the
/// same declared literal per variant, not two hand-kept tables that could
/// drift apart.
///
/// `protocol::dtype` is the canonical, documented path -- the source module,
/// `crate::dtype_wire`, is `#[doc(hidden)]` because it exists only so this
/// re-export has something public to alias; it is emission plumbing, not a
/// second API for the same codes.
pub use cratedtype_wire as dtype;
/// Map a [`crate::DType`] to its wire code.
///
/// Compiled into both backends: [`from_parts`] is reached from
/// `TensorDyn::descriptor_pinned`, which now exists on the `dynamic`
/// backend too (task P2a). Nothing here touches storage -- it is the wire
/// encoding of facts both backends can already answer -- so there was never
/// a backend-specific reason for the gate this replaces, only a
/// dead-code-warning one.
pub
/// Map a wire [`dtype`] code back to a [`crate::DType`]. Inverse of
/// [`dtype_of`]. `None` for a code this build does not recognise (e.g. a
/// dtype added by a newer producer this build predates) — the caller
/// rejects rather than guessing.
/// HAL pixel format, as reported in [`TensorDesc::format`].
///
/// Same ABI concern as [`dtype`]: these codes cross a package boundary, so
/// they are appended, never reordered. [`crate::PixelFormat`] itself
/// generates this module's per-variant constants (via `ef_vocabulary!`, see
/// `vocabulary.rs`), so this and [`crate::PixelFormat::code`] are two views
/// of the same declared literal per variant, not two hand-kept tables that
/// could drift apart. [`format::NONE`] is the one constant this module adds
/// beyond what the macro emits -- see its own doc.
///
/// This exists because [`TensorDesc::fourcc`] cannot represent every HAL
/// format: `PlanarRgb`/`PlanarRgba` have no standard FourCC and both encode
/// as `fourcc = 0`, the same sentinel a non-image tensor uses, so `fourcc`
/// alone cannot tell "Planar RGB" apart from "no format". `fourcc` stays for
/// third-party/DRM interop; a HAL-aware consumer prefers `format` and
/// falls back to `fourcc` only when `format` is `0`.
///
/// `protocol::format` is the canonical, documented path -- the source
/// module, `crate::pixel_format_wire`, is `#[doc(hidden)]` because it exists
/// only so this re-export has something public to alias; it is emission
/// plumbing, not a second API for the same codes.
/// Map a [`crate::PixelFormat`] to its [`mod@format`] wire code.
///
/// Compiled into both backends: same reason as [`dtype_of`].
pub
/// Map a [`mod@format`] wire code back to a [`crate::PixelFormat`]. `None` for
/// [`format::NONE`] or a code this build does not recognise (e.g. a format
/// added by a newer producer).
/// Bit flags, as reported in [`TensorDesc::flags`].
///
/// A bitfield rather than more discrete fields: the additions this protocol
/// is most likely to need are booleans, and each one spent as its own `u32`
/// would grow the descriptor -- which after release costs a capsule-name
/// bump. Spending one `u32` now buys 32 of them for free.
/// Backing store kind, as reported in [`TensorDesc::kind`].
/// A raw pointer that may cross a `PyCapsule`.
///
/// `PyCapsule::new` requires `T: Send`, which `*mut u8` is not. The producer
/// discharges that obligation by keeping the owning tensor alive for the
/// capsule's lifetime via the capsule's keepalive; see the module docs.
;
// SAFETY: the pointer is only dereferenced by a consumer that received it
// through a capsule whose keepalive holds the producing tensor alive, so the
// memory outlives every use.
unsafe
unsafe
/// The cross-package descriptor. Layout is part of the contract: it is
/// `#[repr(C)]` and its size is pinned by test.
// ---------------------------------------------------------------------------
// Producer side
// ---------------------------------------------------------------------------
use crateTensorMemory;
/// Map a backing store to its [`kind`] constant.
///
/// `TensorMemory::DmaBuf` is a deliberately shared discriminant covering DMA-BUF,
/// IOSurface and AHardwareBuffer (it is ABI-stable across platforms), so the
/// platform decides which protocol kind it reports.
pub
// ---------------------------------------------------------------------------
// Consumer side: the per-kind field checks `TensorDyn::import_storage` needs
// ---------------------------------------------------------------------------
//
// Each of these validates one kind's handle/pointer field and produces the
// value a constructor takes. They live here, next to the descriptor they
// read, rather than in either backend: a descriptor is an *untrusted*
// cross-package payload, and two copies of "is this fd representable" or
// "is this surface id in range" are two places for the refusals to drift.
/// `dup` a `kind::DMABUF` descriptor's fd into one this process owns.
///
/// The producer still owns the original and will close it when its own
/// tensor drops, so an import must have its own -- `from_fd` adopts what it
/// is given.
///
/// # Errors
///
/// [`crate::Error::InvalidArgument`] when the descriptor carries no fd, or
/// one not representable as a file descriptor. `as i32` would wrap a handle
/// above `i32::MAX` onto an unrelated -- possibly live -- descriptor, which
/// the `dup` would then adopt as the buffer.
pub
/// Read a `kind::IOSURFACE` descriptor's surface id.
///
/// # Errors
///
/// [`crate::Error::InvalidArgument`] when the descriptor carries no id, or
/// one out of `u32` range. Liveness is *not* checked here -- that belongs
/// with the lookup, which happens inside the constructor (and, under the
/// `dynamic` backend, inside `libedgefirst_tensor.so`) so no window exists
/// between "found it" and "retained it".
pub
/// Refuse a `kind::HOST` descriptor that carries no address.
///
/// # Errors
///
/// [`crate::Error::InvalidArgument`], naming the producer-side fix: a
/// descriptive-only capsule was requested, so no address was pinned.
pub
/// Read a `kind::PBO` descriptor's GL buffer id, and refuse one carrying no
/// ops vtable.
///
/// Both checks together, unlike the other kinds' one apiece, because the
/// `PBO` kind is the one that needs *two* of the descriptor's fields to be
/// present before a constructor can be called at all -- `handle` names the
/// buffer, `ptr` carries the vtable that can reach it, and either alone is
/// useless.
///
/// # Errors
///
/// [`crate::Error::InvalidArgument`] when the buffer id is absent or out of
/// range, or when no vtable address is carried.
pub
/// Inputs to [`from_parts`], grouped into one named-field struct rather than
/// a positional argument list.
///
/// This crossed nine parameters once the pixel-format and capacity inputs
/// were added, several of them same-typed (`fourcc: u32`, a `u32`
/// handle-adjacent value, `colorimetry: u32`) — a positional call at that
/// width compiles happily with two arguments transposed and silently
/// corrupts the descriptor. Named fields make that a compile error instead. Internal to
/// this crate: `TensorDyn::descriptor_pinned` is presently the only caller --
/// on both backends now, not just `static` (task P2a).
pub
/// Build a descriptor from its parts.
///
/// The single construction site: `TensorDyn::descriptor` and any future
/// `Tensor<T>::descriptor` both route here, so the layout and the stride
/// convention are defined once.
///
/// `ptr` is null unless a [`HostPin`](crate::HostPin) is supplied, or the
/// tensor is PBO-backed and `pbo_vtable_ptr` is supplied instead — see
/// [`TensorDesc::ptr`]'s own doc comment for why that one field carries two
/// different meanings by [`TensorDesc::kind`]. A raw address is only
/// meaningful while something guarantees the memory (or, for PBO, the
/// `PboHandle` the vtable addresses) stays put, so the descriptor carries
/// one only when the producer has pinned or built a vtable — and the
/// capsule's keepalive then holds the relevant tensor alive for the
/// consumer's benefit either way.
///
/// `row_stride` is the producer's [`TensorDyn::row_stride`](crate::TensorDyn::row_stride)
/// **in bytes**, or `None` for a tightly-packed tensor. A pitch-aligned image
/// carries padding between rows that the shape alone cannot express, so when
/// present it overrides the stride of the dimension that is actually the row
/// dimension for `format`'s layout. `format` is also the source of
/// [`TensorDesc::format`] (via [`format_of`]) and is taken separately
/// from `fourcc` rather than derived from it: `PlanarRgb`/`PlanarRgba` both
/// encode as `fourcc = 0` (no standard FourCC exists for them), the same
/// sentinel a non-image tensor uses, so `fourcc` alone cannot tell Planar
/// apart from "no format" — for either purpose.
///
/// * Packed (`[H, W, C]`) / SemiPlanar (`[combined_H, W]`): dimension 0.
/// * Planar (`[C, H, W]`, e.g. `PlanarRgb`): dimension 1 — dimension 0 there
/// is the *plane* count, and clobbering it with the row pitch would
/// corrupt the plane stride while leaving the true per-row stride
/// (dimension 1) unfixed. `Tensor` tracks only one row-pitch scalar (no
/// separate per-plane stride), so dimension 0 keeps its packed value here
/// exactly as it did before any padding was known — unchanged, not
/// recomputed.
///
/// `row_stride` is a byte pitch and is carried verbatim. It need not be a
/// whole number of `dtype` elements: `set_row_stride` validates only
/// `stride >= minimum`, not element alignment, and since strides are bytes
/// there is no division to lose a remainder to. The element-stride
/// representation this replaced could not express such a pitch at all — it
/// fell back to the packed stride and reported a pitch the buffer does not
/// have, across a package/ABI boundary.
///
/// `handle`, `colorimetry` and `capacity` are carried through verbatim; `-1`
/// and `0` are the "unused"/"undefined" sentinels for the first two.
pub