edgefirst-tensor 0.33.0

Zero-copy tensor memory management with DMA, shared memory, and heap backends
Documentation
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
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
// 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_v2` whose payload leads with [`TensorDesc`] (followed by
//! the quantization metadata `TensorDesc` deliberately does not carry -- see
//! `edgefirst-python-common`'s `QuantDesc`); 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_v2`, 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 crate::dtype_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(crate) fn dtype_of(d: crate::DType) -> u32 {
    d.code()
}

/// 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.
pub fn dtype_to_dtype(code: u32) -> Option<crate::DType> {
    crate::DType::from_code(code)
}

/// 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.
pub mod format {
    /// No format (non-image tensor), or a format this build predates. Not a
    /// [`crate::PixelFormat`] variant -- every code `1..=11` below is one of
    /// those, generated by the same `ef_vocabulary!` declaration as
    /// [`crate::PixelFormat::code`]; `0` is a sentinel this module alone
    /// defines, since "no format" has no corresponding Rust variant.
    pub const NONE: u32 = 0;
    pub use crate::format::pixel_format_wire::*;
}

/// Map a [`crate::PixelFormat`] to its [`mod@format`] wire code.
///
/// Compiled into both backends: same reason as [`dtype_of`].
pub(crate) fn format_of(f: crate::PixelFormat) -> u32 {
    f.code()
}

/// 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).
pub fn format_from_code(code: u32) -> Option<crate::PixelFormat> {
    crate::PixelFormat::from_code(code)
}

/// 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.
pub mod flags {
    /// [`TensorDesc::sync`] carries a real handle. When clear, `sync` is
    /// meaningless and must not be waited on.
    ///
    /// A flag rather than a sentinel because every plausible sentinel is a
    /// legal handle somewhere: `0` is a valid `GLsync`-adjacent value and a
    /// valid fd number is never negative, so `-1` and `0` cannot both be
    /// spare across the kinds this field spans.
    ///
    /// Under [`kind::D3D11_TEXTURE`] `sync` is not a handle at all: it is a
    /// value on the timeline of the fence whose NT handle
    /// [`TensorDesc::ptr`] carries, so a producer that sets this flag must
    /// also fill in `ptr`.
    ///
    /// Bit 0.
    pub const SYNC_PRESENT: u32 = 1;
}

/// Backing store kind, as reported in [`TensorDesc::kind`].
pub mod kind {
    /// Host memory: `Mem` or `Shm`. `ptr` is valid; `handle` is -1.
    pub const HOST: u32 = 0;
    /// Linux dma-buf. `handle` is the fd.
    pub const DMABUF: u32 = 1;
    /// Apple IOSurface. `handle` is the surface id.
    pub const IOSURFACE: u32 = 2;
    /// OpenGL pixel buffer object. `handle` is the buffer id.
    pub const PBO: u32 = 3;
    /// CUDA device memory. `ptr` is a device pointer, not host-addressable.
    pub const CUDA_DEVICE: u32 = 4;
    /// Windows D3D11 texture. `handle` is the texture's NT shared handle and
    /// `ptr` the device fence's NT shared handle, both valid in the producing
    /// process (other modules of the same process included); `sync` is the
    /// fence value of the last recorded GPU write when `SYNC_PRESENT` is set.
    /// The `ptr` reuse follows `PBO`'s vtable precedent.
    pub const D3D11_TEXTURE: u32 = 5;
}

/// 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.
#[repr(transparent)]
#[derive(Clone, Copy, Debug)]
pub struct SendPtr(pub *mut u8);

// 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 impl Send for SendPtr {}
unsafe impl Sync for SendPtr {}

impl SendPtr {
    /// A null pointer, for descriptors whose memory is not host-addressable.
    pub const fn null() -> Self {
        SendPtr(std::ptr::null_mut())
    }
    /// True when the descriptor carries no host address.
    pub fn is_null(&self) -> bool {
        self.0.is_null()
    }
}

/// The cross-package descriptor. Layout is part of the contract: it is
/// `#[repr(C)]` and its size is pinned by test.
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct TensorDesc {
    /// [`ABI_VERSION`]. Checked, never assumed.
    pub version: u32,
    /// One of the [`kind`] constants.
    pub kind: u32,
    /// dma-buf fd, IOSurface id, GL buffer id or D3D11 texture NT handle
    /// value; `-1` when unused.
    pub handle: i64,
    /// Host pointer when addressable, else null -- **except under
    /// [`kind::PBO`]**, where it instead carries a `*const
    /// crate::pbo::PboOpsVtable`, or null if the producer attached none,
    /// **and under [`kind::D3D11_TEXTURE`]**, where it carries the process
    /// device fence's NT handle value, or null when [`Self::sync`] carries
    /// nothing to wait on.
    /// Interpretation is keyed by [`Self::kind`], the same established
    /// pattern [`Self::sync`]'s own doc comment already documents for that
    /// field: a PBO tensor never takes a `HostPin` (`PboTensor` refuses
    /// `pin_host` — the address cannot outlive the map), so this field is
    /// otherwise permanently unused for that kind, which is exactly why it
    /// is the field that carries the vtable instead of a new one requiring
    /// a layout change. Valid under the exact same borrow contract as the
    /// host-pointer case: only while the producer's own keepalive (for the
    /// capsule protocol, the capsule's) keeps the owning tensor alive.
    pub ptr: SendPtr,
    /// Number of significant entries in `shape` and `strides`.
    pub ndim: u32,
    /// Element type, one of the [`dtype`] constants.
    ///
    /// Deliberately placed here: `ndim` leaves a 4-byte hole before the
    /// 8-aligned `shape`, so this field costs nothing. Without it `strides`
    /// — documented in *elements* — cannot be converted to bytes by a
    /// consumer, and a `Tensor<f32>` is indistinguishable from a
    /// `Tensor<u8>` of the same shape.
    pub dtype: u32,
    /// Logical shape, most-significant dimension first.
    pub shape: [u64; 8],
    /// Strides **in bytes**, signed so a consumer can express flips.
    ///
    /// Bytes rather than elements so a hardware row pitch is always
    /// representable: a pitch need not be a whole number of `dtype` elements
    /// (and a sub-byte dtype has no element size to divide by). Multiply a
    /// coordinate by the stride directly; do not scale by `dtype` size.
    pub strides: [i64; 8],
    /// DRM FourCC of the pixel format, or 0 for a non-image tensor. Kept for
    /// third-party/DRM interop; a HAL-aware consumer should prefer
    /// [`Self::format`] and fall back to this only when `format` is 0
    /// (see [`mod@format`]'s doc for why `fourcc` alone is insufficient).
    pub fourcc: u32,
    /// One of the [`mod@format`] constants, or 0 for a non-image tensor.
    pub format: u32,
    /// Packed colorimetry, or 0 when unspecified.
    pub colorimetry: u32,
    /// Bit flags; see [`mod@flags`]. Zero when nothing applies.
    ///
    /// Occupies what was a 4-byte alignment hole before `capacity`, so it
    /// cost nothing to add.
    pub flags: u32,
    /// Bytes available in the producer's underlying allocation, from this
    /// tensor's window start (its [`TensorTrait::capacity_bytes`
    /// ](crate::TensorTrait::capacity_bytes)) -- may exceed the byte size
    /// `shape`/`strides` imply when the producer over-allocated: a pool
    /// tensor sized for its largest expected image but holding a smaller
    /// one today, or row-pitch/MCU-alignment padding a decoder needs beyond
    /// the logical width. Without this, a consumer importing a `HOST`
    /// descriptor has no way to learn that headroom exists and must treat
    /// the declared shape as the allocation's exact size -- which starves a
    /// later `configure_image`/`set_logical_shape` of capacity the producer
    /// actually has. `DMABUF`/`IOSURFACE` consumers query their real
    /// capacity from the handle itself and do not depend on this field, but
    /// it is populated for every kind for consistency.
    pub capacity: u64,
    /// Completion handle for work still in flight against this tensor, or 0
    /// when [`flags::SYNC_PRESENT`] is clear.
    ///
    /// Filled in by [`kind::D3D11_TEXTURE`] producers, and left clear by
    /// every other kind in this build. It was declared before any producer
    /// set it because the field has to exist before the first release or
    /// adding it later costs a capsule-name bump, and it is the one gap the
    /// synchronous-at-the-boundary contract cannot close: a *foreign*
    /// producer with queued GPU work has no other way to say "wait on this
    /// first".
    ///
    /// Interpretation is keyed by [`Self::kind`], since each backing already
    /// implies its own fence flavour and a separate discriminant would be
    /// redundant:
    ///
    /// * [`kind::DMABUF`] -- a `sync_file` fd from
    ///   `DMA_BUF_IOCTL_EXPORT_SYNC_FILE`. The consumer owns it and must
    ///   close it.
    /// * [`kind::PBO`] -- a `GLsync` from `glFenceSync`, valid only in the
    ///   producer's share group.
    /// * [`kind::CUDA_DEVICE`] -- a `cudaEvent_t`.
    /// * [`kind::D3D11_TEXTURE`] -- a value on the timeline of the
    ///   `ID3D11Fence` [`Self::ptr`] names, not a handle of its own: the
    ///   handle and the value together are what a consumer waits on.
    /// * [`kind::HOST`], [`kind::IOSURFACE`] -- no fence flavour defined;
    ///   `SYNC_PRESENT` must be clear.
    pub sync: u64,
}

impl TensorDesc {
    /// Maximum rank the fixed-size `shape`/`strides` arrays can carry.
    pub const MAX_NDIM: usize = 8;

    /// The logical shape as a slice.
    pub fn shape(&self) -> &[u64] {
        &self.shape[..(self.ndim as usize).min(Self::MAX_NDIM)]
    }

    /// The strides as a slice, in bytes.
    pub fn strides(&self) -> &[i64] {
        &self.strides[..(self.ndim as usize).min(Self::MAX_NDIM)]
    }

    /// Total element count.
    pub fn len(&self) -> u64 {
        self.shape().iter().product()
    }

    /// True when the descriptor addresses no elements.
    pub fn is_empty(&self) -> bool {
        self.len() == 0
    }
}

// ---------------------------------------------------------------------------
// Producer side
// ---------------------------------------------------------------------------

use crate::TensorMemory;

/// 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(crate) fn kind_of(memory: TensorMemory) -> u32 {
    match memory {
        // Both are a host-addressable pointer with no handle, which is all
        // `kind` records -- it is a narrower vocabulary than `TensorMemory`
        // on purpose, not a copy of it that lost a variant.
        TensorMemory::Mem | TensorMemory::Shm => kind::HOST,
        TensorMemory::Pbo => kind::PBO,
        TensorMemory::Cuda => kind::CUDA_DEVICE,
        // Named specifically rather than through `DmaBuf`. Unreachable
        // today (no backend reports it) but correct the day one does, which
        // is cheaper than the panic the obvious `unreachable!()` becomes.
        TensorMemory::IoSurface => kind::IOSURFACE,
        TensorMemory::DmaBuf => {
            #[cfg(any(target_os = "macos", target_os = "ios"))]
            {
                kind::IOSURFACE
            }
            // Windows has no dma-buf: the shared `DmaBuf` discriminant is a
            // D3D11 texture there, whose handle field means something else
            // again (an NT handle, not an fd).
            #[cfg(target_os = "windows")]
            {
                kind::D3D11_TEXTURE
            }
            #[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "windows")))]
            {
                kind::DMABUF
            }
        }
    }
}

// ---------------------------------------------------------------------------
// 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.
#[cfg(target_os = "linux")]
pub(crate) fn dup_descriptor_fd(desc: &TensorDesc) -> crate::Result<std::os::fd::OwnedFd> {
    use std::os::fd::BorrowedFd;
    if desc.handle < 0 {
        return Err(crate::Error::InvalidArgument(
            "dma-buf descriptor carries no fd".into(),
        ));
    }
    let raw_fd = i32::try_from(desc.handle).map_err(|_| {
        crate::Error::InvalidArgument(format!(
            "dma-buf descriptor fd {} is not representable as a file descriptor",
            desc.handle
        ))
    })?;
    // SAFETY: the producer owns `desc.handle` and the capsule keepalive
    // holds it open for the duration of this call, so it is a valid fd to
    // borrow (not adopt) for the dup below.
    let borrowed = unsafe { BorrowedFd::borrow_raw(raw_fd) };
    Ok(nix::unistd::dup(borrowed)?)
}

/// 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".
#[cfg(any(target_os = "macos", target_os = "ios"))]
pub(crate) fn descriptor_surface_id(desc: &TensorDesc) -> crate::Result<u32> {
    if desc.handle < 0 {
        return Err(crate::Error::InvalidArgument(
            "IOSurface descriptor carries no surface id".into(),
        ));
    }
    u32::try_from(desc.handle).map_err(|_| {
        crate::Error::InvalidArgument(format!(
            "IOSurface descriptor id {} is out of range",
            desc.handle
        ))
    })
}

/// 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(crate) fn check_descriptor_host_ptr(desc: &TensorDesc) -> crate::Result<()> {
    if desc.ptr.is_null() {
        return Err(crate::Error::InvalidArgument(
            "host descriptor has no address: the producer was asked \
             for a descriptive-only capsule (request access=\"read\" \
             or \"readwrite\")"
                .into(),
        ));
    }
    Ok(())
}

/// 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(crate) fn descriptor_pbo_buffer_id(desc: &TensorDesc) -> crate::Result<u32> {
    if desc.handle < 0 {
        return Err(crate::Error::InvalidArgument(
            "PBO descriptor carries no buffer id".into(),
        ));
    }
    let buffer_id = u32::try_from(desc.handle).map_err(|_| {
        crate::Error::InvalidArgument(format!(
            "PBO descriptor buffer id {} is out of range",
            desc.handle
        ))
    })?;
    if desc.ptr.is_null() {
        return Err(crate::Error::InvalidArgument(
            "PBO descriptor carries no ops vtable -- the producer either \
             predates this build's cross-cdylib PBO support or its own \
             PboTensor could not build one"
                .into(),
        ));
    }
    Ok(buffer_id)
}

/// Read a `kind::D3D11_TEXTURE` descriptor's texture handle and, when the
/// producer recorded a GPU write, the fence handle plus value an import
/// waits on before reading.
///
/// Both checks together, like [`descriptor_pbo_buffer_id`]'s, because this
/// kind also spends two of the descriptor's fields on one backing:
/// `handle` names the texture and `ptr` the fence that orders access to it.
/// The completion is optional -- a producer that has queued no GPU work
/// leaves `SYNC_PRESENT` clear, and there is then nothing to wait for.
///
/// Both handles stay the producer's: they are its own, not duplicates, and
/// are valid only while the capsule keepalive holds the producing tensor
/// alive. `from_d3d11_shared_handle` duplicates what it keeps.
///
/// # Errors
///
/// [`crate::Error::InvalidArgument`] when the descriptor carries no texture
/// handle.
#[cfg(target_os = "windows")]
pub(crate) fn descriptor_d3d11_handles(
    desc: &TensorDesc,
) -> crate::Result<(
    std::os::windows::io::RawHandle,
    Option<(std::os::windows::io::RawHandle, u64)>,
)> {
    if desc.handle <= 0 {
        return Err(crate::Error::InvalidArgument(
            "D3D11_TEXTURE descriptor without a texture handle".into(),
        ));
    }
    let tex = desc.handle as usize as std::os::windows::io::RawHandle;
    let completion = (desc.flags & flags::SYNC_PRESENT != 0 && !desc.ptr.is_null())
        .then_some((desc.ptr.0 as std::os::windows::io::RawHandle, desc.sync));
    Ok((tex, completion))
}

/// The pixel format and image dimensions a `kind::D3D11_TEXTURE` import is
/// built at, with the descriptor's shape checked against them.
///
/// The dimensions come from the texture, not from `shape`: a shape cannot
/// tell an allocation grid from an addressing grid, and a producer may
/// legitimately send either (`descriptor_pinned` emits `shape()`, which is
/// the allocation shape until someone calls `set_logical_shape`). The one
/// thing the texture cannot supply is a semi-planar image's width -- such a
/// texture is as wide as the driver's row pitch -- which is why `shape` is
/// passed down to [`crate::d3d11_shared_handle_geometry`] rather than only
/// checked here.
///
/// A third spelling is accepted here and nowhere else: a packed *window* of
/// the texture, which is what a `view()` describes (`[h', w', c]` with `h'`
/// and `w'` no larger than the texture's, and no smaller than one). The
/// import still opens the whole texture; `restore_d3d11_logical_shape`
/// narrows it to the window and the producer's plane offset places it. The
/// blob transport does not take this spelling because nothing narrows on
/// that path, so a window shape would be accepted and then ignored.
///
/// # Errors
///
/// [`crate::Error::InvalidArgument`] when the descriptor names no pixel
/// format, or when its shape is none of the three spellings of the geometry
/// the texture reports. Propagates whatever opening and describing the
/// texture reports.
///
/// # Safety
///
/// `texture` must be an NT shared handle of a D3D11 texture, valid in this
/// process. It stays owned by the caller.
#[cfg(target_os = "windows")]
pub(crate) unsafe fn descriptor_d3d11_geometry(
    desc: &TensorDesc,
    texture: std::os::windows::io::RawHandle,
    shape: &[usize],
) -> crate::Result<(crate::PixelFormat, usize, usize)> {
    let format = crate::PixelFormat::from_code(desc.format).ok_or_else(|| {
        crate::Error::InvalidArgument(
            "D3D11_TEXTURE descriptor without a pixel format: the texture's own \
             description cannot say which format it was allocated for"
                .into(),
        )
    })?;
    // SAFETY: the caller guarantees `texture` is a shared NT handle valid in
    // this process.
    let (width, height) =
        unsafe { d3d11_geometry_with_rule(format, texture, shape, ShapeRule::OrWindow) }?;
    Ok((format, width, height))
}

/// Which spellings of a texture's geometry a `shape` may take.
#[cfg(target_os = "windows")]
#[derive(Clone, Copy, PartialEq, Eq)]
enum ShapeRule {
    /// The allocation shape or the addressing shape, exactly.
    #[cfg_attr(feature = "dynamic", allow(dead_code))]
    Exact,
    /// `Exact`, or a packed window of the texture (a `view()`).
    OrWindow,
}

/// Whether `shape` is a packed sub-rectangle of a `width` x `height` image
/// in `format`: the allocation spelling with a smaller height or width. A
/// window is never a semi-planar or planar image (`Tensor::view` makes
/// packed views only) and never empty.
#[cfg(target_os = "windows")]
fn is_packed_window_of(
    format: crate::PixelFormat,
    shape: &[usize],
    width: usize,
    height: usize,
) -> bool {
    if format.layout() != crate::PixelLayout::Packed {
        return false;
    }
    let [h, w, c] = shape else {
        return false;
    };
    *c == format.channels() && (1..=height).contains(h) && (1..=width).contains(w)
}

/// The image dimensions a shared D3D11 texture reports, with `shape` checked
/// against them.
///
/// Split out of [`descriptor_d3d11_geometry`] so the blob's reference-mode
/// import ([`crate::blob::import`]) makes the same refusal rather than a
/// second copy of it: the two transports differ in how they name the format,
/// not in which shapes describe a given texture.
///
/// # Errors
///
/// [`crate::Error::InvalidArgument`] when `shape` is neither spelling of the
/// geometry the texture reports. Propagates whatever opening and describing
/// the texture reports.
///
/// # Safety
///
/// `texture` must be an NT shared handle of a D3D11 texture, valid in this
/// process. It stays owned by the caller.
#[cfg(target_os = "windows")]
#[cfg_attr(feature = "dynamic", allow(dead_code))]
pub(crate) unsafe fn d3d11_geometry_checked(
    format: crate::PixelFormat,
    texture: std::os::windows::io::RawHandle,
    shape: &[usize],
) -> crate::Result<(usize, usize)> {
    // SAFETY: forwarded unchanged.
    unsafe { d3d11_geometry_with_rule(format, texture, shape, ShapeRule::Exact) }
}

#[cfg(target_os = "windows")]
unsafe fn d3d11_geometry_with_rule(
    format: crate::PixelFormat,
    texture: std::os::windows::io::RawHandle,
    shape: &[usize],
    rule: ShapeRule,
) -> crate::Result<(usize, usize)> {
    // SAFETY: the caller guarantees `texture` is a shared NT handle valid in
    // this process; the helper opens its own texture and drops it on return.
    let (width, height) =
        unsafe { crate::d3d11_shared_handle_geometry(texture, format, Some(shape)) }?;
    let allocation = format.allocation_shape(width, height);
    let addressing = format.addressing_shape(width, height);
    if allocation.as_deref() == Some(shape) || addressing.as_deref() == Some(shape) {
        return Ok((width, height));
    }
    if rule == ShapeRule::OrWindow && is_packed_window_of(format, shape, width, height) {
        return Ok((width, height));
    }
    let window = match rule {
        ShapeRule::OrWindow => " nor a packed window",
        ShapeRule::Exact => "",
    };
    Err(crate::Error::InvalidArgument(format!(
        "D3D11 texture shape {shape:?} is neither the {format:?} allocation \
         shape {allocation:?} nor the addressing shape {addressing:?}{window} of the \
         {width}x{height} texture it names"
    )))
}

/// 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(crate) struct DescParts<'a, 'p> {
    pub dims: &'a [usize],
    pub memory: TensorMemory,
    pub dtype: crate::DType,
    /// DRM FourCC for [`TensorDesc::fourcc`]; 0 when the format has none.
    pub fourcc: u32,
    /// The HAL format, source for both the row-dimension decision below and
    /// [`TensorDesc::format`] (via [`format_of`]).
    pub format: Option<crate::PixelFormat>,
    /// Row pitch in bytes; see [`from_parts`]'s doc for how it maps to a
    /// stride index.
    pub row_stride: Option<usize>,
    pub handle: i64,
    pub colorimetry: u32,
    /// Producer's [`crate::TensorTrait::capacity_bytes`] for
    /// [`TensorDesc::capacity`]; see that field's docs.
    pub capacity: u64,
    pub pin: Option<&'a crate::HostPin<'p>>,
    /// The producer's `PboOpsVtable` address, for [`TensorDesc::ptr`] under
    /// [`kind::PBO`] — see that field's own doc comment. `None` for every
    /// other kind (mutually exclusive with `pin`: a PBO tensor never has
    /// one, since `PboTensor` refuses `pin_host`).
    pub pbo_vtable_ptr: Option<*const std::ffi::c_void>,
    /// The process device fence's NT handle value, for [`TensorDesc::ptr`]
    /// under [`kind::D3D11_TEXTURE`] — the third meaning that one field
    /// carries, keyed by kind exactly as the PBO vtable is. `0` for every
    /// other kind and for a device with no fence.
    pub fence_handle: usize,
    /// The fence value of the producer's last recorded GPU write, for
    /// [`TensorDesc::sync`] under [`kind::D3D11_TEXTURE`]. `None` for every
    /// other kind and when nothing has been recorded.
    pub sync: Option<u64>,
}

/// Row-major byte strides for `shape`, with the row dimension replaced by
/// `row_stride` when one is recorded.
///
/// The one definition of the stride convention both wire formats carry: the
/// descriptor ([`from_parts`]) and the blob (`blob::export`). See
/// [`from_parts`] for which dimension is the row dimension per layout, and
/// why a planar image's plane stride follows the pitch. Also the source of
/// the C API's `ef_tensor_strides`, so every surface reports one layout.
pub fn c_byte_strides(
    shape: &[u64],
    esz: i64,
    layout: Option<crate::PixelLayout>,
    row_stride: Option<usize>,
) -> Vec<i64> {
    let mut strides = vec![0i64; shape.len()];
    let mut acc = esz;
    for (stride, dim) in strides.iter_mut().zip(shape).rev() {
        *stride = acc;
        acc *= *dim as i64;
    }
    let Some(rs) = row_stride.map(|rs| rs as i64) else {
        return strides;
    };
    // A formatted tensor one rank above its image rank is a batch `[N, ...]`
    // of images stacked at the padded pitch, so the row override applies to
    // the per-image dims and `N` steps over one padded image.
    let image_rank = match layout {
        Some(crate::PixelLayout::SemiPlanar) => Some(2),
        Some(crate::PixelLayout::Packed | crate::PixelLayout::Planar) => Some(3),
        None => None,
    };
    let batch_dims = match image_rank {
        Some(rank) if shape.len() == rank + 1 => 1,
        _ => 0,
    };
    let (lead, image) = strides.split_at_mut(batch_dims);
    let image_bytes = match (layout, image, &shape[batch_dims..]) {
        (Some(crate::PixelLayout::Planar), [plane, row, _, ..], [planes, rows, ..]) => {
            *row = rs;
            *plane = rs * *rows as i64;
            *plane * *planes as i64
        }
        // A rank-1 tensor has no row to pad.
        (_, [row, _, ..], [rows, ..]) => {
            *row = rs;
            rs * *rows as i64
        }
        _ => return strides,
    };
    if let [n] = lead {
        *n = image_bytes;
    }
    strides
}

/// 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. Dimension 0 becomes `row_stride * H` instead: a
///   strided planar tensor stores its planes as `C * H` rows of one pitch,
///   back to back (the layout `Tensor::map`, `copy_to_flat` and
///   [`PixelFormat::plane_table`](crate::PixelFormat::plane_table) all
///   assume), so the tight `H * W * size` would place plane 1 inside plane
///   0's row padding.
///
/// `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(crate) fn from_parts(parts: DescParts) -> TensorDesc {
    let DescParts {
        dims,
        memory,
        dtype,
        fourcc,
        format,
        row_stride,
        handle,
        colorimetry,
        capacity,
        pin,
        pbo_vtable_ptr,
        fence_handle,
        sync,
    } = parts;
    let desc_kind = kind_of(memory);
    // Both fields are meaningful only under `D3D11_TEXTURE`, and only when
    // the device has a fence to name: a value with no handle behind it would
    // set `SYNC_PRESENT` on a descriptor no consumer can wait on.
    let completion = (desc_kind == kind::D3D11_TEXTURE && fence_handle != 0)
        .then_some(sync)
        .flatten()
        .map(|value| (fence_handle, value));
    // `ndim` carries the TRUE rank even when it exceeds the eight slots the
    // descriptor can hold, so a consumer can detect the case and refuse.
    // Reporting the clamped value instead would ship a descriptor whose
    // `ndim` disagrees with its own `dims`, and the consumer would address
    // `product(dims[..8])` elements of a larger allocation -- silently wrong
    // data across a package boundary. This mirrors the `row_stride` override
    // below, which likewise refuses to truncate quietly.
    let ndim = dims.len();
    let filled = ndim.min(TensorDesc::MAX_NDIM);
    if ndim > TensorDesc::MAX_NDIM {
        log::warn!(
            "tensor of rank {ndim} exceeds the descriptor's {} shape slots; \
             the descriptor will be rejected on import",
            TensorDesc::MAX_NDIM
        );
    }
    let mut shape = [0u64; TensorDesc::MAX_NDIM];
    let mut strides = [0i64; TensorDesc::MAX_NDIM];
    for (i, d) in dims.iter().take(filled).enumerate() {
        shape[i] = *d as u64;
    }
    // `filled`, not `ndim`: the arrays hold MAX_NDIM slots and `ndim` may
    // legitimately exceed that.
    let byte_strides = c_byte_strides(
        &shape[..filled],
        dtype.size() as i64,
        format.map(|f| f.layout()),
        row_stride,
    );
    strides[..filled].copy_from_slice(&byte_strides);
    TensorDesc {
        version: ABI_VERSION,
        kind: desc_kind,
        handle,
        // Keyed by kind, because that is what this field's meaning is keyed
        // by. Under `D3D11_TEXTURE` it is the fence handle or nothing, even
        // when the producer also pinned a host address -- a Python capsule
        // asked for `access="read"` does pin a texture tensor, and writing
        // that address here would hand the consumer a host pointer to open
        // as a fence. The pin stays the producer's own keepalive; a D3D11
        // import reopens the texture and takes its own CPU mapping, so it
        // never wanted the address. Otherwise `pin` and `pbo_vtable_ptr`
        // are mutually exclusive by construction (see
        // `DescParts::pbo_vtable_ptr`'s own doc comment) and `pin` takes
        // precedence purely because it is the older, more general case.
        ptr: if desc_kind == kind::D3D11_TEXTURE {
            completion.map_or_else(SendPtr::null, |(h, _)| SendPtr(h as *mut u8))
        } else {
            pin.map(|p| SendPtr(p.as_mut_ptr()))
                .or_else(|| pbo_vtable_ptr.map(|p| SendPtr(p as *mut u8)))
                .unwrap_or_else(SendPtr::null)
        },
        ndim: ndim as u32,
        dtype: dtype_of(dtype),
        shape,
        strides,
        fourcc,
        format: format.map(format_of).unwrap_or(format::NONE),
        colorimetry,
        // `D3D11_TEXTURE` is the one kind that fills these in; every other
        // producer in this build leaves them clear. See the field docs.
        flags: if completion.is_some() {
            flags::SYNC_PRESENT
        } else {
            0
        },
        capacity,
        sync: completion.map_or(0, |(_, value)| value),
    }
}

// ---------------------------------------------------------------------------
// Quantization, as it crosses the capsule.
//
// This lives here rather than in `edgefirst-python-common` beside
// `TensorCapsulePayload`, which is the only thing that embeds it, for one
// reason: it is unsafe pointer/length decoding of a versioned wire format,
// and `edgefirst-python-common` cannot host a test that runs. That crate
// pins `pyo3/extension-module`, which suppresses linking libpython, so its
// test binary cannot link at all -- and `make test-rust` excludes it by
// name regardless. Here, the round trip is covered by `quant_desc_tests`
// below, which `make test` runs.
//
// It does NOT go in `TensorDesc`: that descriptor is also the C ABI's, and
// the C libraries pass real `ef_tensor` handles into one shared
// `libedgefirst_tensor.so` and never rebuild a tensor from a descriptor, so
// they never lose the metadata. See `python-common/INTEROP.md`.
// ---------------------------------------------------------------------------

/// A `*const T` that may cross a `PyCapsule`.
///
/// The same obligation [`crate::SendPtr`] discharges for
/// [`crate::TensorDesc::ptr`], for the two arrays [`QuantDesc`] points at: the
/// producer keeps them alive for the capsule's lifetime, here via
/// `TensorCapsulePayload::quant_keepalive`.
#[repr(transparent)]
#[derive(Clone, Copy, Debug)]
pub struct SendConstPtr<T>(pub *const T);

// SAFETY: the pointer is only dereferenced by a consumer that received it
// through a capsule whose `quant_keepalive` owns the pointed-to arrays, so
// the memory outlives every use. See `QuantDesc`.
unsafe impl<T> Send for SendConstPtr<T> {}
unsafe impl<T> Sync for SendConstPtr<T> {}

impl<T> SendConstPtr<T> {
    /// A null pointer, for a descriptor that carries no array.
    pub const fn null() -> Self {
        SendConstPtr(std::ptr::null())
    }
}

/// Quantization metadata, in a form that can cross an `.so` boundary.
///
/// [`crate::TensorDesc`] deliberately does not carry this: it is the *shared*
/// descriptor, pinned by `ABI_VERSION` and consumed by the C API, where
/// tensors travel as real `ef_tensor` handles into one shared
/// `libedgefirst_tensor.so` and no reconstruction ever happens. The Python
/// packages are the only consumers that rebuild a tensor from a descriptor,
/// so this rides in the capsule payload rather than widening a C ABI that
/// does not need it.
///
/// Variable-length by nature -- per-channel quantization carries one scale
/// (and optionally one zero-point) per channel -- so the arrays are
/// *borrowed*, not inlined: `scales`/`zero_points` point into the
/// `Quantization` that `TensorCapsulePayload::quant_keepalive` owns, on
/// exactly the terms [`crate::TensorDesc::ptr`] borrows the producer's host
/// address. A consumer copies the values out during import (see
/// [`QuantDesc::to_quantization`]) while the capsule is still alive, and
/// never retains the pointers.
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct QuantDesc {
    /// Number of entries in `scales` (and in `zero_points` when non-null).
    /// **Zero means the tensor carries no quantization** -- the one field a
    /// consumer must check before reading either pointer.
    pub len: u64,
    /// Channel axis for per-channel quantization, **plus one**; `0` means
    /// per-tensor. Encoded this way so the whole struct zeroes to "absent"
    /// rather than needing a separate presence flag for an `Option<usize>`.
    pub axis_plus_one: u64,
    /// `len` scale factors. Null iff `len == 0`.
    pub scales: SendConstPtr<f32>,
    /// `len` zero-points, or null for symmetric quantization.
    pub zero_points: SendConstPtr<i32>,
}

impl QuantDesc {
    /// The descriptor for a tensor with no quantization metadata.
    pub const fn absent() -> Self {
        Self {
            len: 0,
            axis_plus_one: 0,
            scales: SendConstPtr::null(),
            zero_points: SendConstPtr::null(),
        }
    }

    /// Describe `quant`, borrowing its arrays.
    ///
    /// The caller must keep `quant` alive for as long as the descriptor is
    /// readable -- in the capsule protocol, by storing the same `Arc` in
    /// `TensorCapsulePayload::quant_keepalive`.
    pub fn borrowing(quant: &crate::Quantization) -> Self {
        Self {
            len: quant.scale().len() as u64,
            axis_plus_one: quant.axis().map_or(0, |a| a as u64 + 1),
            scales: SendConstPtr(quant.scale().as_ptr()),
            zero_points: quant
                .zero_point()
                .map_or(SendConstPtr::null(), |zp| SendConstPtr(zp.as_ptr())),
        }
    }

    /// Copy the borrowed arrays into an owned [`crate::Quantization`], or `None`
    /// when the producer declared no quantization.
    ///
    /// `Ok(None)` means "the producer has none"; a descriptor that declares
    /// quantization but cannot be read back as a valid [`crate::Quantization`] is
    /// an `Err`, not a second spelling of `None`. Conflating the two is
    /// what made the bug this whole capsule version exists to fix so hard
    /// to place: a tensor that quietly arrives unquantized surfaces much
    /// later as the consumer's own "requires quantization metadata", which
    /// names the symptom and not the cause.
    ///
    /// # Safety
    ///
    /// `self` must have come from a capsule that is still alive, so the
    /// arrays `scales`/`zero_points` point at are still owned by the
    /// producer's `quant_keepalive`.
    pub unsafe fn to_quantization(&self) -> crate::Result<Option<crate::Quantization>> {
        if self.len == 0 {
            return Ok(None);
        }
        if self.scales.0.is_null() {
            return Err(crate::Error::InvalidArgument(format!(
                "malformed tensor capsule: quantization declares {} scale(s) but \
                 carries a null scale array",
                self.len
            )));
        }
        let len = self.len as usize;
        // SAFETY: the caller's obligation, above: the producer's
        // `quant_keepalive` owns these arrays and the capsule holding it is
        // alive, and `len` came from the same producer as the pointers.
        let scales = unsafe { std::slice::from_raw_parts(self.scales.0, len) }.to_vec();
        let zero_points = if self.zero_points.0.is_null() {
            None
        } else {
            Some(unsafe { std::slice::from_raw_parts(self.zero_points.0, len) }.to_vec())
        };
        // Rebuilt through the named constructors so an ill-formed
        // descriptor cannot produce a `Quantization` that the type's own
        // invariants say is impossible -- `crate::Quantization::validate` rejects
        // per-channel scales with no axis and a per-tensor scale that
        // carries one, so those shapes cannot be forced into existence here
        // either. A descriptor that names one of them is reported, not
        // silently downgraded to "unquantized".
        let axis = self.axis_plus_one.checked_sub(1).map(|a| a as usize);
        let quant = match (axis, zero_points) {
            (None, None) if len == 1 => Ok(crate::Quantization::per_tensor_symmetric(scales[0])),
            (None, Some(zp)) if len == 1 => Ok(crate::Quantization::per_tensor(scales[0], zp[0])),
            (Some(a), None) => crate::Quantization::per_channel_symmetric(scales, a),
            (Some(a), Some(zp)) => crate::Quantization::per_channel(scales, zp, a),
            // `len > 1` with no axis: per-channel scales that name no
            // channel dimension. `Quantization` has no such shape.
            (None, _) => {
                return Err(crate::Error::InvalidArgument(format!(
                    "malformed tensor capsule: {len} quantization scales but no \
                     channel axis; per-channel quantization must name one"
                )));
            }
        };
        quant.map(Some).map_err(|e| {
            crate::Error::InvalidArgument(format!(
                "malformed tensor capsule: quantization metadata is not valid: {e}"
            ))
        })
    }
}

#[cfg(test)]
mod quant_desc_tests {
    use super::QuantDesc;
    use crate::Quantization;

    /// Borrow `q`, then copy back out of the descriptor while `q` is still
    /// alive -- exactly the producer/consumer ordering the capsule enforces
    /// via `TensorCapsulePayload::quant_keepalive`.
    fn round_trip(q: &Quantization) -> Option<Quantization> {
        let desc = QuantDesc::borrowing(q);
        // SAFETY: `q` outlives `desc` here, which is the whole contract.
        unsafe { desc.to_quantization() }.expect("a well-formed descriptor must import")
    }

    #[test]
    fn absent_descriptor_is_none_not_an_error() {
        // SAFETY: an all-zero descriptor names no arrays to read.
        let imported = unsafe { QuantDesc::absent().to_quantization() };
        assert!(imported
            .expect("absent is well-formed, not an error")
            .is_none());
    }

    #[test]
    fn per_tensor_round_trips() {
        for q in [
            Quantization::per_tensor_symmetric(0.5),
            Quantization::per_tensor(0.125, -7),
        ] {
            assert_eq!(round_trip(&q).as_ref(), Some(&q));
        }
    }

    /// The arms this type exists for. A per-tensor descriptor carries one
    /// scale and no axis, so it would round-trip correctly even if the
    /// length and the axis were both ignored -- only a per-channel case
    /// exercises the borrowed variable-length arrays.
    #[test]
    fn per_channel_symmetric_round_trips_with_its_length_and_axis() {
        let q = Quantization::per_channel_symmetric(vec![0.5, 0.25, 0.125], 0)
            .expect("per-channel symmetric");
        let imported = round_trip(&q).expect("per-channel must survive the descriptor");
        assert_eq!(imported, q);
        // Spelled out rather than left to `PartialEq`: dropping the axis or
        // truncating the array to one entry is the specific failure the
        // borrowed-array encoding risks, and both would still be a valid
        // `Quantization`.
        assert_eq!(imported.scale(), &[0.5, 0.25, 0.125]);
        assert_eq!(imported.axis(), Some(0));
        assert_eq!(imported.zero_point(), None);
    }

    #[test]
    fn per_channel_asymmetric_round_trips_with_its_zero_points() {
        let q = Quantization::per_channel(vec![0.5, 0.25, 0.125], vec![1, 2, 3], 2)
            .expect("per-channel asymmetric");
        let imported = round_trip(&q).expect("per-channel must survive the descriptor");
        assert_eq!(imported, q);
        assert_eq!(imported.scale().len(), 3);
        assert_eq!(imported.axis(), Some(2));
        assert_eq!(imported.zero_point(), Some(&[1, 2, 3][..]));
    }

    /// A non-zero axis must survive as itself, not be clamped to 0 -- the
    /// `+1` encoding is the one place an off-by-one would hide.
    #[test]
    fn the_axis_plus_one_encoding_is_reversible() {
        for axis in 0..4usize {
            let q =
                Quantization::per_channel_symmetric(vec![0.5, 0.25], axis).expect("per-channel");
            assert_eq!(QuantDesc::borrowing(&q).axis_plus_one, axis as u64 + 1);
            assert_eq!(round_trip(&q).and_then(|i| i.axis()), Some(axis));
        }
    }

    /// A descriptor that survives the capsule-name check but disagrees with
    /// itself is *reported*, not forced into a `Quantization` whose own
    /// invariants say it cannot exist -- and not quietly downgraded to
    /// "unquantized" either, which would resurface as the consumer's own
    /// "requires quantization metadata" and name the symptom, not the cause.
    #[test]
    fn per_channel_without_an_axis_is_an_error_not_a_silent_none() {
        let q = Quantization::per_channel_symmetric(vec![0.5, 0.25], 0).expect("per-channel");
        let mut desc = QuantDesc::borrowing(&q);
        desc.axis_plus_one = 0;
        // SAFETY: `q` still owns the arrays `desc` points at.
        let err = unsafe { desc.to_quantization() }
            .expect_err("per-channel scales naming no axis is malformed");
        assert!(
            err.to_string().contains("no channel axis"),
            "the error must say what is wrong with the descriptor, got: {err}"
        );
    }

    /// The other malformed shape the length field admits: a non-zero count
    /// with nothing to read it from. Checked before any dereference.
    #[test]
    fn a_null_scale_array_with_a_non_zero_length_is_an_error() {
        let mut desc = QuantDesc::absent();
        desc.len = 3;
        // SAFETY: nothing is dereferenced -- the null check precedes it.
        let err =
            unsafe { desc.to_quantization() }.expect_err("a length with a null array is malformed");
        assert!(err.to_string().contains("null scale array"), "got: {err}");
    }
}

#[cfg(test)]
mod desc_tests {
    use super::{c_byte_strides, from_parts, DescParts};
    use crate::{DType, PixelFormat, PixelLayout, TensorMemory};

    #[test]
    fn a_padded_batch_steps_n_over_one_padded_image() {
        // [N, H, W, C] = [2, 2, 8, 3] at a 32 B pitch: each tile is 2 rows.
        assert_eq!(
            c_byte_strides(&[2, 2, 8, 3], 1, Some(PixelLayout::Packed), Some(32)),
            [64, 32, 3, 1]
        );
        // [N, C, H, W] = [2, 3, 4, 8] at 16 B: 3 planes of 4 rows per image.
        assert_eq!(
            c_byte_strides(&[2, 3, 4, 8], 1, Some(PixelLayout::Planar), Some(16)),
            [192, 64, 16, 1]
        );
        // [N, H*k, W] = [2, 6, 8] NV12 at 32 B: 6 combined rows per image.
        assert_eq!(
            c_byte_strides(&[2, 6, 8], 1, Some(PixelLayout::SemiPlanar), Some(32)),
            [192, 32, 1]
        );
        // Unformatted rank 4 has no batch reading: the pitch lands on dim 0.
        assert_eq!(
            c_byte_strides(&[2, 2, 8, 3], 1, None, Some(64)),
            [64, 24, 3, 1]
        );
    }

    #[test]
    fn a_tight_tensor_gets_row_major_byte_strides() {
        assert_eq!(c_byte_strides(&[3, 8], 4, None, None), [32, 4]);
        assert_eq!(
            c_byte_strides(&[48, 64, 3], 1, Some(PixelLayout::Packed), None),
            [192, 3, 1]
        );
    }

    #[test]
    fn a_recorded_pitch_replaces_the_row_stride_of_a_packed_or_semi_planar_image() {
        assert_eq!(
            c_byte_strides(&[48, 64, 3], 1, Some(PixelLayout::Packed), Some(256)),
            [256, 3, 1]
        );
        assert_eq!(
            c_byte_strides(&[48, 64], 1, Some(PixelLayout::SemiPlanar), Some(128)),
            [128, 1]
        );
    }

    /// Planes of a strided planar image are `H` rows of the pitch apart, so
    /// the plane stride follows the pitch rather than staying `H * W`.
    #[test]
    fn a_recorded_pitch_moves_both_the_row_and_the_plane_stride_of_a_planar_image() {
        assert_eq!(
            c_byte_strides(&[3, 48, 64], 1, Some(PixelLayout::Planar), Some(128)),
            [128 * 48, 128, 1]
        );
        // Bytes, not elements: an f32 planar image at a 320-byte pitch.
        assert_eq!(
            c_byte_strides(&[3, 48, 64], 4, Some(PixelLayout::Planar), Some(320)),
            [320 * 48, 320, 4]
        );
    }

    #[test]
    fn a_rank_one_tensor_has_no_row_to_pad() {
        assert_eq!(c_byte_strides(&[16], 1, None, Some(64)), [1]);
    }

    fn parts(
        memory: TensorMemory,
        fence_handle: usize,
        sync: Option<u64>,
    ) -> DescParts<'static, 'static> {
        DescParts {
            dims: &[48, 64, 3],
            memory,
            dtype: DType::U8,
            fourcc: 0,
            format: Some(PixelFormat::Rgb),
            row_stride: None,
            handle: -1,
            colorimetry: 0,
            capacity: 48 * 64 * 3,
            pin: None,
            pbo_vtable_ptr: None,
            fence_handle,
            sync,
        }
    }

    /// Only a D3D11 texture has a fence to name. A completion handed to any
    /// other kind is dropped, never advertised as `SYNC_PRESENT`.
    #[test]
    fn a_completion_on_a_kind_with_no_fence_is_not_advertised() {
        let d = from_parts(parts(TensorMemory::Mem, 5, Some(9)));
        assert_eq!(d.flags, 0);
        assert_eq!(d.sync, 0);
        assert!(d.ptr.is_null());
    }

    /// On Windows the shared `DmaBuf` discriminant is a D3D11 texture: its
    /// completion needs both the fence handle and a recorded value.
    #[cfg(target_os = "windows")]
    #[test]
    fn a_texture_completion_needs_a_fence_handle() {
        use super::flags;
        let d = from_parts(parts(TensorMemory::DmaBuf, 5, Some(9)));
        assert_eq!(d.flags, flags::SYNC_PRESENT);
        assert_eq!(d.sync, 9);
        assert_eq!(d.ptr.0 as usize, 5);

        let d = from_parts(parts(TensorMemory::DmaBuf, 0, Some(9)));
        assert_eq!(
            d.flags, 0,
            "a value with no fence handle cannot be waited on"
        );
        assert_eq!(d.sync, 0);
        assert!(d.ptr.is_null());
    }
}