Skip to main content

hurray_core/
buffer.rs

1//! Buffer protocol types for the Hurray tensor format.
2//!
3//! A **buffer handle** is the unit by which a tensor descriptor references a
4//! contiguous region of memory. This module defines the in-memory representation
5//! of buffer handles, the device tag that identifies where a buffer resides, and
6//! a colocation validator that enforces the spec's rule that all buffers within
7//! a single tensor descriptor must reside on the same device.
8//!
9//! The binary encoding of a buffer handle in the descriptor wire format is
10//! defined in `docs/spec/metadata.md § Buffer Table`. The normative rules
11//! governing alignment, device tags, ownership, and zero-copy invariants are
12//! in `docs/spec/buffer-protocol.md`.
13//!
14//! ## Quick reference
15//!
16//! | Item | Description |
17//! |------|-------------|
18//! | [`DeviceTag`] | Identifies the memory space a buffer resides in |
19//! | [`SyncMode`] | Describes how the producer–consumer memory ordering is established |
20//! | [`BufferHandle`] | Declares a buffer's size, alignment, device, and sync mode |
21//! | [`validate_colocation`] | Checks that a set of handles all share the same device |
22//! | [`MIN_BUFFER_ALIGNMENT`] | 64-byte SIMD minimum for non-empty buffers |
23//! | [`PAGE_ALIGNMENT`] | 4096-byte recommendation for GPU / IPC buffers |
24
25use std::fmt;
26
27use crate::Error;
28
29// ── PrivateTag ────────────────────────────────────────────────────────────────
30
31/// An implementation-private device tag byte, guaranteed to be in `0xF0`–`0xFE`.
32///
33/// Values of this type are constructible only via [`DeviceTag::from_byte`];
34/// direct construction is not possible from outside the crate, preventing
35/// callers from forging an out-of-range private tag.
36///
37/// # Examples
38///
39/// ```
40/// use hurray_core::{DeviceTag, PrivateTag};
41///
42/// let tag = DeviceTag::from_byte(0xF2).unwrap();
43/// if let DeviceTag::Private(pt) = tag {
44///     assert_eq!(pt.byte(), 0xF2);
45/// }
46/// ```
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49pub struct PrivateTag(u8);
50
51impl PrivateTag {
52    /// Returns the raw wire byte for this private device tag (`0xF0`–`0xFE`).
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// use hurray_core::DeviceTag;
58    ///
59    /// let tag = DeviceTag::from_byte(0xF5).unwrap();
60    /// if let DeviceTag::Private(pt) = tag {
61    ///     assert_eq!(pt.byte(), 0xF5);
62    /// }
63    /// ```
64    #[inline]
65    pub fn byte(self) -> u8 {
66        self.0
67    }
68}
69
70// ── Alignment constants ───────────────────────────────────────────────────────
71
72/// Minimum buffer alignment for SIMD compatibility (64 bytes).
73///
74/// The base address of every **non-empty** buffer MUST be aligned to at least
75/// this many bytes. This ensures compatibility with all current SIMD instruction
76/// sets (AVX-512, NEON, SVE) without per-operation alignment negotiation.
77///
78/// See `docs/spec/buffer-protocol.md § Minimum Alignment`.
79///
80/// # Examples
81///
82/// ```
83/// use hurray_core::MIN_BUFFER_ALIGNMENT;
84///
85/// assert_eq!(MIN_BUFFER_ALIGNMENT, 64);
86/// ```
87pub const MIN_BUFFER_ALIGNMENT: u32 = 64;
88
89/// Recommended alignment for GPU, IPC, and RDMA buffers (one host page = 4096 bytes).
90///
91/// Buffers shared across process boundaries (IPC) or placed in device memory
92/// (GPU) SHOULD be aligned to at least this value. Writers targeting RDMA MUST
93/// set `alignment` to at least `4096`.
94///
95/// See `docs/spec/buffer-protocol.md § Page Alignment for GPU and IPC`.
96///
97/// # Examples
98///
99/// ```
100/// use hurray_core::PAGE_ALIGNMENT;
101///
102/// assert_eq!(PAGE_ALIGNMENT, 4096);
103/// ```
104pub const PAGE_ALIGNMENT: u32 = 4096;
105
106// ── SyncMode ──────────────────────────────────────────────────────────────────
107
108/// Describes how the producer–consumer memory ordering guarantee is established
109/// for a buffer.
110///
111/// The `sync_mode` field in the binary buffer handle is a single `uint8` at
112/// wire offset 13. This enum is the typed representation of that byte; use
113/// [`SyncMode::from_byte`] to parse and [`SyncMode::to_byte`] to serialize.
114///
115/// CPU buffers (`device_tag == 0x00`) MUST use [`SyncMode::ProducerSynced`];
116/// [`BufferHandle::new`] enforces this and returns [`Error::InvalidSyncMode`]
117/// if any other mode is combined with [`DeviceTag::Cpu`].
118///
119/// | Wire value | Variant |
120/// |------------|---------|
121/// | `0x00` | [`ProducerSynced`][SyncMode::ProducerSynced] |
122/// | `0x01` | [`Event`][SyncMode::Event] |
123/// | `0x02` | [`ConsumerStream`][SyncMode::ConsumerStream] |
124/// | `0x03`–`0xFF` | reserved / permanently invalid → [`Error::InvalidSyncMode`] |
125///
126/// See `docs/spec/buffer-protocol.md § Synchronization Mode` and ADR-018.
127///
128/// # Examples
129///
130/// ```
131/// use hurray_core::{SyncMode, Error};
132///
133/// let mode = SyncMode::from_byte(0x00).unwrap();
134/// assert_eq!(mode, SyncMode::ProducerSynced);
135/// assert_eq!(mode.to_byte(), 0x00);
136/// assert_eq!(mode.to_string(), "producer_synced");
137///
138/// let event = SyncMode::from_byte(0x01).unwrap();
139/// assert_eq!(event, SyncMode::Event);
140///
141/// assert!(matches!(SyncMode::from_byte(0x03), Err(Error::InvalidSyncMode(0x03))));
142/// assert!(matches!(SyncMode::from_byte(0xFF), Err(Error::InvalidSyncMode(0xFF))));
143/// ```
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
145#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
146pub enum SyncMode {
147    /// Producer has issued a host-side wait; consumer may access on any stream.
148    ///
149    /// This is the only valid mode for CPU buffers (`device_tag == 0x00`).
150    /// Wire byte `0x00`.
151    ProducerSynced,
152    /// Producer recorded a device event; consumer must wait on it via the C ABI.
153    ///
154    /// Wire byte `0x01`.
155    Event,
156    /// Consumer declared a target stream; producer ordered it device-side.
157    ///
158    /// Wire byte `0x02`.
159    ConsumerStream,
160}
161
162impl SyncMode {
163    /// Parses a [`SyncMode`] from its one-byte wire representation.
164    ///
165    /// # Errors
166    ///
167    /// - [`Error::InvalidSyncMode`] — byte is `0x03`–`0xFF` (reserved or
168    ///   permanently invalid).
169    ///
170    /// # Examples
171    ///
172    /// ```
173    /// use hurray_core::{SyncMode, Error};
174    ///
175    /// assert_eq!(SyncMode::from_byte(0x00).unwrap(), SyncMode::ProducerSynced);
176    /// assert_eq!(SyncMode::from_byte(0x01).unwrap(), SyncMode::Event);
177    /// assert_eq!(SyncMode::from_byte(0x02).unwrap(), SyncMode::ConsumerStream);
178    /// assert!(matches!(SyncMode::from_byte(0x03), Err(Error::InvalidSyncMode(0x03))));
179    /// assert!(matches!(SyncMode::from_byte(0xFE), Err(Error::InvalidSyncMode(0xFE))));
180    /// assert!(matches!(SyncMode::from_byte(0xFF), Err(Error::InvalidSyncMode(0xFF))));
181    /// ```
182    pub fn from_byte(b: u8) -> crate::Result<Self> {
183        match b {
184            0x00 => Ok(Self::ProducerSynced),
185            0x01 => Ok(Self::Event),
186            0x02 => Ok(Self::ConsumerStream),
187            // 0x03–0xFF: all reserved or permanently invalid; reject unconditionally.
188            _ => Err(Error::InvalidSyncMode(b)),
189        }
190    }
191
192    /// Returns the one-byte wire representation of this sync mode.
193    ///
194    /// # Examples
195    ///
196    /// ```
197    /// use hurray_core::SyncMode;
198    ///
199    /// assert_eq!(SyncMode::ProducerSynced.to_byte(), 0x00);
200    /// assert_eq!(SyncMode::Event.to_byte(), 0x01);
201    /// assert_eq!(SyncMode::ConsumerStream.to_byte(), 0x02);
202    /// ```
203    pub fn to_byte(self) -> u8 {
204        match self {
205            Self::ProducerSynced => 0x00,
206            Self::Event => 0x01,
207            Self::ConsumerStream => 0x02,
208        }
209    }
210}
211
212impl fmt::Display for SyncMode {
213    /// Formats the sync mode as a human-readable lowercase string.
214    ///
215    /// # Examples
216    ///
217    /// ```
218    /// use hurray_core::SyncMode;
219    ///
220    /// assert_eq!(SyncMode::ProducerSynced.to_string(), "producer_synced");
221    /// assert_eq!(SyncMode::Event.to_string(), "event");
222    /// assert_eq!(SyncMode::ConsumerStream.to_string(), "consumer_stream");
223    /// ```
224    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
225        match self {
226            Self::ProducerSynced => f.write_str("producer_synced"),
227            Self::Event => f.write_str("event"),
228            Self::ConsumerStream => f.write_str("consumer_stream"),
229        }
230    }
231}
232
233// ── DeviceTag ─────────────────────────────────────────────────────────────────
234
235/// Identifies the memory space in which a buffer resides.
236///
237/// The `device_tag` field in the binary buffer handle is a single `uint8`.
238/// This enum is the typed representation of that byte; use [`DeviceTag::from_byte`]
239/// to parse and [`DeviceTag::to_byte`] to serialize.
240///
241/// | Wire value | Variant |
242/// |------------|---------|
243/// | `0x00` | [`Cpu`][DeviceTag::Cpu] |
244/// | `0x01` | [`Cuda`][DeviceTag::Cuda] |
245/// | `0x02` | [`Rocm`][DeviceTag::Rocm] |
246/// | `0x03` | [`Metal`][DeviceTag::Metal] |
247/// | `0x04` | [`Vulkan`][DeviceTag::Vulkan] |
248/// | `0x05` | [`WebGpu`][DeviceTag::WebGpu] |
249/// | `0x06` | [`Hexagon`][DeviceTag::Hexagon] |
250/// | `0x07` | [`LevelZero`][DeviceTag::LevelZero] |
251/// | `0x08` | [`OpenCl`][DeviceTag::OpenCl] |
252/// | `0x09`–`0xEF` | reserved — yields [`Error::ReservedDeviceTag`] |
253/// | `0xF0`–`0xFE` | [`Private(b)`][DeviceTag::Private] |
254/// | `0xFF` | permanently invalid — yields [`Error::InvalidDeviceTag`] |
255///
256/// See `docs/spec/buffer-protocol.md § Device Tags` for the normative table.
257///
258/// # Examples
259///
260/// ```
261/// use hurray_core::DeviceTag;
262///
263/// let tag = DeviceTag::from_byte(0x00).unwrap();
264/// assert_eq!(tag, DeviceTag::Cpu);
265/// assert_eq!(tag.to_byte(), 0x00);
266/// assert_eq!(tag.to_string(), "cpu");
267///
268/// let private = DeviceTag::from_byte(0xF2).unwrap();
269/// assert!(private.is_private());
270/// assert_eq!(private.to_byte(), 0xF2);
271/// ```
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
273#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
274pub enum DeviceTag {
275    /// CPU host memory. Wire byte `0x00`.
276    Cpu,
277    /// CUDA device memory. Wire byte `0x01`.
278    Cuda,
279    /// ROCm device memory. Wire byte `0x02`.
280    Rocm,
281    /// Metal device memory (Apple Silicon unified memory). Wire byte `0x03`.
282    Metal,
283    /// Vulkan device memory (cross-vendor GPU API). Wire byte `0x04`.
284    ///
285    /// # Examples
286    ///
287    /// ```
288    /// use hurray_core::DeviceTag;
289    ///
290    /// let tag = DeviceTag::from_byte(0x04).unwrap();
291    /// assert_eq!(tag, DeviceTag::Vulkan);
292    /// assert_eq!(tag.to_byte(), 0x04);
293    /// assert_eq!(tag.to_string(), "vulkan");
294    /// ```
295    Vulkan,
296    /// WebGPU device memory (browser and native WebGPU API). Wire byte `0x05`.
297    ///
298    /// # Examples
299    ///
300    /// ```
301    /// use hurray_core::DeviceTag;
302    ///
303    /// let tag = DeviceTag::from_byte(0x05).unwrap();
304    /// assert_eq!(tag, DeviceTag::WebGpu);
305    /// assert_eq!(tag.to_byte(), 0x05);
306    /// assert_eq!(tag.to_string(), "webgpu");
307    /// ```
308    WebGpu,
309    /// Qualcomm Hexagon DSP memory. Wire byte `0x06`.
310    ///
311    /// # Examples
312    ///
313    /// ```
314    /// use hurray_core::DeviceTag;
315    ///
316    /// let tag = DeviceTag::from_byte(0x06).unwrap();
317    /// assert_eq!(tag, DeviceTag::Hexagon);
318    /// assert_eq!(tag.to_byte(), 0x06);
319    /// assert_eq!(tag.to_string(), "hexagon");
320    /// ```
321    Hexagon,
322    /// Intel oneAPI Level Zero device memory. Wire byte `0x07`.
323    ///
324    /// # Examples
325    ///
326    /// ```
327    /// use hurray_core::DeviceTag;
328    ///
329    /// let tag = DeviceTag::from_byte(0x07).unwrap();
330    /// assert_eq!(tag, DeviceTag::LevelZero);
331    /// assert_eq!(tag.to_byte(), 0x07);
332    /// assert_eq!(tag.to_string(), "level_zero");
333    /// ```
334    LevelZero,
335    /// OpenCL device memory (cross-vendor compute API). Wire byte `0x08`.
336    ///
337    /// # Examples
338    ///
339    /// ```
340    /// use hurray_core::DeviceTag;
341    ///
342    /// let tag = DeviceTag::from_byte(0x08).unwrap();
343    /// assert_eq!(tag, DeviceTag::OpenCl);
344    /// assert_eq!(tag.to_byte(), 0x08);
345    /// assert_eq!(tag.to_string(), "opencl");
346    /// ```
347    OpenCl,
348    /// Implementation-private device type. Wire byte in `0xF0`–`0xFE`.
349    ///
350    /// Descriptors carrying private device tags MUST NOT be exchanged between
351    /// independent implementations unless both parties have agreed on the
352    /// semantics out of band.
353    ///
354    /// Use [`DeviceTag::from_byte`] to construct; direct construction of the
355    /// inner [`PrivateTag`] from outside the crate is not possible.
356    Private(PrivateTag),
357}
358
359impl DeviceTag {
360    /// Parses a [`DeviceTag`] from its one-byte wire representation.
361    ///
362    /// # Errors
363    ///
364    /// - [`Error::InvalidDeviceTag`] — byte is `0xFF` (permanently reserved).
365    /// - [`Error::ReservedDeviceTag`] — byte is in `0x09`–`0xEF` (reserved for
366    ///   future specification versions).
367    ///
368    /// # Examples
369    ///
370    /// ```
371    /// use hurray_core::{DeviceTag, Error};
372    ///
373    /// assert_eq!(DeviceTag::from_byte(0x00).unwrap(), DeviceTag::Cpu);
374    /// assert_eq!(DeviceTag::from_byte(0x01).unwrap(), DeviceTag::Cuda);
375    /// assert_eq!(DeviceTag::from_byte(0x02).unwrap(), DeviceTag::Rocm);
376    /// assert_eq!(DeviceTag::from_byte(0x03).unwrap(), DeviceTag::Metal);
377    /// assert_eq!(DeviceTag::from_byte(0x04).unwrap(), DeviceTag::Vulkan);
378    /// assert_eq!(DeviceTag::from_byte(0x05).unwrap(), DeviceTag::WebGpu);
379    /// assert_eq!(DeviceTag::from_byte(0x06).unwrap(), DeviceTag::Hexagon);
380    /// assert_eq!(DeviceTag::from_byte(0x07).unwrap(), DeviceTag::LevelZero);
381    /// assert_eq!(DeviceTag::from_byte(0x08).unwrap(), DeviceTag::OpenCl);
382    /// assert!(DeviceTag::from_byte(0xF0).unwrap().is_private());
383    /// assert_eq!(DeviceTag::from_byte(0xF0).unwrap().to_byte(), 0xF0);
384    /// assert_eq!(DeviceTag::from_byte(0xFE).unwrap().to_byte(), 0xFE);
385    /// assert!(matches!(DeviceTag::from_byte(0x09), Err(Error::ReservedDeviceTag(0x09))));
386    /// assert!(matches!(DeviceTag::from_byte(0xEF), Err(Error::ReservedDeviceTag(0xEF))));
387    /// assert!(matches!(DeviceTag::from_byte(0xFF), Err(Error::InvalidDeviceTag(0xFF))));
388    /// ```
389    pub fn from_byte(b: u8) -> crate::Result<Self> {
390        match b {
391            0x00 => Ok(Self::Cpu),
392            0x01 => Ok(Self::Cuda),
393            0x02 => Ok(Self::Rocm),
394            0x03 => Ok(Self::Metal),
395            0x04 => Ok(Self::Vulkan),
396            0x05 => Ok(Self::WebGpu),
397            0x06 => Ok(Self::Hexagon),
398            0x07 => Ok(Self::LevelZero),
399            0x08 => Ok(Self::OpenCl),
400            0x09..=0xEF => Err(Error::ReservedDeviceTag(b)),
401            0xF0..=0xFE => Ok(Self::Private(PrivateTag(b))),
402            0xFF => Err(Error::InvalidDeviceTag(b)),
403        }
404    }
405
406    /// Returns the one-byte wire representation of this device tag.
407    ///
408    /// # Examples
409    ///
410    /// ```
411    /// use hurray_core::DeviceTag;
412    ///
413    /// assert_eq!(DeviceTag::Cpu.to_byte(), 0x00);
414    /// assert_eq!(DeviceTag::Cuda.to_byte(), 0x01);
415    /// assert_eq!(DeviceTag::Rocm.to_byte(), 0x02);
416    /// assert_eq!(DeviceTag::Metal.to_byte(), 0x03);
417    /// assert_eq!(DeviceTag::Vulkan.to_byte(), 0x04);
418    /// assert_eq!(DeviceTag::WebGpu.to_byte(), 0x05);
419    /// assert_eq!(DeviceTag::Hexagon.to_byte(), 0x06);
420    /// assert_eq!(DeviceTag::LevelZero.to_byte(), 0x07);
421    /// assert_eq!(DeviceTag::OpenCl.to_byte(), 0x08);
422    /// assert_eq!(DeviceTag::from_byte(0xF5).unwrap().to_byte(), 0xF5);
423    /// ```
424    pub fn to_byte(self) -> u8 {
425        match self {
426            Self::Cpu => 0x00,
427            Self::Cuda => 0x01,
428            Self::Rocm => 0x02,
429            Self::Metal => 0x03,
430            Self::Vulkan => 0x04,
431            Self::WebGpu => 0x05,
432            Self::Hexagon => 0x06,
433            Self::LevelZero => 0x07,
434            Self::OpenCl => 0x08,
435            Self::Private(t) => t.0,
436        }
437    }
438
439    /// Returns `true` if this tag is a private/experimental device type
440    /// (`0xF0`–`0xFE`).
441    ///
442    /// Private tags MAY be used by implementations for experimental device
443    /// types but MUST NOT be exchanged between independent implementations
444    /// without an out-of-band agreement.
445    ///
446    /// # Examples
447    ///
448    /// ```
449    /// use hurray_core::DeviceTag;
450    ///
451    /// assert!(DeviceTag::from_byte(0xF0).unwrap().is_private());
452    /// assert!(!DeviceTag::Cpu.is_private());
453    /// assert!(!DeviceTag::Cuda.is_private());
454    /// ```
455    pub fn is_private(self) -> bool {
456        matches!(self, Self::Private(_))
457    }
458}
459
460impl fmt::Display for DeviceTag {
461    /// Formats the device tag as a human-readable lowercase string.
462    ///
463    /// Private tags are formatted as `private(0xNN)` where `NN` is the
464    /// hex wire byte.
465    ///
466    /// # Examples
467    ///
468    /// ```
469    /// use hurray_core::DeviceTag;
470    ///
471    /// assert_eq!(DeviceTag::Cpu.to_string(), "cpu");
472    /// assert_eq!(DeviceTag::Cuda.to_string(), "cuda");
473    /// assert_eq!(DeviceTag::Rocm.to_string(), "rocm");
474    /// assert_eq!(DeviceTag::Metal.to_string(), "metal");
475    /// assert_eq!(DeviceTag::Vulkan.to_string(), "vulkan");
476    /// assert_eq!(DeviceTag::WebGpu.to_string(), "webgpu");
477    /// assert_eq!(DeviceTag::Hexagon.to_string(), "hexagon");
478    /// assert_eq!(DeviceTag::LevelZero.to_string(), "level_zero");
479    /// assert_eq!(DeviceTag::OpenCl.to_string(), "opencl");
480    /// assert_eq!(DeviceTag::from_byte(0xF3).unwrap().to_string(), "private(0xF3)");
481    /// ```
482    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
483        match self {
484            Self::Cpu => f.write_str("cpu"),
485            Self::Cuda => f.write_str("cuda"),
486            Self::Rocm => f.write_str("rocm"),
487            Self::Metal => f.write_str("metal"),
488            Self::Vulkan => f.write_str("vulkan"),
489            Self::WebGpu => f.write_str("webgpu"),
490            Self::Hexagon => f.write_str("hexagon"),
491            Self::LevelZero => f.write_str("level_zero"),
492            Self::OpenCl => f.write_str("opencl"),
493            Self::Private(t) => write!(f, "private(0x{:02X})", t.0),
494        }
495    }
496}
497
498// ── MemoryClass ───────────────────────────────────────────────────────────────
499
500/// An implementation-private memory class byte, guaranteed to be in `0xF0`–`0xFE`.
501///
502/// Constructible only via [`MemoryClass::from_byte`].
503#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
504#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
505pub struct PrivateMemoryClass(u8);
506
507impl PrivateMemoryClass {
508    /// Returns the raw wire byte for this private memory class.
509    ///
510    /// # Examples
511    ///
512    /// ```
513    /// use hurray_core::MemoryClass;
514    ///
515    /// let cls = MemoryClass::from_byte(0xF1).unwrap();
516    /// if let MemoryClass::Private(p) = cls {
517    ///     assert_eq!(p.to_byte(), 0xF1);
518    /// }
519    /// ```
520    pub fn to_byte(self) -> u8 {
521        self.0
522    }
523}
524
525/// The memory access class of a buffer handle.
526///
527/// Identifies *how* a buffer is accessible — whether it is device-exclusive or
528/// can be accessed by the CPU and/or peer accelerators without copying.
529///
530/// The `memory_class` field in the binary buffer handle is a single `uint8` at
531/// offset 14. Use [`MemoryClass::from_byte`] to parse and [`MemoryClass::to_byte`]
532/// to serialize. See `docs/spec/buffer-protocol.md § Memory Class` for the
533/// normative definition.
534///
535/// | Value | Variant |
536/// |-------|---------|
537/// | `0x00` | [`Standard`][MemoryClass::Standard] |
538/// | `0x01` | [`HostPinned`][MemoryClass::HostPinned] |
539/// | `0x02` | [`Unified`][MemoryClass::Unified] |
540/// | `0x03` | [`Peer`][MemoryClass::Peer] |
541/// | `0x04`–`0xEF` | reserved → [`Error::ReservedMemoryClass`] |
542/// | `0xF0`–`0xFE` | [`Private(b)`][MemoryClass::Private] |
543/// | `0xFF` | permanently invalid → [`Error::InvalidMemoryClass`] |
544///
545/// # Examples
546///
547/// ```
548/// use hurray_core::{MemoryClass, Error};
549///
550/// assert_eq!(MemoryClass::from_byte(0x00).unwrap(), MemoryClass::Standard);
551/// assert_eq!(MemoryClass::from_byte(0x02).unwrap(), MemoryClass::Unified);
552/// assert!(matches!(MemoryClass::from_byte(0x04), Err(Error::ReservedMemoryClass(0x04))));
553/// assert!(matches!(MemoryClass::from_byte(0xFF), Err(Error::InvalidMemoryClass(0xFF))));
554/// ```
555#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
556#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
557pub enum MemoryClass {
558    /// Device-exclusive memory. Only the primary compute unit of the tagged device
559    /// can access this buffer without a copy. Default for all device types; backward-
560    /// compatible with pre-ADR-020 descriptors whose `_reserved[0]` byte was `0x00`.
561    Standard,
562    /// CPU-accessible, device-mapped. No hardware-managed coherency. Examples:
563    /// `cudaMallocHost`, `hipHostMalloc`, `CL_MEM_ALLOC_HOST_PTR`.
564    HostPinned,
565    /// Hardware-managed unified or coherent memory. CPU and device can access this
566    /// buffer simultaneously; the hardware ensures coherency. Examples:
567    /// `cudaMallocManaged`, ROCm HMM, Metal `MTLStorageModeShared`.
568    Unified,
569    /// Peer-to-peer device memory. Accessible by a set of peer accelerators agreed
570    /// out of band (NVLink, xGMI, PCIe BAR). Not CPU-accessible without a copy.
571    Peer,
572    /// Implementation-private memory class (`0xF0`–`0xFE`). Semantics agreed out of band.
573    Private(PrivateMemoryClass),
574}
575
576impl MemoryClass {
577    /// Parses a [`MemoryClass`] from its one-byte wire representation.
578    ///
579    /// # Errors
580    ///
581    /// - [`Error::ReservedMemoryClass`] — byte is `0x04`–`0xEF`.
582    /// - [`Error::InvalidMemoryClass`] — byte is `0xFF`.
583    ///
584    /// # Examples
585    ///
586    /// ```
587    /// use hurray_core::{MemoryClass, Error};
588    ///
589    /// assert_eq!(MemoryClass::from_byte(0x00).unwrap(), MemoryClass::Standard);
590    /// assert_eq!(MemoryClass::from_byte(0x01).unwrap(), MemoryClass::HostPinned);
591    /// assert_eq!(MemoryClass::from_byte(0x02).unwrap(), MemoryClass::Unified);
592    /// assert_eq!(MemoryClass::from_byte(0x03).unwrap(), MemoryClass::Peer);
593    /// assert!(matches!(MemoryClass::from_byte(0x04), Err(Error::ReservedMemoryClass(0x04))));
594    /// assert!(matches!(MemoryClass::from_byte(0xEF), Err(Error::ReservedMemoryClass(0xEF))));
595    /// assert!(matches!(MemoryClass::from_byte(0xFF), Err(Error::InvalidMemoryClass(0xFF))));
596    /// ```
597    pub fn from_byte(b: u8) -> crate::Result<Self> {
598        match b {
599            0x00 => Ok(Self::Standard),
600            0x01 => Ok(Self::HostPinned),
601            0x02 => Ok(Self::Unified),
602            0x03 => Ok(Self::Peer),
603            0x04..=0xEF => Err(Error::ReservedMemoryClass(b)),
604            0xF0..=0xFE => Ok(Self::Private(PrivateMemoryClass(b))),
605            0xFF => Err(Error::InvalidMemoryClass(b)),
606        }
607    }
608
609    /// Returns the one-byte wire representation of this memory class.
610    ///
611    /// # Examples
612    ///
613    /// ```
614    /// use hurray_core::MemoryClass;
615    ///
616    /// assert_eq!(MemoryClass::Standard.to_byte(), 0x00);
617    /// assert_eq!(MemoryClass::HostPinned.to_byte(), 0x01);
618    /// assert_eq!(MemoryClass::Unified.to_byte(), 0x02);
619    /// assert_eq!(MemoryClass::Peer.to_byte(), 0x03);
620    /// ```
621    pub fn to_byte(self) -> u8 {
622        match self {
623            Self::Standard => 0x00,
624            Self::HostPinned => 0x01,
625            Self::Unified => 0x02,
626            Self::Peer => 0x03,
627            Self::Private(p) => p.0,
628        }
629    }
630
631    /// Returns `true` if this is an implementation-private memory class (`0xF0`–`0xFE`).
632    ///
633    /// # Examples
634    ///
635    /// ```
636    /// use hurray_core::MemoryClass;
637    ///
638    /// assert!(!MemoryClass::Standard.is_private());
639    /// assert!(MemoryClass::from_byte(0xF0).unwrap().is_private());
640    /// ```
641    pub fn is_private(self) -> bool {
642        matches!(self, Self::Private(_))
643    }
644}
645
646impl fmt::Display for MemoryClass {
647    /// # Examples
648    ///
649    /// ```
650    /// use hurray_core::MemoryClass;
651    ///
652    /// assert_eq!(MemoryClass::Standard.to_string(), "standard");
653    /// assert_eq!(MemoryClass::Unified.to_string(), "unified");
654    /// ```
655    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
656        match self {
657            Self::Standard => f.write_str("standard"),
658            Self::HostPinned => f.write_str("host_pinned"),
659            Self::Unified => f.write_str("unified"),
660            Self::Peer => f.write_str("peer"),
661            Self::Private(p) => write!(f, "private(0x{:02X})", p.0),
662        }
663    }
664}
665
666// ── BufferHandle ──────────────────────────────────────────────────────────────
667
668/// A declaration of a buffer's size, alignment, device location, and sync mode.
669///
670/// A `BufferHandle` is the in-memory representation of the 16-byte buffer
671/// handle entry in the tensor descriptor's buffer table (see
672/// `docs/spec/metadata.md § Buffer Table`). It carries the metadata needed to
673/// locate and access a buffer but does not itself hold a pointer — the actual
674/// memory address is communicated out-of-band via the interchange protocol or
675/// the C ABI (see `docs/impl/c-ffi.md`).
676///
677/// ## Wire layout (ADR-018 § 3, ADR-020)
678///
679/// | Offset | Field | Type | Size |
680/// |--------|-------|------|------|
681/// | 0 | `byte_size` | uint64 LE | 8 |
682/// | 8 | `alignment` | uint32 LE | 4 |
683/// | 12 | `device_tag` | uint8 | 1 |
684/// | 13 | `sync_mode` | uint8 | 1 |
685/// | 14 | `memory_class` | uint8 | 1 |
686/// | 15 | `_reserved` | uint8 | 1 |
687///
688/// # Alignment rules
689///
690/// - `alignment` MUST be a power of two.
691/// - For **non-empty** buffers (`byte_size > 0`): `alignment` MUST be at least
692///   [`MIN_BUFFER_ALIGNMENT`] (64 bytes).
693/// - For **empty** buffers (`byte_size == 0`): any power-of-two alignment is
694///   valid, including `1`. A reader MUST NOT dereference the pointer of an empty
695///   buffer.
696///
697/// See `docs/spec/buffer-protocol.md § Alignment` for the normative rules.
698///
699/// # Examples
700///
701/// ```
702/// use hurray_core::{BufferHandle, DeviceTag, MemoryClass, SyncMode, MIN_BUFFER_ALIGNMENT};
703///
704/// // Non-empty CPU buffer, minimum SIMD alignment, default Standard class.
705/// let handle = BufferHandle::new(1024, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
706/// assert_eq!(handle.byte_size(), 1024);
707/// assert_eq!(handle.alignment(), 64);
708/// assert_eq!(handle.device_tag(), DeviceTag::Cpu);
709/// assert_eq!(handle.sync_mode(), SyncMode::ProducerSynced);
710/// assert_eq!(handle.memory_class(), MemoryClass::Standard);
711/// assert!(!handle.is_empty());
712///
713/// // CUDA buffer with Unified memory class.
714/// let unified = BufferHandle::with_memory_class(
715///     4096, 4096, DeviceTag::Cuda, SyncMode::ProducerSynced, MemoryClass::Unified,
716/// ).unwrap();
717/// assert_eq!(unified.memory_class(), MemoryClass::Unified);
718///
719/// // Empty buffer — alignment 1 is valid.
720/// let empty = BufferHandle::empty(DeviceTag::Cuda);
721/// assert!(empty.is_empty());
722/// assert_eq!(empty.alignment(), 1);
723/// ```
724#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
725#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
726pub struct BufferHandle {
727    byte_size: u64,
728    alignment: u32,
729    device_tag: DeviceTag,
730    sync_mode: SyncMode,
731    memory_class: MemoryClass,
732}
733
734impl BufferHandle {
735    /// Creates a new [`BufferHandle`] with the given size, alignment, device, and sync mode.
736    ///
737    /// # Errors
738    ///
739    /// - [`Error::AlignmentNotPowerOfTwo`] — `alignment` is not a power of two.
740    /// - [`Error::AlignmentBelowMinimum`] — `byte_size > 0` and `alignment` is
741    ///   less than [`MIN_BUFFER_ALIGNMENT`] (64).
742    /// - [`Error::InvalidSyncMode`] — `device_tag` is [`DeviceTag::Cpu`] and
743    ///   `sync_mode` is not [`SyncMode::ProducerSynced`] (CPU buffers MUST use
744    ///   `SYNC_PRODUCER_SYNCED` per the spec).
745    ///
746    /// # Examples
747    ///
748    /// ```
749    /// use hurray_core::{BufferHandle, DeviceTag, Error, SyncMode, MIN_BUFFER_ALIGNMENT};
750    ///
751    /// // Valid: non-empty CPU buffer with minimum SIMD alignment.
752    /// assert!(BufferHandle::new(512, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).is_ok());
753    ///
754    /// // Valid: CUDA buffer with Event sync.
755    /// assert!(BufferHandle::new(512, 64, DeviceTag::Cuda, SyncMode::Event).is_ok());
756    ///
757    /// // Valid: empty buffer with alignment 1.
758    /// assert!(BufferHandle::new(0, 1, DeviceTag::Cpu, SyncMode::ProducerSynced).is_ok());
759    ///
760    /// // Error: alignment is not a power of two.
761    /// assert!(matches!(
762    ///     BufferHandle::new(512, 3, DeviceTag::Cpu, SyncMode::ProducerSynced),
763    ///     Err(Error::AlignmentNotPowerOfTwo { alignment: 3 })
764    /// ));
765    ///
766    /// // Error: non-empty buffer with alignment below the 64-byte minimum.
767    /// assert!(matches!(
768    ///     BufferHandle::new(512, 32, DeviceTag::Cpu, SyncMode::ProducerSynced),
769    ///     Err(Error::AlignmentBelowMinimum { alignment: 32, minimum: 64 })
770    /// ));
771    ///
772    /// // Error: CPU buffer with non-ProducerSynced mode.
773    /// assert!(matches!(
774    ///     BufferHandle::new(512, 64, DeviceTag::Cpu, SyncMode::Event),
775    ///     Err(Error::InvalidSyncMode(0x01))
776    /// ));
777    /// ```
778    pub fn new(
779        byte_size: u64,
780        alignment: u32,
781        device_tag: DeviceTag,
782        sync_mode: SyncMode,
783    ) -> crate::Result<Self> {
784        Self::with_memory_class(
785            byte_size,
786            alignment,
787            device_tag,
788            sync_mode,
789            MemoryClass::Standard,
790        )
791    }
792
793    /// Creates a new [`BufferHandle`] with an explicit memory class.
794    ///
795    /// Identical to [`BufferHandle::new`] but accepts a [`MemoryClass`] value.
796    /// Use this constructor when the buffer's memory class is not [`MemoryClass::Standard`]
797    /// (e.g., CUDA managed memory, Metal shared storage, or peer-to-peer memory).
798    ///
799    /// # Errors
800    ///
801    /// - [`Error::AlignmentNotPowerOfTwo`] — `alignment` is not a power of two.
802    /// - [`Error::AlignmentBelowMinimum`] — `byte_size > 0` and `alignment < 64`.
803    /// - [`Error::InvalidSyncMode`] — `device_tag` is [`DeviceTag::Cpu`] and
804    ///   `sync_mode` is not [`SyncMode::ProducerSynced`].
805    ///
806    /// # Examples
807    ///
808    /// ```
809    /// use hurray_core::{BufferHandle, DeviceTag, MemoryClass, SyncMode};
810    ///
811    /// // CUDA unified (managed) memory buffer.
812    /// let handle = BufferHandle::with_memory_class(
813    ///     4096, 4096, DeviceTag::Cuda, SyncMode::ProducerSynced, MemoryClass::Unified,
814    /// ).unwrap();
815    /// assert_eq!(handle.memory_class(), MemoryClass::Unified);
816    ///
817    /// // CPU host-pinned buffer.
818    /// let pinned = BufferHandle::with_memory_class(
819    ///     512, 64, DeviceTag::Cpu, SyncMode::ProducerSynced, MemoryClass::HostPinned,
820    /// ).unwrap();
821    /// assert_eq!(pinned.memory_class(), MemoryClass::HostPinned);
822    /// ```
823    pub fn with_memory_class(
824        byte_size: u64,
825        alignment: u32,
826        device_tag: DeviceTag,
827        sync_mode: SyncMode,
828        memory_class: MemoryClass,
829    ) -> crate::Result<Self> {
830        if !alignment.is_power_of_two() {
831            return Err(Error::AlignmentNotPowerOfTwo { alignment });
832        }
833        if byte_size > 0 && alignment < MIN_BUFFER_ALIGNMENT {
834            return Err(Error::AlignmentBelowMinimum {
835                alignment,
836                minimum: MIN_BUFFER_ALIGNMENT,
837            });
838        }
839        // CPU buffers must use ProducerSynced: no device-side sync primitives exist.
840        if device_tag == DeviceTag::Cpu && sync_mode != SyncMode::ProducerSynced {
841            return Err(Error::InvalidSyncMode(sync_mode.to_byte()));
842        }
843        Ok(Self {
844            byte_size,
845            alignment,
846            device_tag,
847            sync_mode,
848            memory_class,
849        })
850    }
851
852    /// Creates an **empty** [`BufferHandle`] (zero bytes) on the given device.
853    ///
854    /// The alignment is set to `1` — the minimum valid power-of-two for an
855    /// empty buffer — and `sync_mode` is always [`SyncMode::ProducerSynced`].
856    /// This constructor is infallible.
857    ///
858    /// # Examples
859    ///
860    /// ```
861    /// use hurray_core::{BufferHandle, DeviceTag, SyncMode};
862    ///
863    /// let handle = BufferHandle::empty(DeviceTag::Cpu);
864    /// assert!(handle.is_empty());
865    /// assert_eq!(handle.byte_size(), 0);
866    /// assert_eq!(handle.alignment(), 1);
867    /// assert_eq!(handle.device_tag(), DeviceTag::Cpu);
868    /// assert_eq!(handle.sync_mode(), SyncMode::ProducerSynced);
869    /// ```
870    pub fn empty(device_tag: DeviceTag) -> Self {
871        // ProducerSynced is the safest universal default: empty buffers carry no
872        // data and no device-side synchronisation is required.
873        Self {
874            byte_size: 0,
875            alignment: 1,
876            device_tag,
877            sync_mode: SyncMode::ProducerSynced,
878            memory_class: MemoryClass::Standard,
879        }
880    }
881
882    /// Returns the size of the buffer in bytes.
883    ///
884    /// A value of `0` denotes an empty buffer whose backing pointer MUST NOT
885    /// be dereferenced.
886    ///
887    /// # Examples
888    ///
889    /// ```
890    /// use hurray_core::{BufferHandle, DeviceTag, SyncMode};
891    ///
892    /// let handle = BufferHandle::new(4096, 4096, DeviceTag::Cuda, SyncMode::Event).unwrap();
893    /// assert_eq!(handle.byte_size(), 4096);
894    /// ```
895    pub fn byte_size(self) -> u64 {
896        self.byte_size
897    }
898
899    /// Returns the minimum alignment of the buffer's base address in bytes.
900    ///
901    /// Always a power of two. For non-empty buffers, always at least
902    /// [`MIN_BUFFER_ALIGNMENT`].
903    ///
904    /// # Examples
905    ///
906    /// ```
907    /// use hurray_core::{BufferHandle, DeviceTag, SyncMode, PAGE_ALIGNMENT};
908    ///
909    /// let handle = BufferHandle::new(8192, PAGE_ALIGNMENT, DeviceTag::Cuda, SyncMode::Event).unwrap();
910    /// assert_eq!(handle.alignment(), 4096);
911    /// ```
912    pub fn alignment(self) -> u32 {
913        self.alignment
914    }
915
916    /// Returns the [`DeviceTag`] identifying the memory space this buffer resides in.
917    ///
918    /// # Examples
919    ///
920    /// ```
921    /// use hurray_core::{BufferHandle, DeviceTag, SyncMode};
922    ///
923    /// let handle = BufferHandle::new(256, 64, DeviceTag::Metal, SyncMode::Event).unwrap();
924    /// assert_eq!(handle.device_tag(), DeviceTag::Metal);
925    /// ```
926    pub fn device_tag(self) -> DeviceTag {
927        self.device_tag
928    }
929
930    /// Returns the [`SyncMode`] describing the producer–consumer ordering guarantee
931    /// for this buffer.
932    ///
933    /// # Examples
934    ///
935    /// ```
936    /// use hurray_core::{BufferHandle, DeviceTag, SyncMode};
937    ///
938    /// let handle = BufferHandle::new(1024, 64, DeviceTag::Cuda, SyncMode::ConsumerStream).unwrap();
939    /// assert_eq!(handle.sync_mode(), SyncMode::ConsumerStream);
940    ///
941    /// // CPU buffers are always ProducerSynced.
942    /// let cpu = BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
943    /// assert_eq!(cpu.sync_mode(), SyncMode::ProducerSynced);
944    /// ```
945    pub fn sync_mode(self) -> SyncMode {
946        self.sync_mode
947    }
948
949    /// Returns the [`MemoryClass`] describing how this buffer is accessible.
950    ///
951    /// # Examples
952    ///
953    /// ```
954    /// use hurray_core::{BufferHandle, DeviceTag, MemoryClass, SyncMode};
955    ///
956    /// // new() defaults to Standard.
957    /// let handle = BufferHandle::new(1024, 64, DeviceTag::Cuda, SyncMode::ProducerSynced).unwrap();
958    /// assert_eq!(handle.memory_class(), MemoryClass::Standard);
959    ///
960    /// // with_memory_class() sets an explicit class.
961    /// let unified = BufferHandle::with_memory_class(
962    ///     1024, 64, DeviceTag::Cuda, SyncMode::ProducerSynced, MemoryClass::Unified,
963    /// ).unwrap();
964    /// assert_eq!(unified.memory_class(), MemoryClass::Unified);
965    /// ```
966    pub fn memory_class(self) -> MemoryClass {
967        self.memory_class
968    }
969
970    /// Returns `true` if this buffer has zero bytes (`byte_size == 0`).
971    ///
972    /// Readers MUST NOT dereference the pointer of an empty buffer. In C ABI
973    /// contexts an empty buffer MAY be represented by a null pointer.
974    ///
975    /// # Examples
976    ///
977    /// ```
978    /// use hurray_core::{BufferHandle, DeviceTag, SyncMode};
979    ///
980    /// assert!(BufferHandle::empty(DeviceTag::Cpu).is_empty());
981    /// assert!(!BufferHandle::new(1, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap().is_empty());
982    /// ```
983    pub fn is_empty(self) -> bool {
984        self.byte_size == 0
985    }
986}
987
988// ── validate_colocation ───────────────────────────────────────────────────────
989
990/// Checks that all buffer handles in `handles` share the same [`DeviceTag`] and [`MemoryClass`].
991///
992/// All buffers referenced by a single tensor descriptor — the data buffer plus
993/// all quantization-parameter buffers — MUST share the same `device_tag` AND the
994/// same `memory_class` (see `docs/spec/buffer-protocol.md § Device Colocation`).
995///
996/// Returns the common [`DeviceTag`] on success.
997///
998/// # Errors
999///
1000/// - [`Error::EmptyBufferList`] — `handles` is empty.
1001/// - [`Error::DeviceTagMismatch`] — two or more handles carry different device tags.
1002/// - [`Error::MemoryClassMismatch`] — two or more handles carry different memory classes.
1003///
1004/// # Examples
1005///
1006/// ```
1007/// use hurray_core::{BufferHandle, DeviceTag, Error, MemoryClass, SyncMode, validate_colocation};
1008///
1009/// // All handles on CPU, all Standard — succeeds.
1010/// let handles = [
1011///     BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1012///     BufferHandle::new(256, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1013/// ];
1014/// assert_eq!(validate_colocation(&handles).unwrap(), DeviceTag::Cpu);
1015///
1016/// // Empty slice — error.
1017/// assert!(matches!(validate_colocation(&[]), Err(Error::EmptyBufferList)));
1018///
1019/// // Mixed devices — error.
1020/// let mixed_device = [
1021///     BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1022///     BufferHandle::new(256, 64, DeviceTag::Cuda, SyncMode::ProducerSynced).unwrap(),
1023/// ];
1024/// assert!(matches!(
1025///     validate_colocation(&mixed_device),
1026///     Err(Error::DeviceTagMismatch { expected: 0x00, found: 0x01 })
1027/// ));
1028///
1029/// // Mixed memory classes — error.
1030/// let mixed_class = [
1031///     BufferHandle::new(1024, 64, DeviceTag::Cuda, SyncMode::ProducerSynced).unwrap(),
1032///     BufferHandle::with_memory_class(256, 64, DeviceTag::Cuda, SyncMode::ProducerSynced, MemoryClass::Unified).unwrap(),
1033/// ];
1034/// assert!(matches!(
1035///     validate_colocation(&mixed_class),
1036///     Err(Error::MemoryClassMismatch { expected: 0x00, found: 0x02 })
1037/// ));
1038/// ```
1039pub fn validate_colocation(handles: &[BufferHandle]) -> crate::Result<DeviceTag> {
1040    let first = handles.first().ok_or(Error::EmptyBufferList)?;
1041    let expected_tag = first.device_tag;
1042    let expected_tag_byte = expected_tag.to_byte();
1043    let expected_class = first.memory_class;
1044    let expected_class_byte = expected_class.to_byte();
1045
1046    for handle in handles.iter().skip(1) {
1047        if handle.device_tag != expected_tag {
1048            return Err(Error::DeviceTagMismatch {
1049                expected: expected_tag_byte,
1050                found: handle.device_tag.to_byte(),
1051            });
1052        }
1053        if handle.memory_class != expected_class {
1054            return Err(Error::MemoryClassMismatch {
1055                expected: expected_class_byte,
1056                found: handle.memory_class.to_byte(),
1057            });
1058        }
1059    }
1060
1061    Ok(expected_tag)
1062}
1063
1064// ── Tests ─────────────────────────────────────────────────────────────────────
1065
1066#[cfg(test)]
1067mod tests {
1068    use super::*;
1069
1070    // ── DeviceTag::from_byte — named variants ────────────────────────────────
1071
1072    /// Spec § buffer-protocol.md Device Tags: 0x00 decodes to Cpu.
1073    #[test]
1074    fn from_byte_cpu() {
1075        assert_eq!(DeviceTag::from_byte(0x00).unwrap(), DeviceTag::Cpu);
1076    }
1077
1078    /// Spec § buffer-protocol.md Device Tags: 0x01 decodes to Cuda.
1079    #[test]
1080    fn from_byte_cuda() {
1081        assert_eq!(DeviceTag::from_byte(0x01).unwrap(), DeviceTag::Cuda);
1082    }
1083
1084    /// Spec § buffer-protocol.md Device Tags: 0x02 decodes to Rocm.
1085    #[test]
1086    fn from_byte_rocm() {
1087        assert_eq!(DeviceTag::from_byte(0x02).unwrap(), DeviceTag::Rocm);
1088    }
1089
1090    /// Spec § buffer-protocol.md Device Tags: 0x03 decodes to Metal.
1091    #[test]
1092    fn from_byte_metal() {
1093        assert_eq!(DeviceTag::from_byte(0x03).unwrap(), DeviceTag::Metal);
1094    }
1095
1096    // ── DeviceTag::from_byte — reserved range 0x09–0xEF ─────────────────────
1097
1098    /// Spec § buffer-protocol.md Device Tags: 0x09 (lower bound of reserved range
1099    /// after ADR-016 assigned 0x04–0x08) must return ReservedDeviceTag.
1100    #[test]
1101    fn from_byte_reserved_lower_bound() {
1102        assert!(matches!(
1103            DeviceTag::from_byte(0x09),
1104            Err(Error::ReservedDeviceTag(0x09))
1105        ));
1106    }
1107
1108    /// Spec § buffer-protocol.md Device Tags: 0xEF (upper bound of reserved range)
1109    /// must return ReservedDeviceTag.
1110    #[test]
1111    fn from_byte_reserved_upper_bound() {
1112        assert!(matches!(
1113            DeviceTag::from_byte(0xEF),
1114            Err(Error::ReservedDeviceTag(0xEF))
1115        ));
1116    }
1117
1118    /// Spot-check mid-reserved byte 0x80 returns ReservedDeviceTag.
1119    #[test]
1120    fn from_byte_reserved_mid_range() {
1121        assert!(matches!(
1122            DeviceTag::from_byte(0x80),
1123            Err(Error::ReservedDeviceTag(0x80))
1124        ));
1125    }
1126
1127    // ── DeviceTag::from_byte — private range 0xF0–0xFE ──────────────────────
1128
1129    /// Spec § buffer-protocol.md Device Tags: 0xF0 (lower bound of private range)
1130    /// must decode to Private(0xF0).
1131    #[test]
1132    fn from_byte_private_lower_bound() {
1133        let tag = DeviceTag::from_byte(0xF0).unwrap();
1134        assert!(tag.is_private());
1135        assert_eq!(tag.to_byte(), 0xF0);
1136    }
1137
1138    /// Spec § buffer-protocol.md Device Tags: 0xFE (upper bound of private range)
1139    /// must decode to Private(0xFE).
1140    #[test]
1141    fn from_byte_private_upper_bound() {
1142        let tag = DeviceTag::from_byte(0xFE).unwrap();
1143        assert!(tag.is_private());
1144        assert_eq!(tag.to_byte(), 0xFE);
1145    }
1146
1147    // ── DeviceTag::from_byte — permanently invalid sentinel 0xFF ────────────
1148
1149    /// Spec § buffer-protocol.md Device Tags: 0xFF is permanently reserved and
1150    /// MUST be rejected with InvalidDeviceTag.
1151    #[test]
1152    fn from_byte_invalid_sentinel() {
1153        assert!(matches!(
1154            DeviceTag::from_byte(0xFF),
1155            Err(Error::InvalidDeviceTag(0xFF))
1156        ));
1157    }
1158
1159    // ── DeviceTag::to_byte — round-trip for named variants ──────────────────
1160
1161    /// Each named variant must serialize back to its documented wire byte.
1162    #[test]
1163    fn to_byte_cpu() {
1164        assert_eq!(DeviceTag::Cpu.to_byte(), 0x00);
1165    }
1166
1167    #[test]
1168    fn to_byte_cuda() {
1169        assert_eq!(DeviceTag::Cuda.to_byte(), 0x01);
1170    }
1171
1172    #[test]
1173    fn to_byte_rocm() {
1174        assert_eq!(DeviceTag::Rocm.to_byte(), 0x02);
1175    }
1176
1177    #[test]
1178    fn to_byte_metal() {
1179        assert_eq!(DeviceTag::Metal.to_byte(), 0x03);
1180    }
1181
1182    /// Private(b) must serialize to exactly b.
1183    #[test]
1184    fn to_byte_private() {
1185        assert_eq!(DeviceTag::from_byte(0xF2).unwrap().to_byte(), 0xF2);
1186    }
1187
1188    // ── DeviceTag round-trip: from_byte → to_byte ────────────────────────────
1189
1190    /// For every valid byte (named variants + private range), from_byte then
1191    /// to_byte must be the identity.
1192    #[test]
1193    fn round_trip_named_variants() {
1194        for b in [0x00u8, 0x01, 0x02, 0x03] {
1195            let tag = DeviceTag::from_byte(b).unwrap();
1196            assert_eq!(tag.to_byte(), b, "round-trip failed for byte 0x{b:02X}");
1197        }
1198    }
1199
1200    #[test]
1201    fn round_trip_private_range() {
1202        for b in 0xF0u8..=0xFE {
1203            let tag = DeviceTag::from_byte(b).unwrap();
1204            assert_eq!(tag.to_byte(), b, "round-trip failed for byte 0x{b:02X}");
1205        }
1206    }
1207
1208    // ── DeviceTag::is_private ────────────────────────────────────────────────
1209
1210    #[test]
1211    fn is_private_true_for_private_variant() {
1212        assert!(DeviceTag::from_byte(0xF0).unwrap().is_private());
1213    }
1214
1215    #[test]
1216    fn is_private_false_for_cpu() {
1217        assert!(!DeviceTag::Cpu.is_private());
1218    }
1219
1220    #[test]
1221    fn is_private_false_for_cuda() {
1222        assert!(!DeviceTag::Cuda.is_private());
1223    }
1224
1225    #[test]
1226    fn is_private_false_for_rocm() {
1227        assert!(!DeviceTag::Rocm.is_private());
1228    }
1229
1230    #[test]
1231    fn is_private_false_for_metal() {
1232        assert!(!DeviceTag::Metal.is_private());
1233    }
1234
1235    // ── DeviceTag Display ────────────────────────────────────────────────────
1236
1237    /// Spec § buffer-protocol.md Display: named variants use lowercase ASCII.
1238    #[test]
1239    fn display_cpu() {
1240        assert_eq!(DeviceTag::Cpu.to_string(), "cpu");
1241    }
1242
1243    #[test]
1244    fn display_cuda() {
1245        assert_eq!(DeviceTag::Cuda.to_string(), "cuda");
1246    }
1247
1248    #[test]
1249    fn display_rocm() {
1250        assert_eq!(DeviceTag::Rocm.to_string(), "rocm");
1251    }
1252
1253    #[test]
1254    fn display_metal() {
1255        assert_eq!(DeviceTag::Metal.to_string(), "metal");
1256    }
1257
1258    /// Private tags display as "private(0xNN)" with uppercase hex digits.
1259    #[test]
1260    fn display_private() {
1261        assert_eq!(
1262            DeviceTag::from_byte(0xF1).unwrap().to_string(),
1263            "private(0xF1)"
1264        );
1265    }
1266
1267    #[test]
1268    fn display_private_lower_bound() {
1269        assert_eq!(
1270            DeviceTag::from_byte(0xF0).unwrap().to_string(),
1271            "private(0xF0)"
1272        );
1273    }
1274
1275    #[test]
1276    fn display_private_upper_bound() {
1277        assert_eq!(
1278            DeviceTag::from_byte(0xFE).unwrap().to_string(),
1279            "private(0xFE)"
1280        );
1281    }
1282
1283    // ── MIN_BUFFER_ALIGNMENT and PAGE_ALIGNMENT constants ───────────────────
1284
1285    /// Spec § buffer-protocol.md Minimum Alignment: SIMD minimum is 64 bytes.
1286    #[test]
1287    fn min_buffer_alignment_is_64() {
1288        assert_eq!(MIN_BUFFER_ALIGNMENT, 64);
1289    }
1290
1291    /// Spec § buffer-protocol.md Page Alignment: page-aligned value is 4096 bytes.
1292    #[test]
1293    fn page_alignment_is_4096() {
1294        assert_eq!(PAGE_ALIGNMENT, 4096);
1295    }
1296
1297    /// Both constants must be powers of two (required by the alignment contract).
1298    #[test]
1299    fn min_buffer_alignment_is_power_of_two() {
1300        assert!(MIN_BUFFER_ALIGNMENT.is_power_of_two());
1301    }
1302
1303    #[test]
1304    fn page_alignment_is_power_of_two() {
1305        assert!(PAGE_ALIGNMENT.is_power_of_two());
1306    }
1307
1308    // ── BufferHandle::new — success cases ────────────────────────────────────
1309
1310    /// Spec § buffer-protocol.md Alignment: non-empty buffer with minimum SIMD
1311    /// alignment (64) on CPU is valid.
1312    #[test]
1313    fn new_nonempty_cpu_min_alignment() {
1314        let result = BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced);
1315        assert!(result.is_ok());
1316    }
1317
1318    /// A non-empty CUDA buffer at page alignment is valid.
1319    #[test]
1320    fn new_nonempty_cuda_page_alignment() {
1321        let result = BufferHandle::new(4096, 4096, DeviceTag::Cuda, SyncMode::Event);
1322        assert!(result.is_ok());
1323    }
1324
1325    /// Spec § buffer-protocol.md Alignment: an empty buffer (byte_size == 0)
1326    /// may have any power-of-two alignment, including 1.
1327    #[test]
1328    fn new_empty_alignment_1_is_valid() {
1329        let result = BufferHandle::new(0, 1, DeviceTag::Cpu, SyncMode::ProducerSynced);
1330        assert!(result.is_ok());
1331    }
1332
1333    /// An empty buffer with the SIMD minimum alignment is also valid.
1334    #[test]
1335    fn new_empty_alignment_64_is_valid() {
1336        let result = BufferHandle::new(0, 64, DeviceTag::Cpu, SyncMode::ProducerSynced);
1337        assert!(result.is_ok());
1338    }
1339
1340    // ── BufferHandle::new — failure cases ────────────────────────────────────
1341
1342    /// alignment=0 is not a power of two; must return AlignmentNotPowerOfTwo.
1343    #[test]
1344    fn new_alignment_zero_not_power_of_two() {
1345        assert!(matches!(
1346            BufferHandle::new(1024, 0, DeviceTag::Cpu, SyncMode::ProducerSynced),
1347            Err(Error::AlignmentNotPowerOfTwo { alignment: 0 })
1348        ));
1349    }
1350
1351    /// alignment=63 is not a power of two; must return AlignmentNotPowerOfTwo.
1352    #[test]
1353    fn new_alignment_63_not_power_of_two() {
1354        assert!(matches!(
1355            BufferHandle::new(1024, 63, DeviceTag::Cpu, SyncMode::ProducerSynced),
1356            Err(Error::AlignmentNotPowerOfTwo { alignment: 63 })
1357        ));
1358    }
1359
1360    /// alignment=100 is not a power of two; must return AlignmentNotPowerOfTwo.
1361    #[test]
1362    fn new_alignment_100_not_power_of_two() {
1363        assert!(matches!(
1364            BufferHandle::new(1024, 100, DeviceTag::Cpu, SyncMode::ProducerSynced),
1365            Err(Error::AlignmentNotPowerOfTwo { alignment: 100 })
1366        ));
1367    }
1368
1369    /// Non-empty buffer with alignment=32 (power of two but below MIN_BUFFER_ALIGNMENT)
1370    /// must return AlignmentBelowMinimum.
1371    #[test]
1372    fn new_nonempty_alignment_below_minimum_32() {
1373        assert!(matches!(
1374            BufferHandle::new(1, 32, DeviceTag::Cpu, SyncMode::ProducerSynced),
1375            Err(Error::AlignmentBelowMinimum {
1376                alignment: 32,
1377                minimum: 64
1378            })
1379        ));
1380    }
1381
1382    /// Non-empty buffer with alignment=1 (power of two but well below MIN_BUFFER_ALIGNMENT)
1383    /// must return AlignmentBelowMinimum.
1384    #[test]
1385    fn new_nonempty_alignment_below_minimum_1() {
1386        assert!(matches!(
1387            BufferHandle::new(1, 1, DeviceTag::Cpu, SyncMode::ProducerSynced),
1388            Err(Error::AlignmentBelowMinimum {
1389                alignment: 1,
1390                minimum: 64
1391            })
1392        ));
1393    }
1394
1395    /// ADR-018: CPU buffer with non-ProducerSynced mode must return InvalidSyncMode.
1396    #[test]
1397    fn new_cpu_with_event_sync_rejected() {
1398        assert!(matches!(
1399            BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::Event),
1400            Err(Error::InvalidSyncMode(0x01))
1401        ));
1402    }
1403
1404    /// ADR-018: CPU buffer with ConsumerStream must return InvalidSyncMode.
1405    #[test]
1406    fn new_cpu_with_consumer_stream_rejected() {
1407        assert!(matches!(
1408            BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ConsumerStream),
1409            Err(Error::InvalidSyncMode(0x02))
1410        ));
1411    }
1412
1413    // ── BufferHandle::empty ──────────────────────────────────────────────────
1414
1415    /// BufferHandle::empty must produce a zero-sized handle with alignment 1.
1416    #[test]
1417    fn empty_byte_size_is_zero() {
1418        assert_eq!(BufferHandle::empty(DeviceTag::Cpu).byte_size(), 0);
1419    }
1420
1421    #[test]
1422    fn empty_alignment_is_one() {
1423        assert_eq!(BufferHandle::empty(DeviceTag::Cpu).alignment(), 1);
1424    }
1425
1426    #[test]
1427    fn empty_is_empty_returns_true() {
1428        assert!(BufferHandle::empty(DeviceTag::Cpu).is_empty());
1429    }
1430
1431    /// The device tag passed to empty() must be preserved.
1432    #[test]
1433    fn empty_preserves_device_tag_cpu() {
1434        assert_eq!(
1435            BufferHandle::empty(DeviceTag::Cpu).device_tag(),
1436            DeviceTag::Cpu
1437        );
1438    }
1439
1440    #[test]
1441    fn empty_preserves_device_tag_cuda() {
1442        assert_eq!(
1443            BufferHandle::empty(DeviceTag::Cuda).device_tag(),
1444            DeviceTag::Cuda
1445        );
1446    }
1447
1448    #[test]
1449    fn empty_preserves_device_tag_private() {
1450        let private = DeviceTag::from_byte(0xF5).unwrap();
1451        assert_eq!(BufferHandle::empty(private).device_tag(), private);
1452    }
1453
1454    // ── BufferHandle accessors ───────────────────────────────────────────────
1455
1456    /// byte_size(), alignment(), device_tag(), sync_mode(), and is_empty() must
1457    /// return the values that were passed to new().
1458    #[test]
1459    fn accessors_byte_size() {
1460        let handle = BufferHandle::new(8192, 4096, DeviceTag::Cuda, SyncMode::Event).unwrap();
1461        assert_eq!(handle.byte_size(), 8192);
1462    }
1463
1464    #[test]
1465    fn accessors_alignment() {
1466        let handle = BufferHandle::new(256, 256, DeviceTag::Metal, SyncMode::Event).unwrap();
1467        assert_eq!(handle.alignment(), 256);
1468    }
1469
1470    #[test]
1471    fn accessors_device_tag() {
1472        let handle = BufferHandle::new(512, 64, DeviceTag::Rocm, SyncMode::ConsumerStream).unwrap();
1473        assert_eq!(handle.device_tag(), DeviceTag::Rocm);
1474    }
1475
1476    #[test]
1477    fn accessors_sync_mode_producer_synced() {
1478        let handle = BufferHandle::new(512, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
1479        assert_eq!(handle.sync_mode(), SyncMode::ProducerSynced);
1480    }
1481
1482    #[test]
1483    fn accessors_sync_mode_event() {
1484        let handle = BufferHandle::new(512, 64, DeviceTag::Cuda, SyncMode::Event).unwrap();
1485        assert_eq!(handle.sync_mode(), SyncMode::Event);
1486    }
1487
1488    #[test]
1489    fn accessors_sync_mode_consumer_stream() {
1490        let handle = BufferHandle::new(512, 64, DeviceTag::Cuda, SyncMode::ConsumerStream).unwrap();
1491        assert_eq!(handle.sync_mode(), SyncMode::ConsumerStream);
1492    }
1493
1494    #[test]
1495    fn accessors_is_empty_false_for_nonempty() {
1496        let handle = BufferHandle::new(1, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
1497        assert!(!handle.is_empty());
1498    }
1499
1500    #[test]
1501    fn accessors_is_empty_true_for_zero_size() {
1502        let handle = BufferHandle::new(0, 1, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
1503        assert!(handle.is_empty());
1504    }
1505
1506    // ── validate_colocation ──────────────────────────────────────────────────
1507
1508    /// Empty slice must return EmptyBufferList.
1509    #[test]
1510    fn colocation_empty_slice_returns_error() {
1511        assert!(matches!(
1512            validate_colocation(&[]),
1513            Err(Error::EmptyBufferList)
1514        ));
1515    }
1516
1517    /// Single-element slice must succeed and return the handle's device tag.
1518    #[test]
1519    fn colocation_single_handle_returns_its_tag() {
1520        let handle = BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap();
1521        let result = validate_colocation(&[handle]);
1522        assert_eq!(result.unwrap(), DeviceTag::Cpu);
1523    }
1524
1525    /// All-CPU slice of three handles must succeed and return Cpu.
1526    #[test]
1527    fn colocation_all_cpu_three_handles() {
1528        let handles = [
1529            BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1530            BufferHandle::new(512, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1531            BufferHandle::new(256, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1532        ];
1533        assert_eq!(validate_colocation(&handles).unwrap(), DeviceTag::Cpu);
1534    }
1535
1536    /// Mixed Cpu + Cuda must return DeviceTagMismatch with correct wire bytes.
1537    #[test]
1538    fn colocation_mixed_cpu_cuda_returns_mismatch() {
1539        let handles = [
1540            BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1541            BufferHandle::new(256, 64, DeviceTag::Cuda, SyncMode::ProducerSynced).unwrap(),
1542        ];
1543        assert!(matches!(
1544            validate_colocation(&handles),
1545            Err(Error::DeviceTagMismatch {
1546                expected: 0x00,
1547                found: 0x01
1548            })
1549        ));
1550    }
1551
1552    /// All-Private(0xF0) slice must succeed and return Private(0xF0).
1553    #[test]
1554    fn colocation_all_private_same_tag() {
1555        let private = DeviceTag::from_byte(0xF0).unwrap();
1556        let handles = [
1557            BufferHandle::new(1024, 64, private, SyncMode::ProducerSynced).unwrap(),
1558            BufferHandle::new(512, 64, private, SyncMode::ProducerSynced).unwrap(),
1559        ];
1560        assert_eq!(validate_colocation(&handles).unwrap(), private);
1561    }
1562
1563    /// Mixed named + private must return DeviceTagMismatch.
1564    #[test]
1565    fn colocation_named_and_private_returns_mismatch() {
1566        let private = DeviceTag::from_byte(0xF0).unwrap();
1567        let handles = [
1568            BufferHandle::new(1024, 64, DeviceTag::Cpu, SyncMode::ProducerSynced).unwrap(),
1569            BufferHandle::new(512, 64, private, SyncMode::ProducerSynced).unwrap(),
1570        ];
1571        assert!(matches!(
1572            validate_colocation(&handles),
1573            Err(Error::DeviceTagMismatch {
1574                expected: 0x00,
1575                found: 0xF0
1576            })
1577        ));
1578    }
1579
1580    /// colocation validates the first mismatch position: 2nd handle agrees,
1581    /// 3rd handle disagrees. Error captures the first vs. third wire bytes.
1582    #[test]
1583    fn colocation_mismatch_at_third_element() {
1584        let handles = [
1585            BufferHandle::new(1024, 64, DeviceTag::Cuda, SyncMode::Event).unwrap(),
1586            BufferHandle::new(512, 64, DeviceTag::Cuda, SyncMode::Event).unwrap(),
1587            BufferHandle::new(256, 64, DeviceTag::Metal, SyncMode::Event).unwrap(),
1588        ];
1589        assert!(matches!(
1590            validate_colocation(&handles),
1591            Err(Error::DeviceTagMismatch {
1592                expected: 0x01,
1593                found: 0x03
1594            })
1595        ));
1596    }
1597
1598    // ── SyncMode ──────────────────────────────────────────────────────────────
1599
1600    mod sync_mode {
1601        use super::*;
1602        use std::collections::HashSet;
1603
1604        // ── from_byte — named values ─────────────────────────────────────────
1605
1606        /// Spec § buffer-protocol.md Sync Mode: 0x00 decodes to ProducerSynced.
1607        #[test]
1608        fn from_byte_producer_synced() {
1609            assert_eq!(SyncMode::from_byte(0x00).unwrap(), SyncMode::ProducerSynced);
1610        }
1611
1612        /// Spec § buffer-protocol.md Sync Mode: 0x01 decodes to Event.
1613        #[test]
1614        fn from_byte_event() {
1615            assert_eq!(SyncMode::from_byte(0x01).unwrap(), SyncMode::Event);
1616        }
1617
1618        /// Spec § buffer-protocol.md Sync Mode: 0x02 decodes to ConsumerStream.
1619        #[test]
1620        fn from_byte_consumer_stream() {
1621            assert_eq!(SyncMode::from_byte(0x02).unwrap(), SyncMode::ConsumerStream);
1622        }
1623
1624        // ── from_byte — reserved / invalid bytes ─────────────────────────────
1625
1626        /// Spec § buffer-protocol.md Sync Mode: 0x03 (first reserved) must
1627        /// return InvalidSyncMode(0x03).
1628        #[test]
1629        fn from_byte_0x03_is_invalid() {
1630            assert!(matches!(
1631                SyncMode::from_byte(0x03),
1632                Err(Error::InvalidSyncMode(0x03))
1633            ));
1634        }
1635
1636        /// Spec § buffer-protocol.md Sync Mode: 0xFE (reserved) must return
1637        /// InvalidSyncMode(0xFE).
1638        #[test]
1639        fn from_byte_0xfe_is_invalid() {
1640            assert!(matches!(
1641                SyncMode::from_byte(0xFE),
1642                Err(Error::InvalidSyncMode(0xFE))
1643            ));
1644        }
1645
1646        /// Spec § buffer-protocol.md Sync Mode: 0xFF (reserved) must return
1647        /// InvalidSyncMode(0xFF).
1648        #[test]
1649        fn from_byte_0xff_is_invalid() {
1650            assert!(matches!(
1651                SyncMode::from_byte(0xFF),
1652                Err(Error::InvalidSyncMode(0xFF))
1653            ));
1654        }
1655
1656        // ── to_byte — wire byte values ────────────────────────────────────────
1657
1658        /// ProducerSynced serializes to wire byte 0x00.
1659        #[test]
1660        fn to_byte_producer_synced() {
1661            assert_eq!(SyncMode::ProducerSynced.to_byte(), 0x00);
1662        }
1663
1664        /// Event serializes to wire byte 0x01.
1665        #[test]
1666        fn to_byte_event() {
1667            assert_eq!(SyncMode::Event.to_byte(), 0x01);
1668        }
1669
1670        /// ConsumerStream serializes to wire byte 0x02.
1671        #[test]
1672        fn to_byte_consumer_stream() {
1673            assert_eq!(SyncMode::ConsumerStream.to_byte(), 0x02);
1674        }
1675
1676        // ── from_byte → to_byte identity ─────────────────────────────────────
1677
1678        /// For each named value, from_byte(m.to_byte()) == m.
1679        #[test]
1680        fn from_byte_to_byte_identity_producer_synced() {
1681            let m = SyncMode::ProducerSynced;
1682            assert_eq!(SyncMode::from_byte(m.to_byte()).unwrap(), m);
1683        }
1684
1685        #[test]
1686        fn from_byte_to_byte_identity_event() {
1687            let m = SyncMode::Event;
1688            assert_eq!(SyncMode::from_byte(m.to_byte()).unwrap(), m);
1689        }
1690
1691        #[test]
1692        fn from_byte_to_byte_identity_consumer_stream() {
1693            let m = SyncMode::ConsumerStream;
1694            assert_eq!(SyncMode::from_byte(m.to_byte()).unwrap(), m);
1695        }
1696
1697        // ── Display ───────────────────────────────────────────────────────────
1698
1699        /// Spec § buffer-protocol.md Display: ProducerSynced displays as
1700        /// "producer_synced".
1701        #[test]
1702        fn display_producer_synced() {
1703            assert_eq!(SyncMode::ProducerSynced.to_string(), "producer_synced");
1704        }
1705
1706        /// Event displays as "event".
1707        #[test]
1708        fn display_event() {
1709            assert_eq!(SyncMode::Event.to_string(), "event");
1710        }
1711
1712        /// ConsumerStream displays as "consumer_stream".
1713        #[test]
1714        fn display_consumer_stream() {
1715            assert_eq!(SyncMode::ConsumerStream.to_string(), "consumer_stream");
1716        }
1717
1718        // ── Clone, Copy, PartialEq, Eq, Hash smoke tests ─────────────────────
1719
1720        /// Clone produces an equal value for each variant.
1721        #[test]
1722        fn clone_equals_original() {
1723            for m in [
1724                SyncMode::ProducerSynced,
1725                SyncMode::Event,
1726                SyncMode::ConsumerStream,
1727            ] {
1728                assert_eq!(m, m.clone());
1729            }
1730        }
1731
1732        /// Copy: assigning to a new binding leaves the original usable (Copy
1733        /// semantics verified by using both after the assignment).
1734        #[test]
1735        fn copy_semantics() {
1736            let original = SyncMode::Event;
1737            let copied = original;
1738            assert_eq!(original, copied);
1739        }
1740
1741        /// PartialEq: distinct variants are not equal.
1742        #[test]
1743        fn partial_eq_distinct_variants() {
1744            assert_ne!(SyncMode::ProducerSynced, SyncMode::Event);
1745            assert_ne!(SyncMode::ProducerSynced, SyncMode::ConsumerStream);
1746            assert_ne!(SyncMode::Event, SyncMode::ConsumerStream);
1747        }
1748
1749        /// Hash: all three variants produce distinct hashes (smoke test — hash
1750        /// collisions are possible in theory but the stdlib hasher avoids them
1751        /// for small integers).
1752        #[test]
1753        fn hash_all_variants_distinct() {
1754            let set: HashSet<SyncMode> = [
1755                SyncMode::ProducerSynced,
1756                SyncMode::Event,
1757                SyncMode::ConsumerStream,
1758            ]
1759            .into_iter()
1760            .collect();
1761            assert_eq!(set.len(), 3);
1762        }
1763    }
1764
1765    // ── DeviceTag new variants (ADR-016) ──────────────────────────────────────
1766
1767    mod device_tag_new_variants {
1768        use super::*;
1769
1770        // ── Vulkan (0x04) ────────────────────────────────────────────────────
1771
1772        /// ADR-016: 0x04 decodes to Vulkan.
1773        #[test]
1774        fn vulkan_from_byte() {
1775            assert_eq!(DeviceTag::from_byte(0x04).unwrap(), DeviceTag::Vulkan);
1776        }
1777
1778        /// Vulkan serializes to wire byte 0x04.
1779        #[test]
1780        fn vulkan_to_byte() {
1781            assert_eq!(DeviceTag::Vulkan.to_byte(), 0x04);
1782        }
1783
1784        /// from_byte(0x04) → to_byte() == 0x04.
1785        #[test]
1786        fn vulkan_round_trip() {
1787            let tag = DeviceTag::from_byte(0x04).unwrap();
1788            assert_eq!(tag.to_byte(), 0x04);
1789        }
1790
1791        /// Vulkan displays as "vulkan".
1792        #[test]
1793        fn vulkan_display() {
1794            assert_eq!(DeviceTag::Vulkan.to_string(), "vulkan");
1795        }
1796
1797        /// Vulkan is not a private tag.
1798        #[test]
1799        fn vulkan_is_not_private() {
1800            assert!(!DeviceTag::Vulkan.is_private());
1801        }
1802
1803        // ── WebGpu (0x05) ────────────────────────────────────────────────────
1804
1805        /// ADR-016: 0x05 decodes to WebGpu.
1806        #[test]
1807        fn webgpu_from_byte() {
1808            assert_eq!(DeviceTag::from_byte(0x05).unwrap(), DeviceTag::WebGpu);
1809        }
1810
1811        /// WebGpu serializes to wire byte 0x05.
1812        #[test]
1813        fn webgpu_to_byte() {
1814            assert_eq!(DeviceTag::WebGpu.to_byte(), 0x05);
1815        }
1816
1817        /// from_byte(0x05) → to_byte() == 0x05.
1818        #[test]
1819        fn webgpu_round_trip() {
1820            let tag = DeviceTag::from_byte(0x05).unwrap();
1821            assert_eq!(tag.to_byte(), 0x05);
1822        }
1823
1824        /// WebGpu displays as "webgpu".
1825        #[test]
1826        fn webgpu_display() {
1827            assert_eq!(DeviceTag::WebGpu.to_string(), "webgpu");
1828        }
1829
1830        /// WebGpu is not a private tag.
1831        #[test]
1832        fn webgpu_is_not_private() {
1833            assert!(!DeviceTag::WebGpu.is_private());
1834        }
1835
1836        // ── Hexagon (0x06) ───────────────────────────────────────────────────
1837
1838        /// ADR-016: 0x06 decodes to Hexagon.
1839        #[test]
1840        fn hexagon_from_byte() {
1841            assert_eq!(DeviceTag::from_byte(0x06).unwrap(), DeviceTag::Hexagon);
1842        }
1843
1844        /// Hexagon serializes to wire byte 0x06.
1845        #[test]
1846        fn hexagon_to_byte() {
1847            assert_eq!(DeviceTag::Hexagon.to_byte(), 0x06);
1848        }
1849
1850        /// from_byte(0x06) → to_byte() == 0x06.
1851        #[test]
1852        fn hexagon_round_trip() {
1853            let tag = DeviceTag::from_byte(0x06).unwrap();
1854            assert_eq!(tag.to_byte(), 0x06);
1855        }
1856
1857        /// Hexagon displays as "hexagon".
1858        #[test]
1859        fn hexagon_display() {
1860            assert_eq!(DeviceTag::Hexagon.to_string(), "hexagon");
1861        }
1862
1863        /// Hexagon is not a private tag.
1864        #[test]
1865        fn hexagon_is_not_private() {
1866            assert!(!DeviceTag::Hexagon.is_private());
1867        }
1868
1869        // ── LevelZero (0x07) ─────────────────────────────────────────────────
1870
1871        /// ADR-016: 0x07 decodes to LevelZero.
1872        #[test]
1873        fn level_zero_from_byte() {
1874            assert_eq!(DeviceTag::from_byte(0x07).unwrap(), DeviceTag::LevelZero);
1875        }
1876
1877        /// LevelZero serializes to wire byte 0x07.
1878        #[test]
1879        fn level_zero_to_byte() {
1880            assert_eq!(DeviceTag::LevelZero.to_byte(), 0x07);
1881        }
1882
1883        /// from_byte(0x07) → to_byte() == 0x07.
1884        #[test]
1885        fn level_zero_round_trip() {
1886            let tag = DeviceTag::from_byte(0x07).unwrap();
1887            assert_eq!(tag.to_byte(), 0x07);
1888        }
1889
1890        /// LevelZero displays as "level_zero".
1891        #[test]
1892        fn level_zero_display() {
1893            assert_eq!(DeviceTag::LevelZero.to_string(), "level_zero");
1894        }
1895
1896        /// LevelZero is not a private tag.
1897        #[test]
1898        fn level_zero_is_not_private() {
1899            assert!(!DeviceTag::LevelZero.is_private());
1900        }
1901
1902        // ── OpenCl (0x08) ────────────────────────────────────────────────────
1903
1904        /// ADR-016: 0x08 decodes to OpenCl.
1905        #[test]
1906        fn opencl_from_byte() {
1907            assert_eq!(DeviceTag::from_byte(0x08).unwrap(), DeviceTag::OpenCl);
1908        }
1909
1910        /// OpenCl serializes to wire byte 0x08.
1911        #[test]
1912        fn opencl_to_byte() {
1913            assert_eq!(DeviceTag::OpenCl.to_byte(), 0x08);
1914        }
1915
1916        /// from_byte(0x08) → to_byte() == 0x08.
1917        #[test]
1918        fn opencl_round_trip() {
1919            let tag = DeviceTag::from_byte(0x08).unwrap();
1920            assert_eq!(tag.to_byte(), 0x08);
1921        }
1922
1923        /// OpenCl displays as "opencl".
1924        #[test]
1925        fn opencl_display() {
1926            assert_eq!(DeviceTag::OpenCl.to_string(), "opencl");
1927        }
1928
1929        /// OpenCl is not a private tag.
1930        #[test]
1931        fn opencl_is_not_private() {
1932            assert!(!DeviceTag::OpenCl.is_private());
1933        }
1934
1935        // ── Edge cases ───────────────────────────────────────────────────────
1936
1937        /// 0x08 is the last named tag; it must succeed.
1938        #[test]
1939        fn from_byte_0x08_is_last_named_tag() {
1940            assert!(DeviceTag::from_byte(0x08).is_ok());
1941        }
1942
1943        /// 0x09 is the first reserved byte after the new range; must return
1944        /// ReservedDeviceTag(0x09).
1945        #[test]
1946        fn from_byte_0x09_is_first_reserved_after_new_range() {
1947            assert!(matches!(
1948                DeviceTag::from_byte(0x09),
1949                Err(Error::ReservedDeviceTag(0x09))
1950            ));
1951        }
1952
1953        /// The full new range 0x04–0x08 all round-trip cleanly.
1954        #[test]
1955        fn round_trip_new_range_all_bytes() {
1956            for b in 0x04u8..=0x08 {
1957                let tag = DeviceTag::from_byte(b)
1958                    .unwrap_or_else(|e| panic!("from_byte(0x{b:02X}) failed: {e}"));
1959                assert_eq!(tag.to_byte(), b, "round-trip failed for byte 0x{b:02X}");
1960            }
1961        }
1962    }
1963
1964    // ── MemoryClass ───────────────────────────────────────────────────────────
1965
1966    mod memory_class {
1967        use super::*;
1968
1969        /// Spec § buffer-protocol.md Memory Class: 0x00 decodes to Standard.
1970        #[test]
1971        fn from_byte_standard() {
1972            assert_eq!(MemoryClass::from_byte(0x00).unwrap(), MemoryClass::Standard);
1973        }
1974
1975        /// Spec § buffer-protocol.md Memory Class: 0x01 decodes to HostPinned.
1976        #[test]
1977        fn from_byte_host_pinned() {
1978            assert_eq!(
1979                MemoryClass::from_byte(0x01).unwrap(),
1980                MemoryClass::HostPinned
1981            );
1982        }
1983
1984        /// Spec § buffer-protocol.md Memory Class: 0x02 decodes to Unified.
1985        #[test]
1986        fn from_byte_unified() {
1987            assert_eq!(MemoryClass::from_byte(0x02).unwrap(), MemoryClass::Unified);
1988        }
1989
1990        /// Spec § buffer-protocol.md Memory Class: 0x03 decodes to Peer.
1991        #[test]
1992        fn from_byte_peer() {
1993            assert_eq!(MemoryClass::from_byte(0x03).unwrap(), MemoryClass::Peer);
1994        }
1995
1996        /// 0x04 is the first reserved byte; must return ReservedMemoryClass.
1997        #[test]
1998        fn from_byte_reserved_lower_bound() {
1999            assert!(matches!(
2000                MemoryClass::from_byte(0x04),
2001                Err(Error::ReservedMemoryClass(0x04))
2002            ));
2003        }
2004
2005        /// 0xEF is the upper bound of the reserved range.
2006        #[test]
2007        fn from_byte_reserved_upper_bound() {
2008            assert!(matches!(
2009                MemoryClass::from_byte(0xEF),
2010                Err(Error::ReservedMemoryClass(0xEF))
2011            ));
2012        }
2013
2014        /// 0xF0 (lower bound of private range) decodes to Private(0xF0).
2015        #[test]
2016        fn from_byte_private_lower_bound() {
2017            let cls = MemoryClass::from_byte(0xF0).unwrap();
2018            assert!(cls.is_private());
2019            assert_eq!(cls.to_byte(), 0xF0);
2020        }
2021
2022        /// 0xFE (upper bound of private range) decodes to Private(0xFE).
2023        #[test]
2024        fn from_byte_private_upper_bound() {
2025            let cls = MemoryClass::from_byte(0xFE).unwrap();
2026            assert!(cls.is_private());
2027            assert_eq!(cls.to_byte(), 0xFE);
2028        }
2029
2030        /// 0xFF is permanently reserved; must return InvalidMemoryClass.
2031        #[test]
2032        fn from_byte_invalid_sentinel() {
2033            assert!(matches!(
2034                MemoryClass::from_byte(0xFF),
2035                Err(Error::InvalidMemoryClass(0xFF))
2036            ));
2037        }
2038
2039        /// All four named variants round-trip through to_byte → from_byte.
2040        #[test]
2041        fn named_variants_round_trip() {
2042            for cls in [
2043                MemoryClass::Standard,
2044                MemoryClass::HostPinned,
2045                MemoryClass::Unified,
2046                MemoryClass::Peer,
2047            ] {
2048                assert_eq!(MemoryClass::from_byte(cls.to_byte()).unwrap(), cls);
2049            }
2050        }
2051
2052        /// BufferHandle::new() defaults to MemoryClass::Standard.
2053        #[test]
2054        fn buffer_handle_new_defaults_to_standard() {
2055            let h = BufferHandle::new(64, 64, DeviceTag::Cuda, SyncMode::ProducerSynced).unwrap();
2056            assert_eq!(h.memory_class(), MemoryClass::Standard);
2057        }
2058
2059        /// BufferHandle::with_memory_class() stores the given class.
2060        #[test]
2061        fn buffer_handle_with_memory_class_unified() {
2062            let h = BufferHandle::with_memory_class(
2063                64,
2064                64,
2065                DeviceTag::Cuda,
2066                SyncMode::ProducerSynced,
2067                MemoryClass::Unified,
2068            )
2069            .unwrap();
2070            assert_eq!(h.memory_class(), MemoryClass::Unified);
2071        }
2072
2073        /// BufferHandle::empty() always uses Standard.
2074        #[test]
2075        fn empty_handle_is_standard() {
2076            assert_eq!(
2077                BufferHandle::empty(DeviceTag::Cpu).memory_class(),
2078                MemoryClass::Standard
2079            );
2080        }
2081
2082        /// validate_colocation rejects mixed memory classes.
2083        #[test]
2084        fn validate_colocation_rejects_mixed_memory_class() {
2085            let standard =
2086                BufferHandle::new(64, 64, DeviceTag::Cuda, SyncMode::ProducerSynced).unwrap();
2087            let unified = BufferHandle::with_memory_class(
2088                64,
2089                64,
2090                DeviceTag::Cuda,
2091                SyncMode::ProducerSynced,
2092                MemoryClass::Unified,
2093            )
2094            .unwrap();
2095            assert!(matches!(
2096                validate_colocation(&[standard, unified]),
2097                Err(Error::MemoryClassMismatch {
2098                    expected: 0x00,
2099                    found: 0x02
2100                })
2101            ));
2102        }
2103
2104        /// validate_colocation accepts uniform memory class.
2105        #[test]
2106        fn validate_colocation_accepts_uniform_memory_class() {
2107            let a = BufferHandle::with_memory_class(
2108                64,
2109                64,
2110                DeviceTag::Cuda,
2111                SyncMode::ProducerSynced,
2112                MemoryClass::Unified,
2113            )
2114            .unwrap();
2115            let b = BufferHandle::with_memory_class(
2116                128,
2117                64,
2118                DeviceTag::Cuda,
2119                SyncMode::ProducerSynced,
2120                MemoryClass::Unified,
2121            )
2122            .unwrap();
2123            assert_eq!(validate_colocation(&[a, b]).unwrap(), DeviceTag::Cuda);
2124        }
2125    }
2126
2127    // ── BufferHandle + SyncMode integration ───────────────────────────────────
2128
2129    mod buffer_handle_sync_mode {
2130        use super::*;
2131        use crate::descriptor::TensorDescriptor;
2132        use crate::layout::LayoutDescriptor;
2133        use crate::{ElementType, Shape, MIN_BUFFER_ALIGNMENT};
2134
2135        // ── Non-CPU buffers accept Event and ConsumerStream ───────────────────
2136
2137        /// A non-CPU buffer with SyncMode::Event must be accepted and the
2138        /// sync_mode() accessor must return Event.
2139        #[test]
2140        fn new_with_event_sync_non_cpu_buffer() {
2141            let handle =
2142                BufferHandle::new(512, MIN_BUFFER_ALIGNMENT, DeviceTag::Cuda, SyncMode::Event)
2143                    .unwrap();
2144            assert_eq!(handle.sync_mode(), SyncMode::Event);
2145            assert_eq!(handle.device_tag(), DeviceTag::Cuda);
2146        }
2147
2148        /// A non-CPU buffer with SyncMode::ConsumerStream must be accepted and
2149        /// the sync_mode() accessor must return ConsumerStream.
2150        #[test]
2151        fn new_with_consumer_stream_sync_non_cpu_buffer() {
2152            let handle = BufferHandle::new(
2153                512,
2154                MIN_BUFFER_ALIGNMENT,
2155                DeviceTag::Rocm,
2156                SyncMode::ConsumerStream,
2157            )
2158            .unwrap();
2159            assert_eq!(handle.sync_mode(), SyncMode::ConsumerStream);
2160            assert_eq!(handle.device_tag(), DeviceTag::Rocm);
2161        }
2162
2163        // ── CPU buffers must reject non-ProducerSynced modes ─────────────────
2164
2165        /// ADR-018: CPU buffer with SyncMode::Event must be rejected with
2166        /// InvalidSyncMode(0x01).
2167        #[test]
2168        fn new_cpu_rejects_event() {
2169            let result =
2170                BufferHandle::new(512, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::Event);
2171            assert!(matches!(result, Err(Error::InvalidSyncMode(0x01))));
2172        }
2173
2174        /// ADR-018: CPU buffer with SyncMode::ConsumerStream must be rejected
2175        /// with InvalidSyncMode(0x02).
2176        #[test]
2177        fn new_cpu_rejects_consumer_stream() {
2178            let result = BufferHandle::new(
2179                512,
2180                MIN_BUFFER_ALIGNMENT,
2181                DeviceTag::Cpu,
2182                SyncMode::ConsumerStream,
2183            );
2184            assert!(matches!(result, Err(Error::InvalidSyncMode(0x02))));
2185        }
2186
2187        /// ADR-018: CPU buffer with SyncMode::ProducerSynced must succeed.
2188        #[test]
2189        fn new_cpu_allows_producer_synced() {
2190            let result = BufferHandle::new(
2191                512,
2192                MIN_BUFFER_ALIGNMENT,
2193                DeviceTag::Cpu,
2194                SyncMode::ProducerSynced,
2195            );
2196            assert!(result.is_ok());
2197        }
2198
2199        // ── BufferHandle::empty always uses ProducerSynced ────────────────────
2200
2201        /// BufferHandle::empty always sets sync_mode to ProducerSynced,
2202        /// even for non-CPU devices.
2203        #[test]
2204        fn empty_has_producer_synced() {
2205            assert_eq!(
2206                BufferHandle::empty(DeviceTag::Cuda).sync_mode(),
2207                SyncMode::ProducerSynced
2208            );
2209        }
2210
2211        // ── encode/decode round-trip preserves sync_mode ─────────────────────
2212
2213        /// A TensorDescriptor containing a buffer with SyncMode::Event must
2214        /// survive encode → decode with sync_mode unchanged.
2215        #[test]
2216        fn encode_decode_preserves_sync_mode_event() {
2217            let shape = Shape::new(vec![4u64]).unwrap();
2218            let buffer =
2219                BufferHandle::new(64, MIN_BUFFER_ALIGNMENT, DeviceTag::Cuda, SyncMode::Event)
2220                    .unwrap();
2221            let desc = TensorDescriptor::new(
2222                1,
2223                0,
2224                ElementType::Float32,
2225                shape,
2226                0,
2227                LayoutDescriptor::RowMajor,
2228                vec![buffer],
2229                None,
2230                None,
2231                None,
2232                None,
2233            )
2234            .unwrap();
2235
2236            let encoded = desc.encode().unwrap();
2237            let decoded = TensorDescriptor::decode(&encoded).unwrap();
2238
2239            assert_eq!(decoded.buffers[0].sync_mode(), SyncMode::Event);
2240            assert_eq!(decoded.buffers[0].device_tag(), DeviceTag::Cuda);
2241            assert_eq!(decoded, desc);
2242        }
2243
2244        /// A TensorDescriptor containing a buffer with SyncMode::ConsumerStream
2245        /// must survive encode → decode with sync_mode unchanged.
2246        #[test]
2247        fn encode_decode_preserves_sync_mode_consumer_stream() {
2248            let shape = Shape::new(vec![8u64]).unwrap();
2249            let buffer = BufferHandle::new(
2250                128,
2251                MIN_BUFFER_ALIGNMENT,
2252                DeviceTag::Vulkan,
2253                SyncMode::ConsumerStream,
2254            )
2255            .unwrap();
2256            let desc = TensorDescriptor::new(
2257                1,
2258                0,
2259                ElementType::Float32,
2260                shape,
2261                0,
2262                LayoutDescriptor::RowMajor,
2263                vec![buffer],
2264                None,
2265                None,
2266                None,
2267                None,
2268            )
2269            .unwrap();
2270
2271            let encoded = desc.encode().unwrap();
2272            let decoded = TensorDescriptor::decode(&encoded).unwrap();
2273
2274            assert_eq!(decoded.buffers[0].sync_mode(), SyncMode::ConsumerStream);
2275            assert_eq!(decoded.buffers[0].device_tag(), DeviceTag::Vulkan);
2276            assert_eq!(decoded, desc);
2277        }
2278    }
2279}