Skip to main content

virtio_accel_core/
lib.rs

1//! Transport-independent accelerator semantics.
2//!
3//! This crate does not name virtqueues, guest memory, host operating systems, or vendor APIs.
4//! Transport adapters validate untrusted input and translate it into these typed contracts.
5
6#![no_std]
7#![forbid(unsafe_code)]
8
9use bitflags::bitflags;
10use core::fmt;
11use core::num::{NonZeroU32, NonZeroU64};
12use virtio_accel_transport::{ByteAccessError, ReadableBytes, WritableBytes};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum BackendError {
16    Unsupported,
17    Incompatible,
18    InvalidArgument,
19    OutOfBounds,
20    Busy,
21    OutOfMemory,
22    ResourceLimit,
23    DeadlineExpired,
24    DeviceLost,
25    PermissionDenied,
26    /// Stable provider-owned error namespace. Transport adapters must not reinterpret it.
27    External {
28        domain: u32,
29        code: i64,
30    },
31}
32
33impl fmt::Display for BackendError {
34    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
35        write!(formatter, "{self:?}")
36    }
37}
38
39/// Extensible accelerator class. Unknown values remain representable across newer implementations.
40#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41#[repr(transparent)]
42pub struct AcceleratorClass(u16);
43
44impl AcceleratorClass {
45    pub const OTHER: Self = Self(0);
46    pub const NPU: Self = Self(1);
47    pub const GPU: Self = Self(2);
48    pub const DSP: Self = Self(3);
49
50    pub const fn new(value: u16) -> Self {
51        Self(value)
52    }
53
54    pub const fn get(self) -> u16 {
55        self.0
56    }
57}
58
59bitflags! {
60    /// Semantic capabilities exposed by a backend, independent of virtio feature negotiation.
61    ///
62    /// Capabilities describe which accelerator operations the backend can perform. They do not
63    /// change the wire layout. A transport feature bit is required separately whenever enabling a
64    /// capability would change descriptor framing or any other device/driver protocol behavior.
65    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
66    pub struct Capabilities: u64 {
67        /// Supports [`MemoryDomain::Host`] allocations.
68        const HOST_VISIBLE_MEMORY = 1 << 0;
69        /// Supports [`MemoryDomain::Device`] allocations.
70        const DEVICE_LOCAL_MEMORY = 1 << 1;
71        /// [`Accelerator::cancel_event`] is implemented for pending events.
72        const EVENT_CANCELLATION = 1 << 2;
73        /// Reserved for post-v1 external-allocation import/export semantics.
74        const EXTERNAL_MEMORY = 1 << 3;
75        /// Reserved until secure-context isolation requirements are specified.
76        const SECURE_CONTEXTS = 1 << 4;
77        /// Supports provider-owned [`MemoryDomain::Shared`] allocations.
78        const SHARED_MEMORY = 1 << 5;
79
80        /// Capabilities that make at least one provider-owned memory domain usable.
81        const MEMORY_DOMAINS = Self::HOST_VISIBLE_MEMORY.bits()
82            | Self::DEVICE_LOCAL_MEMORY.bits()
83            | Self::SHARED_MEMORY.bits();
84        /// Assigned bits whose semantics remain reserved by this version of the contract.
85        const RESERVED = Self::EXTERNAL_MEMORY.bits() | Self::SECURE_CONTEXTS.bits();
86    }
87}
88
89impl Capabilities {
90    /// Whether the backend can allocate the requested provider-owned memory domain.
91    pub const fn supports_memory_domain(self, domain: MemoryDomain) -> bool {
92        match domain {
93            MemoryDomain::Host => self.contains(Self::HOST_VISIBLE_MEMORY),
94            MemoryDomain::Device => self.contains(Self::DEVICE_LOCAL_MEMORY),
95            MemoryDomain::Shared => self.contains(Self::SHARED_MEMORY),
96        }
97    }
98}
99
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub struct DeviceIdentity {
102    pub uuid: [u8; 16],
103    pub class: AcceleratorClass,
104    pub vendor_id: u32,
105    pub device_id: u32,
106}
107
108#[derive(Clone, Copy, Debug, PartialEq, Eq)]
109pub struct DeviceLimits {
110    pub max_contexts: u32,
111    pub max_buffers_per_context: u32,
112    pub max_programs_per_context: u32,
113    pub max_queues_per_context: u32,
114    pub max_events_per_context: u32,
115    pub max_bindings_per_submission: u32,
116    pub max_buffer_bytes: u64,
117    pub max_artifact_bytes: u64,
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
121pub struct DeviceInfo {
122    pub identity: DeviceIdentity,
123    pub capabilities: Capabilities,
124    pub limits: DeviceLimits,
125}
126
127/// Invalid provider metadata discovered before any resource operation is invoked.
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129pub enum DeviceInfoError {
130    /// A capability whose semantics are still reserved was advertised.
131    ReservedCapabilities,
132    /// No provider-owned memory domain can be allocated.
133    MissingMemoryDomain,
134    /// A mandatory resource or byte limit is zero.
135    ZeroLimit,
136}
137
138bitflags! {
139    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
140    pub struct ContextFlags: u32 {
141        /// Reserved until secure-context isolation and transport semantics are specified.
142        const SECURE = 1 << 0;
143    }
144}
145
146#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
147pub struct ContextDesc {
148    pub flags: ContextFlags,
149}
150
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152#[repr(u8)]
153pub enum MemoryDomain {
154    /// Provider memory optimized for host transfers.
155    ///
156    /// If the usage includes program access, the returned allocation is still directly bindable;
157    /// this value never permits per-submission staging.
158    Host = 1,
159    /// Provider memory optimized for accelerator access.
160    ///
161    /// Explicit read/write transfers may stage through provider-owned temporary memory.
162    Device = 2,
163    /// One provider-owned allocation that is host visible and directly accelerator bindable.
164    ///
165    /// This does not imply cross-process export, guest-memory import, cache coherence, or any
166    /// platform external-memory handle.
167    Shared = 3,
168}
169
170impl TryFrom<u8> for MemoryDomain {
171    type Error = BackendError;
172
173    fn try_from(value: u8) -> Result<Self, Self::Error> {
174        match value {
175            1 => Ok(Self::Host),
176            2 => Ok(Self::Device),
177            3 => Ok(Self::Shared),
178            _ => Err(BackendError::InvalidArgument),
179        }
180    }
181}
182
183bitflags! {
184    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
185    pub struct BufferUsage: u32 {
186        /// The buffer may be the source of an explicit [`Accelerator::read_buffer`] transfer.
187        const TRANSFER_SOURCE = 1 << 0;
188        /// The buffer may be the destination of an explicit [`Accelerator::write_buffer`] transfer.
189        const TRANSFER_DESTINATION = 1 << 1;
190        const PROGRAM_INPUT = 1 << 2;
191        const PROGRAM_OUTPUT = 1 << 3;
192        const MUTABLE_STATE = 1 << 4;
193    }
194}
195
196#[derive(Clone, Copy, Debug, PartialEq, Eq)]
197pub struct BufferDesc {
198    bytes: NonZeroU64,
199    alignment: NonZeroU64,
200    pub domain: MemoryDomain,
201    pub usage: BufferUsage,
202}
203
204impl BufferDesc {
205    pub fn new(
206        bytes: u64,
207        alignment: u64,
208        domain: MemoryDomain,
209        usage: BufferUsage,
210    ) -> Result<Self, BackendError> {
211        let bytes = NonZeroU64::new(bytes).ok_or(BackendError::InvalidArgument)?;
212        let alignment = NonZeroU64::new(alignment).ok_or(BackendError::InvalidArgument)?;
213        if !alignment.get().is_power_of_two()
214            || usage.is_empty()
215            || !BufferUsage::all().contains(usage)
216        {
217            return Err(BackendError::InvalidArgument);
218        }
219        Ok(Self {
220            bytes,
221            alignment,
222            domain,
223            usage,
224        })
225    }
226
227    pub const fn bytes(self) -> u64 {
228        self.bytes.get()
229    }
230
231    pub const fn alignment(self) -> u64 {
232        self.alignment.get()
233    }
234
235    /// Whether this declaration permits one program binding access mode.
236    pub const fn allows_access(self, access: AccessMode) -> bool {
237        match access {
238            AccessMode::Read => self
239                .usage
240                .intersects(BufferUsage::PROGRAM_INPUT.union(BufferUsage::MUTABLE_STATE)),
241            AccessMode::Write => self
242                .usage
243                .intersects(BufferUsage::PROGRAM_OUTPUT.union(BufferUsage::MUTABLE_STATE)),
244            AccessMode::ReadWrite => self.usage.contains(BufferUsage::MUTABLE_STATE),
245        }
246    }
247
248    /// Whether this allocation can appear in a program binding.
249    pub const fn is_program_visible(self) -> bool {
250        self.usage.intersects(
251            BufferUsage::PROGRAM_INPUT
252                .union(BufferUsage::PROGRAM_OUTPUT)
253                .union(BufferUsage::MUTABLE_STATE),
254        )
255    }
256}
257
258bitflags! {
259    /// Properties of the actual provider allocation returned for a [`BufferDesc`].
260    ///
261    /// These properties describe the backing allocation, not an aspirational fast path. A backend
262    /// must reject allocation rather than advertise a property that it can satisfy only by
263    /// allocating and copying a full-size bounce buffer during submission.
264    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
265    pub struct BufferProperties: u32 {
266        /// The provider can access the allocation through a host mapping.
267        const HOST_VISIBLE = 1 << 0;
268        /// The allocation uses the provider's accelerator-local placement class.
269        const DEVICE_LOCAL = 1 << 1;
270        /// Compatible program submissions bind this exact allocation without copying the bound
271        /// byte range into or out of a different allocation.
272        const DIRECT_BINDING = 1 << 2;
273    }
274}
275
276/// Verified properties of one provider allocation.
277#[derive(Clone, Copy, Debug, PartialEq, Eq)]
278pub struct BufferInfo {
279    desc: BufferDesc,
280    allocation_bytes: NonZeroU64,
281    alignment: NonZeroU64,
282    properties: BufferProperties,
283}
284
285impl BufferInfo {
286    /// Validate that actual allocation properties honestly satisfy the requested descriptor.
287    pub fn new(
288        desc: BufferDesc,
289        allocation_bytes: u64,
290        alignment: u64,
291        properties: BufferProperties,
292    ) -> Result<Self, BackendError> {
293        let allocation_bytes =
294            NonZeroU64::new(allocation_bytes).ok_or(BackendError::InvalidArgument)?;
295        let alignment = NonZeroU64::new(alignment).ok_or(BackendError::InvalidArgument)?;
296        if !BufferProperties::all().contains(properties) {
297            return Err(BackendError::InvalidArgument);
298        }
299        if allocation_bytes.get() < desc.bytes()
300            || !alignment.get().is_power_of_two()
301            || alignment.get() < desc.alignment()
302        {
303            return Err(BackendError::Incompatible);
304        }
305
306        let required = match desc.domain {
307            MemoryDomain::Host => BufferProperties::HOST_VISIBLE,
308            MemoryDomain::Device => BufferProperties::DEVICE_LOCAL,
309            MemoryDomain::Shared => {
310                BufferProperties::HOST_VISIBLE.union(BufferProperties::DIRECT_BINDING)
311            }
312        };
313        if !properties.contains(required)
314            || (desc.is_program_visible() && !properties.contains(BufferProperties::DIRECT_BINDING))
315        {
316            return Err(BackendError::Incompatible);
317        }
318
319        Ok(Self {
320            desc,
321            allocation_bytes,
322            alignment,
323            properties,
324        })
325    }
326
327    pub const fn desc(self) -> BufferDesc {
328        self.desc
329    }
330
331    /// Physical/provider backing bytes retained for this logical buffer.
332    pub const fn allocation_bytes(self) -> u64 {
333        self.allocation_bytes.get()
334    }
335
336    /// Alignment guaranteed by the actual provider allocation.
337    pub const fn alignment(self) -> u64 {
338        self.alignment.get()
339    }
340
341    pub const fn properties(self) -> BufferProperties {
342        self.properties
343    }
344}
345
346impl DeviceInfo {
347    /// Validate immutable provider metadata once, before constructing live object state.
348    ///
349    /// Unknown capability bits remain representable for forward-compatible diagnostics. Assigned
350    /// reserved bits are rejected because this version cannot enforce their ownership and
351    /// synchronization rules.
352    pub const fn validate(self) -> Result<(), DeviceInfoError> {
353        if self.capabilities.intersects(Capabilities::RESERVED) {
354            return Err(DeviceInfoError::ReservedCapabilities);
355        }
356        if !self.capabilities.intersects(Capabilities::MEMORY_DOMAINS) {
357            return Err(DeviceInfoError::MissingMemoryDomain);
358        }
359        if self.limits.max_contexts == 0
360            || self.limits.max_buffers_per_context == 0
361            || self.limits.max_programs_per_context == 0
362            || self.limits.max_queues_per_context == 0
363            || self.limits.max_events_per_context == 0
364            || self.limits.max_bindings_per_submission == 0
365            || self.limits.max_buffer_bytes == 0
366            || self.limits.max_artifact_bytes == 0
367        {
368            return Err(DeviceInfoError::ZeroLimit);
369        }
370        Ok(())
371    }
372
373    /// Validate context intent before backend invocation.
374    ///
375    /// This contract currently reserves every nonempty context flag set.
376    pub fn validate_context_desc(self, desc: ContextDesc) -> Result<(), BackendError> {
377        if desc.flags.is_empty() {
378            Ok(())
379        } else {
380            Err(BackendError::Unsupported)
381        }
382    }
383
384    /// Validate allocation size and memory-domain support before backend invocation.
385    pub fn validate_buffer_desc(self, desc: BufferDesc) -> Result<(), BackendError> {
386        if desc.bytes() > self.limits.max_buffer_bytes {
387            return Err(BackendError::ResourceLimit);
388        }
389        if !self.capabilities.supports_memory_domain(desc.domain) {
390            return Err(BackendError::Unsupported);
391        }
392        Ok(())
393    }
394
395    /// Validate that a backend allocation describes the request it was asked to satisfy.
396    pub fn validate_buffer_info(
397        self,
398        requested: BufferDesc,
399        actual: BufferInfo,
400    ) -> Result<(), BackendError> {
401        self.validate_buffer_desc(requested)?;
402        if actual.desc() != requested {
403            return Err(BackendError::Incompatible);
404        }
405        Ok(())
406    }
407
408    /// Validate execution-queue intent before backend invocation.
409    ///
410    /// This contract currently reserves every nonempty execution-queue flag set.
411    pub fn validate_queue_desc(self, desc: QueueDesc) -> Result<(), BackendError> {
412        if desc.flags.is_empty() {
413            Ok(())
414        } else {
415            Err(BackendError::Unsupported)
416        }
417    }
418
419    /// Validate event-cancellation support before backend invocation.
420    pub fn validate_event_cancellation(self) -> Result<(), BackendError> {
421        if self.capabilities.contains(Capabilities::EVENT_CANCELLATION) {
422            Ok(())
423        } else {
424            Err(BackendError::Unsupported)
425        }
426    }
427}
428
429/// A newly allocated native buffer handle and its verified backing properties.
430///
431/// Device implementations should retain `info` in their object record and pass only `buffer` to
432/// backend hot paths.
433#[derive(Debug)]
434pub struct AllocatedBuffer<B> {
435    buffer: B,
436    info: BufferInfo,
437}
438
439impl<B> AllocatedBuffer<B> {
440    pub const fn new(buffer: B, info: BufferInfo) -> Self {
441        Self { buffer, info }
442    }
443
444    pub const fn buffer(&self) -> &B {
445        &self.buffer
446    }
447
448    pub fn buffer_mut(&mut self) -> &mut B {
449        &mut self.buffer
450    }
451
452    pub const fn info(&self) -> BufferInfo {
453        self.info
454    }
455
456    pub fn into_parts(self) -> (B, BufferInfo) {
457        (self.buffer, self.info)
458    }
459}
460
461#[derive(Clone, Copy, Debug, PartialEq, Eq)]
462pub struct BufferRange {
463    pub offset: u64,
464    bytes: NonZeroU64,
465}
466
467impl BufferRange {
468    pub fn new(offset: u64, bytes: u64) -> Result<Self, BackendError> {
469        let bytes = NonZeroU64::new(bytes).ok_or(BackendError::InvalidArgument)?;
470        offset
471            .checked_add(bytes.get())
472            .ok_or(BackendError::OutOfBounds)?;
473        Ok(Self { offset, bytes })
474    }
475
476    pub const fn bytes(self) -> u64 {
477        self.bytes.get()
478    }
479
480    pub const fn end(self) -> u64 {
481        self.offset + self.bytes.get()
482    }
483}
484
485#[derive(Clone, Copy, Debug, PartialEq, Eq)]
486#[repr(u8)]
487pub enum AccessMode {
488    Read = 1,
489    Write = 2,
490    ReadWrite = 3,
491}
492
493impl TryFrom<u8> for AccessMode {
494    type Error = BackendError;
495
496    fn try_from(value: u8) -> Result<Self, Self::Error> {
497        match value {
498            1 => Ok(Self::Read),
499            2 => Ok(Self::Write),
500            3 => Ok(Self::ReadWrite),
501            _ => Err(BackendError::InvalidArgument),
502        }
503    }
504}
505
506/// A bounded byte source that may be physically segmented.
507///
508/// Transport adapters can implement this trait over validated descriptor-backed regions so
509/// providers can read directly into final program or buffer storage without first coalescing the
510/// complete payload. Every range fully contained in `0..len()` must be readable for the duration of
511/// the backend call. The optional contiguous view preserves the single-slice fast path.
512pub trait ByteSource: fmt::Debug {
513    /// Stable logical length of this source.
514    fn len(&self) -> u64;
515
516    fn is_empty(&self) -> bool {
517        self.len() == 0
518    }
519
520    /// Fill `target` from the exact logical range beginning at `offset`.
521    fn read_at(&self, offset: u64, target: &mut [u8]) -> Result<(), BackendError>;
522
523    /// Borrow the complete logical source when it is one contiguous region.
524    ///
525    /// A returned slice has length [`Self::len`] and contains the same bytes as `read_at`.
526    fn as_contiguous(&self) -> Option<&[u8]> {
527        None
528    }
529}
530
531/// Zero-copy core byte-source adapter over a transport-owned readable port.
532#[derive(Debug)]
533pub struct TransportByteSource<'a, T: ?Sized>(&'a T);
534
535impl<'a, T: ?Sized> TransportByteSource<'a, T> {
536    /// Borrow a transport-readable port without copying or coalescing its bytes.
537    pub const fn new(source: &'a T) -> Self {
538        Self(source)
539    }
540
541    /// Recover the wrapped transport port.
542    pub const fn into_inner(self) -> &'a T {
543        self.0
544    }
545}
546
547impl<T: ReadableBytes + ?Sized> ByteSource for TransportByteSource<'_, T> {
548    fn len(&self) -> u64 {
549        ReadableBytes::len(self.0)
550    }
551
552    fn read_at(&self, offset: u64, target: &mut [u8]) -> Result<(), BackendError> {
553        ReadableBytes::read_at(self.0, offset, target).map_err(backend_error_from_byte_access)
554    }
555
556    fn as_contiguous(&self) -> Option<&[u8]> {
557        ReadableBytes::as_contiguous(self.0)
558    }
559}
560
561impl ByteSource for [u8] {
562    fn len(&self) -> u64 {
563        self.len() as u64
564    }
565
566    fn read_at(&self, offset: u64, target: &mut [u8]) -> Result<(), BackendError> {
567        let start = usize::try_from(offset).map_err(|_| BackendError::OutOfBounds)?;
568        let end = start
569            .checked_add(target.len())
570            .filter(|end| *end <= self.len())
571            .ok_or(BackendError::OutOfBounds)?;
572        target.copy_from_slice(&self[start..end]);
573        Ok(())
574    }
575
576    fn as_contiguous(&self) -> Option<&[u8]> {
577        Some(self)
578    }
579}
580
581impl<const N: usize> ByteSource for [u8; N] {
582    fn len(&self) -> u64 {
583        N as u64
584    }
585
586    fn read_at(&self, offset: u64, target: &mut [u8]) -> Result<(), BackendError> {
587        ByteSource::read_at(self.as_slice(), offset, target)
588    }
589
590    fn as_contiguous(&self) -> Option<&[u8]> {
591        Some(self)
592    }
593}
594
595/// A bounded byte destination that may be physically segmented.
596///
597/// Providers can write buffer contents directly into validated response regions. The optional
598/// contiguous view avoids callback overhead when the destination is already one slice. Every range
599/// fully contained in `0..len()` must be writable for the duration of the backend call.
600pub trait ByteSink: fmt::Debug {
601    /// Stable logical length of this destination.
602    fn len(&self) -> u64;
603
604    fn is_empty(&self) -> bool {
605        self.len() == 0
606    }
607
608    /// Write `source` to the exact logical range beginning at `offset`.
609    fn write_at(&mut self, offset: u64, source: &[u8]) -> Result<(), BackendError>;
610
611    /// Mutably borrow the complete logical destination when it is one contiguous region.
612    ///
613    /// A returned slice has length [`Self::len`] and represents the same bytes as `write_at`.
614    fn as_contiguous_mut(&mut self) -> Option<&mut [u8]> {
615        None
616    }
617}
618
619/// Zero-copy core byte-sink adapter over a transport-owned writable port.
620#[derive(Debug)]
621pub struct TransportByteSink<'a, T: ?Sized>(&'a mut T);
622
623impl<'a, T: ?Sized> TransportByteSink<'a, T> {
624    /// Borrow a transport-writable port without copying or coalescing its bytes.
625    pub const fn new(sink: &'a mut T) -> Self {
626        Self(sink)
627    }
628
629    /// Recover the wrapped transport port.
630    pub fn into_inner(self) -> &'a mut T {
631        self.0
632    }
633}
634
635impl<T: WritableBytes + ?Sized> ByteSink for TransportByteSink<'_, T> {
636    fn len(&self) -> u64 {
637        WritableBytes::len(self.0)
638    }
639
640    fn write_at(&mut self, offset: u64, source: &[u8]) -> Result<(), BackendError> {
641        WritableBytes::write_at(self.0, offset, source).map_err(backend_error_from_byte_access)
642    }
643
644    fn as_contiguous_mut(&mut self) -> Option<&mut [u8]> {
645        WritableBytes::as_contiguous_mut(self.0)
646    }
647}
648
649const fn backend_error_from_byte_access(error: ByteAccessError) -> BackendError {
650    match error {
651        ByteAccessError::OutOfBounds => BackendError::OutOfBounds,
652        ByteAccessError::Busy | ByteAccessError::Reset => BackendError::Busy,
653        ByteAccessError::Access => BackendError::DeviceLost,
654    }
655}
656
657impl ByteSink for [u8] {
658    fn len(&self) -> u64 {
659        self.len() as u64
660    }
661
662    fn write_at(&mut self, offset: u64, source: &[u8]) -> Result<(), BackendError> {
663        let start = usize::try_from(offset).map_err(|_| BackendError::OutOfBounds)?;
664        let end = start
665            .checked_add(source.len())
666            .filter(|end| *end <= self.len())
667            .ok_or(BackendError::OutOfBounds)?;
668        self[start..end].copy_from_slice(source);
669        Ok(())
670    }
671
672    fn as_contiguous_mut(&mut self) -> Option<&mut [u8]> {
673        Some(self)
674    }
675}
676
677impl<const N: usize> ByteSink for [u8; N] {
678    fn len(&self) -> u64 {
679        N as u64
680    }
681
682    fn write_at(&mut self, offset: u64, source: &[u8]) -> Result<(), BackendError> {
683        ByteSink::write_at(self.as_mut_slice(), offset, source)
684    }
685
686    fn as_contiguous_mut(&mut self) -> Option<&mut [u8]> {
687        Some(self)
688    }
689}
690
691/// Opaque, provider-owned executable format identifier.
692#[derive(Clone, Copy, Debug, PartialEq, Eq)]
693#[repr(transparent)]
694pub struct ArtifactFormat(NonZeroU32);
695
696impl ArtifactFormat {
697    pub const fn new(value: u32) -> Option<Self> {
698        match NonZeroU32::new(value) {
699            Some(value) => Some(Self(value)),
700            None => None,
701        }
702    }
703
704    pub const fn get(self) -> u32 {
705        self.0.get()
706    }
707}
708
709/// Opaque target words. Their schema belongs to the artifact format, not this crate.
710#[derive(Clone, Copy, Debug, PartialEq, Eq)]
711#[repr(transparent)]
712pub struct TargetIdentity(pub [u32; 12]);
713
714/// Borrowed program artifact envelope.
715///
716/// Payload bytes may be segmented; providers should stream them into final resident storage or use
717/// [`ByteSource::as_contiguous`] when a borrowed slice is available. `resident_bytes` is the
718/// caller-authorized upper bound for all provider storage retained by the returned program; a
719/// provider must reject the artifact if it cannot stay within that charge.
720#[derive(Clone, Copy, Debug)]
721pub struct ArtifactRef<'a> {
722    pub format: ArtifactFormat,
723    pub target: TargetIdentity,
724    pub payload: &'a dyn ByteSource,
725    pub resident_bytes: u64,
726}
727
728bitflags! {
729    /// Flags for an accelerator execution queue.
730    ///
731    /// This queue is a backend object used to submit programs. It is not a virtqueue; the v1
732    /// protocol uses the term *command virtqueue* for the transport queue carrying requests.
733    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
734    pub struct QueueFlags: u32 {
735        /// Reserved until ordering behavior and capability negotiation are specified.
736        const IN_ORDER = 1 << 0;
737    }
738}
739
740#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
741pub struct QueueDesc {
742    pub flags: QueueFlags,
743}
744
745/// A relative timeout measured from backend admission. Zero on the wire means infinite.
746#[derive(Clone, Copy, Debug, PartialEq, Eq)]
747pub enum Timeout {
748    Infinite,
749    AfterNs(NonZeroU64),
750}
751
752impl Timeout {
753    pub const fn from_wire_ns(value: u64) -> Self {
754        match NonZeroU64::new(value) {
755            Some(value) => Self::AfterNs(value),
756            None => Self::Infinite,
757        }
758    }
759
760    pub const fn to_wire_ns(self) -> u64 {
761        match self {
762            Self::Infinite => 0,
763            Self::AfterNs(value) => value.get(),
764        }
765    }
766}
767
768/// One borrowed program binding. The referenced buffer must remain alive until its event is
769/// reclaimed.
770///
771/// Program-visible buffers carry [`BufferProperties::DIRECT_BINDING`]. A backend must reject an
772/// incompatible buffer/program combination instead of copying the range into a hidden bounce
773/// allocation. Binding order is not semantic; command engines may present the slice in slot order.
774///
775/// Before [`Accelerator::submit`], hosts must reject an [`AccessMode`] incompatible with the
776/// buffer's declared [`BufferUsage`] (see [`Self::validate_for_submit`]).
777#[derive(Debug)]
778pub struct BindingRef<'a, B> {
779    pub slot: u32,
780    pub buffer: &'a B,
781    pub range: BufferRange,
782    pub access: AccessMode,
783}
784
785impl<'a, B> BindingRef<'a, B> {
786    /// Slot/count checks plus [`BufferDesc::allows_access`] for each binding.
787    ///
788    /// Hosts must enforce both checks before [`Accelerator::submit`]. This combined
789    /// helper is suitable when descriptors are already available as a slice. A host
790    /// resolving descriptors individually may call [`validate_bindings`] once and
791    /// [`BufferDesc::allows_access`] as each buffer is resolved, avoiding a descriptor
792    /// mirror allocation. `descs[i]` must be the descriptor for the buffer behind
793    /// `bindings[i].buffer` (equal length alone is not enough). A usage mismatch
794    /// returns [`BackendError::PermissionDenied`].
795    pub fn validate_for_submit(
796        bindings: &[Self],
797        descs: &[BufferDesc],
798        max_bindings: u32,
799    ) -> Result<(), BackendError> {
800        validate_bindings(bindings, max_bindings)?;
801        if bindings.len() != descs.len() {
802            return Err(BackendError::InvalidArgument);
803        }
804        for (binding, desc) in bindings.iter().zip(descs.iter()) {
805            if !desc.allows_access(binding.access) {
806                return Err(BackendError::PermissionDenied);
807            }
808        }
809        Ok(())
810    }
811}
812
813/// Slot/count uniqueness helper for program bindings.
814///
815/// Structural only: nonempty, bounded by `max_bindings`, and unique slots. This is
816/// **incomplete** for pre-admission checks -- it does not enforce access/usage
817/// compatibility required before backend admission. Prefer
818/// [`BindingRef::validate_for_submit`] before [`Accelerator::submit`]. Strictly
819/// slot-ordered input takes a linear, allocation-free path; arbitrary order remains
820/// supported by the allocation-free fallback.
821pub fn validate_bindings<B>(
822    bindings: &[BindingRef<'_, B>],
823    max_bindings: u32,
824) -> Result<(), BackendError> {
825    if bindings.is_empty() || bindings.len() > max_bindings as usize {
826        return Err(BackendError::ResourceLimit);
827    }
828
829    // The wire decoder canonicalizes bindings into slot order. Recognizing that
830    // invariant here avoids a second quadratic uniqueness pass on every host
831    // submission while preserving the public API's order-independent semantics.
832    if bindings.windows(2).all(|pair| pair[0].slot < pair[1].slot) {
833        return Ok(());
834    }
835
836    for (index, binding) in bindings.iter().enumerate() {
837        if bindings[..index]
838            .iter()
839            .any(|prior| prior.slot == binding.slot)
840        {
841            return Err(BackendError::InvalidArgument);
842        }
843    }
844    Ok(())
845}
846
847#[derive(Clone, Copy, Debug, PartialEq, Eq)]
848pub enum EventState {
849    Pending,
850    Complete,
851    Failed(BackendError),
852    Cancelled,
853}
854
855/// Submission failure that makes the provider acceptance boundary explicit.
856#[derive(Debug)]
857pub enum SubmitFailure<E> {
858    /// The backend guarantees execution was not accepted and no resources were retained.
859    Rejected(BackendError),
860    /// Acceptance is uncertain; the event owns the resources until it reaches a terminal state.
861    Indeterminate { error: BackendError, event: E },
862}
863
864/// Failure to release a provider handle.
865#[derive(Debug)]
866pub enum ReleaseFailure<R> {
867    /// The backend rejected the release and returns the still-live resource for retry.
868    Rejected { error: BackendError, resource: R },
869    /// The resource state is unknown. The adapter must invalidate its ID and request device reset.
870    Indeterminate { error: BackendError },
871}
872
873impl<R> ReleaseFailure<R> {
874    pub const fn error(&self) -> BackendError {
875        match self {
876            Self::Rejected { error, .. } | Self::Indeterminate { error } => *error,
877        }
878    }
879}
880
881/// Native accelerator lifecycle over provider-owned handle types.
882///
883/// The reference command engine is generic over this trait, so its calls are statically dispatched
884/// and native handles need no boxing. The trait imposes no `Send` or `Sync` bounds: a provider may
885/// preserve thread-affine handles, while a provider that opts into those auto traits must make the
886/// corresponding shared calls safe. Callers must not overlap a mutable borrow, a consumed handle,
887/// or destruction with another use of the same resource.
888///
889/// Borrowed arguments are valid only for the duration of a call and must not be retained as Rust
890/// references. Destructive methods consume handles. A caller must reject parent destruction while
891/// child objects or in-flight events still exist; it must not use `Drop` timing as lifecycle state.
892///
893/// The only operations that explicitly transfer buffer contents are [`Self::write_buffer`] and
894/// [`Self::read_buffer`]. Allocation, submission, polling, and release must not hide full-range
895/// staging copies. In particular, `submit` binds the exact provider allocation directly or rejects
896/// it as [`BackendError::Incompatible`].
897///
898/// Dynamic loading, a stable binary interface, and erased cross-boundary handle ownership are not
899/// defined here. An integration that needs dynamic dispatch must fix one concrete handle family in
900/// an adapter without weakening this trait's borrowing, acceptance, or release contracts.
901pub trait Accelerator {
902    /// Owned context handle. It may be a native value and need not be boxed, cloneable, or thread
903    /// safe.
904    type Context;
905    /// Owned handle for the exact allocation described by its accompanying [`BufferInfo`].
906    type Buffer;
907    /// Owned resident-program handle with no borrow of its source artifact.
908    type Program;
909    /// Owned accelerator execution-queue handle.
910    type Queue;
911    /// Owned submission and completion token with no borrow of the submitted binding slice.
912    type Event;
913
914    /// Return immutable identity, capability, and limit metadata.
915    ///
916    /// - **Ownership/lifetime:** no ownership changes; a successful value must remain stable for
917    ///   the lifetime of this backend instance.
918    /// - **Progress/concurrency:** discovery may perform bounded synchronous provider work but must
919    ///   not wait for resource progress. Concurrent calls are permitted only when the concrete
920    ///   backend is `Sync`.
921    /// - **Failure/retry:** an error creates no resource and may be retried; callers validate and
922    ///   cache the first successful result before invoking resource methods.
923    /// - **Allocation/copies:** the call must not allocate resource backing or copy bulk content.
924    fn device_info(&self) -> Result<DeviceInfo, BackendError>;
925
926    /// Create one context from prevalidated intent.
927    ///
928    /// - **Ownership/lifetime:** `desc` is consumed by value and not retained by reference; success
929    ///   returns one owned context. All current nonempty context flags are unsupported.
930    /// - **Progress/concurrency:** provider setup may synchronously block, but must not wait for
931    ///   unrelated resource progress. Independent creation may overlap only when concrete types
932    ///   permit it.
933    /// - **Failure/retry:** `Err` guarantees that no context resource was retained and the request
934    ///   may be retried.
935    /// - **Allocation/copies:** context bookkeeping may be allocated; no buffer content is copied.
936    fn create_context(&self, desc: ContextDesc) -> Result<Self::Context, BackendError>;
937
938    /// Destroy an empty context.
939    ///
940    /// - **Ownership/lifetime:** the handle is consumed and must have no live child resources.
941    /// - **Progress/concurrency:** release may synchronously block, but must not wait for children
942    ///   or in-flight work; no use of this context may overlap the call.
943    /// - **Failure/retry:** [`ReleaseFailure::Rejected`] returns the live handle for retry;
944    ///   [`ReleaseFailure::Indeterminate`] invalidates it and forbids retry.
945    /// - **Allocation/copies:** the call releases provider bookkeeping and copies no content.
946    fn destroy_context(&self, context: Self::Context) -> Result<(), ReleaseFailure<Self::Context>>;
947
948    /// Allocate one exact provider-owned buffer backing.
949    ///
950    /// - **Ownership/lifetime:** `context` is borrowed only for this call. Success returns an owned
951    ///   handle plus metadata for the actual backing; neither may borrow `context`.
952    /// - **Progress/concurrency:** allocation may synchronously block. Independent contexts may be
953    ///   used concurrently only when the concrete backend and handles permit it.
954    /// - **Failure/retry:** `Err` guarantees that no buffer backing was retained and may be retried.
955    /// - **Allocation/copies:** this is the buffer-allocation boundary. Program-visible requests
956    ///   allocate directly bindable backing here or fail; they must not reserve a submission-time
957    ///   bounce allocation or copy buffer content.
958    fn allocate_buffer(
959        &self,
960        context: &Self::Context,
961        desc: BufferDesc,
962    ) -> Result<AllocatedBuffer<Self::Buffer>, BackendError>;
963
964    /// Perform one explicit host-to-buffer transfer.
965    ///
966    /// - **Ownership/lifetime:** `buffer` is exclusively borrowed and `data` is borrowed only for
967    ///   this call. The provider must not retain either reference.
968    /// - **Progress/concurrency:** the call may synchronously block until the explicit transfer is
969    ///   complete. The exclusive buffer borrow prevents overlapping access without forcing
970    ///   interior synchronization; unrelated buffers may progress when concrete types permit it.
971    /// - **Failure/retry:** on `Err`, the requested range may be partially modified but the handle
972    ///   remains live. A later successful full-range write replaces it; device loss is not
973    ///   retryable on the same backend instance.
974    /// - **Allocation/copies:** this is an explicit content-copy boundary. Segmented input should
975    ///   flow into final backing without frame-sized coalescing. Device-local backing may use
976    ///   bounded temporary staging during this call.
977    fn write_buffer(
978        &self,
979        buffer: &mut Self::Buffer,
980        offset: u64,
981        data: &dyn ByteSource,
982    ) -> Result<(), BackendError>;
983    /// Perform one explicit buffer-to-host transfer.
984    ///
985    /// - **Ownership/lifetime:** `buffer` is shared-borrowed and `data` is exclusively borrowed only
986    ///   for this call. The provider must not retain either reference.
987    /// - **Progress/concurrency:** the call may synchronously block until the explicit transfer is
988    ///   complete. Shared reads may overlap only when the concrete buffer is `Sync` and the
989    ///   provider supports that access.
990    /// - **Failure/retry:** `Err` leaves the destination potentially partially initialized; the
991    ///   caller must not publish it. The buffer is unchanged and a complete read may be retried
992    ///   unless the backend is lost. `Ok(())` guarantees every destination byte was initialized.
993    /// - **Allocation/copies:** this is an explicit content-copy boundary. The provider should
994    ///   write directly across segmented destinations; device-local backing may use bounded
995    ///   temporary staging during this call.
996    fn read_buffer(
997        &self,
998        buffer: &Self::Buffer,
999        offset: u64,
1000        data: &mut dyn ByteSink,
1001    ) -> Result<(), BackendError>;
1002
1003    /// Release an unreferenced buffer and its exact backing allocation.
1004    ///
1005    /// - **Ownership/lifetime:** the handle is consumed and must not be bound to an in-flight event.
1006    /// - **Progress/concurrency:** release may synchronously block but must not wait for references
1007    ///   to disappear; no access to this buffer may overlap the call.
1008    /// - **Failure/retry:** rejected release returns the live handle for retry; indeterminate
1009    ///   release invalidates it and requires recovery.
1010    /// - **Allocation/copies:** backing is deallocated without copying its contents or allocating a
1011    ///   replacement.
1012    fn free_buffer(&self, buffer: Self::Buffer) -> Result<(), ReleaseFailure<Self::Buffer>>;
1013
1014    /// Create a resident program from an opaque, possibly segmented artifact.
1015    ///
1016    /// - **Ownership/lifetime:** `context`, `artifact.payload`, and the envelope are borrowed only
1017    ///   for this call. Success returns an owned program with no source borrow.
1018    /// - **Progress/concurrency:** program creation may synchronously block. Independent lifecycle
1019    ///   work may overlap only when the concrete backend and context permit it.
1020    /// - **Failure/retry:** `Err` guarantees that no program resource was retained and may be
1021    ///   retried with a still-live context and artifact.
1022    /// - **Allocation/copies:** resident program storage may be allocated but all storage retained
1023    ///   by the returned handle must fit `artifact.resident_bytes`. Segmented bytes should stream
1024    ///   into final resident storage rather than require one artifact-sized coalescing copy.
1025    fn load_program(
1026        &self,
1027        context: &Self::Context,
1028        artifact: ArtifactRef<'_>,
1029    ) -> Result<Self::Program, BackendError>;
1030
1031    /// Release an unreferenced resident program.
1032    ///
1033    /// - **Ownership/lifetime:** the program is consumed and must not be referenced by an event.
1034    /// - **Progress/concurrency:** release may synchronously block but must not wait for in-flight
1035    ///   references; no use of this program may overlap the call.
1036    /// - **Failure/retry:** rejected release returns the live handle for retry; indeterminate
1037    ///   release invalidates it and requires recovery.
1038    /// - **Allocation/copies:** resident storage is released without copying buffer contents or
1039    ///   allocating replacement state.
1040    fn unload_program(&self, program: Self::Program) -> Result<(), ReleaseFailure<Self::Program>>;
1041
1042    /// Create one accelerator execution queue.
1043    ///
1044    /// - **Ownership/lifetime:** `context` is borrowed only for this call and success returns an
1045    ///   owned queue. All current nonempty queue flags are unsupported.
1046    /// - **Progress/concurrency:** queue setup may synchronously block. Independent creation may
1047    ///   overlap only when concrete types permit it.
1048    /// - **Failure/retry:** `Err` guarantees that no queue resource was retained and may be retried.
1049    /// - **Allocation/copies:** queue bookkeeping may be allocated; no program or buffer content is
1050    ///   copied.
1051    fn create_queue(
1052        &self,
1053        context: &Self::Context,
1054        desc: QueueDesc,
1055    ) -> Result<Self::Queue, BackendError>;
1056
1057    /// Release an unreferenced execution queue.
1058    ///
1059    /// - **Ownership/lifetime:** the queue is consumed and must not be referenced by an event.
1060    /// - **Progress/concurrency:** release may synchronously block but must not wait for submitted
1061    ///   work; no use of this queue may overlap the call.
1062    /// - **Failure/retry:** rejected release returns the live handle for retry; indeterminate
1063    ///   release invalidates it and requires recovery.
1064    /// - **Allocation/copies:** queue state is released without copying buffer content or allocating
1065    ///   replacement state.
1066    fn destroy_queue(&self, queue: Self::Queue) -> Result<(), ReleaseFailure<Self::Queue>>;
1067
1068    /// Attempt to admit one program execution and return its event.
1069    ///
1070    /// Hosts must reject an [`AccessMode`] incompatible with each buffer's [`BufferUsage`] before
1071    /// calling this method (see [`BufferDesc::allows_access`] and
1072    /// [`BindingRef::validate_for_submit`]). Providers may repeat the check as defense in depth, but
1073    /// host-side rejection is required by Wire ABI section 4.4.
1074    ///
1075    /// - **Ownership/lifetime:** queue, program, buffers, and the binding slice are borrowed only
1076    ///   during admission and must not be retained as Rust references. The caller keeps every
1077    ///   referenced handle alive until the returned event is terminal and destroyed.
1078    /// - **Progress/concurrency:** synchronous work is limited to validation and admission; the call
1079    ///   must not wait for execution to finish. Concurrent submission requires concrete `Sync`
1080    ///   handles and provider support; the trait requires no lock or atomic operation by itself.
1081    /// - **Failure/retry:** [`SubmitFailure::Rejected`] guarantees no acceptance and permits retry.
1082    ///   Success or [`SubmitFailure::Indeterminate`] transfers invocation ownership to the event and
1083    ///   must not be retried as though rejected.
1084    /// - **Allocation/copies:** the borrowed slice requires no per-binding box or owned mirror.
1085    ///   Providers may use amortized event storage, but must directly bind each exact allocation and
1086    ///   reject incompatibility instead of allocating or copying through hidden bounce buffers.
1087    fn submit(
1088        &self,
1089        queue: &Self::Queue,
1090        program: &Self::Program,
1091        bindings: &[BindingRef<'_, Self::Buffer>],
1092        timeout: Timeout,
1093    ) -> Result<Self::Event, SubmitFailure<Self::Event>>;
1094
1095    /// Observe event state without blocking or driving an executor.
1096    ///
1097    /// - **Ownership/lifetime:** the event is borrowed only for this call and remains live.
1098    /// - **Progress/concurrency:** polling is bounded, nonblocking, and safe to race with provider
1099    ///   completion when the concrete event is `Sync`.
1100    /// - **Failure/retry:** errors do not make an event terminal; polling may be retried unless the
1101    ///   backend is lost. Once observed, a terminal state is stable across every later success.
1102    /// - **Allocation/copies:** polling allocates no per-call state and copies no bulk content.
1103    fn poll_event(&self, event: &Self::Event) -> Result<EventState, BackendError>;
1104
1105    /// Attempt to make a pending event terminal as [`EventState::Cancelled`].
1106    ///
1107    /// - **Ownership/lifetime:** the event is borrowed only for this call and remains live.
1108    /// - **Progress/concurrency:** cancellation is bounded and nonblocking. It may race with
1109    ///   completion; the provider chooses exactly one terminal result without requiring a lock in
1110    ///   the handle contract.
1111    /// - **Failure/retry:** `Ok(())` means cancellation won. [`BackendError::Busy`] means completion
1112    ///   won and the caller should poll. The default `Unsupported` implementation is conformant only
1113    ///   when [`Capabilities::EVENT_CANCELLATION`] is absent.
1114    /// - **Allocation/copies:** cancellation allocates no per-call state and copies no bulk content.
1115    fn cancel_event(&self, _event: &Self::Event) -> Result<(), BackendError> {
1116        Err(BackendError::Unsupported)
1117    }
1118
1119    /// Release one terminal event and its provider invocation state.
1120    ///
1121    /// - **Ownership/lifetime:** the event is consumed. Every referenced queue, program, and buffer
1122    ///   must remain live until this release succeeds or becomes indeterminate.
1123    /// - **Progress/concurrency:** release may synchronously block but must not wait for a pending
1124    ///   event to finish; no poll or cancellation may overlap this call.
1125    /// - **Failure/retry:** rejected release returns the live event for retry; indeterminate release
1126    ///   invalidates it and requires recovery.
1127    /// - **Allocation/copies:** invocation state is released without copying buffer content or
1128    ///   allocating replacement state.
1129    fn destroy_event(&self, event: Self::Event) -> Result<(), ReleaseFailure<Self::Event>>;
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134    use super::*;
1135
1136    #[derive(Debug)]
1137    struct TransportBytes([u8; 4]);
1138
1139    impl ReadableBytes for TransportBytes {
1140        fn len(&self) -> u64 {
1141            self.0.as_slice().len() as u64
1142        }
1143
1144        fn read_at(&self, offset: u64, target: &mut [u8]) -> Result<(), ByteAccessError> {
1145            let start = usize::try_from(offset).map_err(|_| ByteAccessError::OutOfBounds)?;
1146            let end = start
1147                .checked_add(target.len())
1148                .filter(|end| *end <= self.0.as_slice().len())
1149                .ok_or(ByteAccessError::OutOfBounds)?;
1150            target.copy_from_slice(&self.0[start..end]);
1151            Ok(())
1152        }
1153    }
1154
1155    impl WritableBytes for TransportBytes {
1156        fn len(&self) -> u64 {
1157            self.0.as_slice().len() as u64
1158        }
1159
1160        fn write_at(&mut self, offset: u64, source: &[u8]) -> Result<(), ByteAccessError> {
1161            let start = usize::try_from(offset).map_err(|_| ByteAccessError::OutOfBounds)?;
1162            let end = start
1163                .checked_add(source.len())
1164                .filter(|end| *end <= self.0.as_slice().len())
1165                .ok_or(ByteAccessError::OutOfBounds)?;
1166            self.0[start..end].copy_from_slice(source);
1167            Ok(())
1168        }
1169    }
1170
1171    fn valid_device_info() -> DeviceInfo {
1172        DeviceInfo {
1173            identity: DeviceIdentity {
1174                uuid: [0; 16],
1175                class: AcceleratorClass::OTHER,
1176                vendor_id: 0,
1177                device_id: 0,
1178            },
1179            capabilities: Capabilities::HOST_VISIBLE_MEMORY,
1180            limits: DeviceLimits {
1181                max_contexts: 1,
1182                max_buffers_per_context: 1,
1183                max_programs_per_context: 1,
1184                max_queues_per_context: 1,
1185                max_events_per_context: 1,
1186                max_bindings_per_submission: 1,
1187                max_buffer_bytes: 1,
1188                max_artifact_bytes: 1,
1189            },
1190        }
1191    }
1192
1193    #[test]
1194    fn transport_byte_adapters_preserve_segment_ports_without_copying() {
1195        let mut bytes = TransportBytes(*b"abcd");
1196        let source = TransportByteSource::new(&bytes);
1197        let mut read = [0; 2];
1198        ByteSource::read_at(&source, 1, &mut read).unwrap();
1199        assert_eq!(&read, b"bc");
1200
1201        let mut sink = TransportByteSink::new(&mut bytes);
1202        ByteSink::write_at(&mut sink, 2, b"xy").unwrap();
1203        assert_eq!(&bytes.0, b"abxy");
1204    }
1205
1206    #[test]
1207    fn buffer_descriptors_reject_invalid_alignment() {
1208        assert!(BufferDesc::new(1, 0, MemoryDomain::Host, BufferUsage::empty()).is_err());
1209        assert!(BufferDesc::new(1, 3, MemoryDomain::Host, BufferUsage::TRANSFER_SOURCE).is_err());
1210        assert!(BufferDesc::new(1, 1, MemoryDomain::Host, BufferUsage::empty()).is_err());
1211        assert_eq!(
1212            BufferDesc::new(64, 16, MemoryDomain::Shared, BufferUsage::PROGRAM_INPUT)
1213                .unwrap()
1214                .alignment(),
1215            16
1216        );
1217    }
1218
1219    #[test]
1220    fn buffer_usage_defines_submission_access_compatibility() {
1221        let input =
1222            BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::PROGRAM_INPUT).unwrap();
1223        assert!(input.allows_access(AccessMode::Read));
1224        assert!(!input.allows_access(AccessMode::Write));
1225        assert!(!input.allows_access(AccessMode::ReadWrite));
1226
1227        let output =
1228            BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::PROGRAM_OUTPUT).unwrap();
1229        assert!(!output.allows_access(AccessMode::Read));
1230        assert!(output.allows_access(AccessMode::Write));
1231        assert!(!output.allows_access(AccessMode::ReadWrite));
1232
1233        let mutable =
1234            BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::MUTABLE_STATE).unwrap();
1235        assert!(mutable.allows_access(AccessMode::Read));
1236        assert!(mutable.allows_access(AccessMode::Write));
1237        assert!(mutable.allows_access(AccessMode::ReadWrite));
1238    }
1239
1240    #[test]
1241    fn allocation_properties_reject_hidden_submission_staging() {
1242        let host_input =
1243            BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::PROGRAM_INPUT).unwrap();
1244        assert_eq!(
1245            BufferInfo::new(host_input, 64, 16, BufferProperties::HOST_VISIBLE),
1246            Err(BackendError::Incompatible)
1247        );
1248        assert!(
1249            BufferInfo::new(
1250                host_input,
1251                64,
1252                16,
1253                BufferProperties::HOST_VISIBLE | BufferProperties::DIRECT_BINDING
1254            )
1255            .is_ok()
1256        );
1257
1258        let shared =
1259            BufferDesc::new(64, 16, MemoryDomain::Shared, BufferUsage::TRANSFER_SOURCE).unwrap();
1260        assert_eq!(
1261            BufferInfo::new(shared, 64, 16, BufferProperties::HOST_VISIBLE),
1262            Err(BackendError::Incompatible)
1263        );
1264        assert_eq!(
1265            BufferInfo::new(
1266                shared,
1267                63,
1268                16,
1269                BufferProperties::HOST_VISIBLE | BufferProperties::DIRECT_BINDING
1270            ),
1271            Err(BackendError::Incompatible)
1272        );
1273        assert_eq!(
1274            BufferInfo::new(
1275                shared,
1276                64,
1277                8,
1278                BufferProperties::HOST_VISIBLE | BufferProperties::DIRECT_BINDING
1279            ),
1280            Err(BackendError::Incompatible)
1281        );
1282    }
1283
1284    #[test]
1285    fn capabilities_report_memory_domains_independently() {
1286        let capabilities = Capabilities::HOST_VISIBLE_MEMORY | Capabilities::SHARED_MEMORY;
1287        assert!(capabilities.supports_memory_domain(MemoryDomain::Host));
1288        assert!(capabilities.supports_memory_domain(MemoryDomain::Shared));
1289        assert!(!capabilities.supports_memory_domain(MemoryDomain::Device));
1290    }
1291
1292    #[test]
1293    fn device_information_rejects_unusable_provider_contracts() {
1294        let valid = valid_device_info();
1295        assert_eq!(valid.validate(), Ok(()));
1296
1297        let mut reserved = valid;
1298        reserved.capabilities |= Capabilities::EXTERNAL_MEMORY;
1299        assert_eq!(
1300            reserved.validate(),
1301            Err(DeviceInfoError::ReservedCapabilities)
1302        );
1303
1304        let mut no_memory = valid;
1305        no_memory.capabilities = Capabilities::EVENT_CANCELLATION;
1306        assert_eq!(
1307            no_memory.validate(),
1308            Err(DeviceInfoError::MissingMemoryDomain)
1309        );
1310
1311        let mut zero_limit = valid;
1312        zero_limit.limits.max_bindings_per_submission = 0;
1313        assert_eq!(zero_limit.validate(), Err(DeviceInfoError::ZeroLimit));
1314
1315        let mut unknown = valid;
1316        unknown.capabilities |= Capabilities::from_bits_retain(1 << 63);
1317        assert_eq!(unknown.validate(), Ok(()));
1318    }
1319
1320    #[test]
1321    fn reserved_operations_are_rejected_before_provider_invocation() {
1322        let mut info = valid_device_info();
1323        assert_eq!(info.validate_context_desc(ContextDesc::default()), Ok(()));
1324        assert_eq!(info.validate_queue_desc(QueueDesc::default()), Ok(()));
1325        assert_eq!(
1326            info.validate_context_desc(ContextDesc {
1327                flags: ContextFlags::SECURE,
1328            }),
1329            Err(BackendError::Unsupported)
1330        );
1331        assert_eq!(
1332            info.validate_queue_desc(QueueDesc {
1333                flags: QueueFlags::IN_ORDER,
1334            }),
1335            Err(BackendError::Unsupported)
1336        );
1337        assert_eq!(
1338            info.validate_event_cancellation(),
1339            Err(BackendError::Unsupported)
1340        );
1341
1342        info.capabilities |= Capabilities::EVENT_CANCELLATION;
1343        assert_eq!(info.validate_event_cancellation(), Ok(()));
1344    }
1345
1346    #[test]
1347    fn bindings_are_nonempty_bounded_and_unique() {
1348        let buffer = ();
1349        let range = BufferRange::new(0, 16).unwrap();
1350        let binding = BindingRef {
1351            slot: 3,
1352            buffer: &buffer,
1353            range,
1354            access: AccessMode::Read,
1355        };
1356        assert!(validate_bindings(&[binding], 1).is_ok());
1357
1358        let duplicate = [
1359            BindingRef {
1360                slot: 3,
1361                buffer: &buffer,
1362                range,
1363                access: AccessMode::Read,
1364            },
1365            BindingRef {
1366                slot: 3,
1367                buffer: &buffer,
1368                range,
1369                access: AccessMode::Write,
1370            },
1371        ];
1372        assert_eq!(
1373            validate_bindings(&duplicate, 2),
1374            Err(BackendError::InvalidArgument)
1375        );
1376        assert_eq!(
1377            validate_bindings::<()>(&[], 1),
1378            Err(BackendError::ResourceLimit)
1379        );
1380
1381        let arbitrary_order = [
1382            BindingRef {
1383                slot: 7,
1384                buffer: &buffer,
1385                range,
1386                access: AccessMode::Read,
1387            },
1388            BindingRef {
1389                slot: 2,
1390                buffer: &buffer,
1391                range,
1392                access: AccessMode::Write,
1393            },
1394        ];
1395        assert!(validate_bindings(&arbitrary_order, 2).is_ok());
1396
1397        let canonical_order = [
1398            BindingRef {
1399                slot: 2,
1400                buffer: &buffer,
1401                range,
1402                access: AccessMode::Read,
1403            },
1404            BindingRef {
1405                slot: 7,
1406                buffer: &buffer,
1407                range,
1408                access: AccessMode::Write,
1409            },
1410        ];
1411        assert!(validate_bindings(&canonical_order, 2).is_ok());
1412    }
1413
1414    #[test]
1415    fn binding_access_rejects_usage_mismatch_with_unique_slots() {
1416        let buffer = ();
1417        let range = BufferRange::new(0, 16).unwrap();
1418        let bindings = [BindingRef {
1419            slot: 0,
1420            buffer: &buffer,
1421            range,
1422            access: AccessMode::Write,
1423        }];
1424        let input =
1425            BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::PROGRAM_INPUT).unwrap();
1426        assert!(!input.allows_access(AccessMode::Write));
1427        // Slot-only checks still pass; the usage gate lives on validate_for_submit.
1428        assert!(validate_bindings(&bindings, 1).is_ok());
1429        assert_eq!(
1430            BindingRef::validate_for_submit(&bindings, &[input], 1),
1431            Err(BackendError::PermissionDenied)
1432        );
1433
1434        let read_bindings = [BindingRef {
1435            slot: 0,
1436            buffer: &buffer,
1437            range,
1438            access: AccessMode::Read,
1439        }];
1440        assert!(BindingRef::validate_for_submit(&read_bindings, &[input], 1).is_ok());
1441        assert_eq!(
1442            BindingRef::validate_for_submit(&read_bindings, &[], 1),
1443            Err(BackendError::InvalidArgument)
1444        );
1445    }
1446
1447    #[test]
1448    fn wire_timeouts_are_relative_and_zero_is_infinite() {
1449        assert_eq!(Timeout::from_wire_ns(0), Timeout::Infinite);
1450        assert_eq!(Timeout::from_wire_ns(42).to_wire_ns(), 42);
1451    }
1452}