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 call this before [`Accelerator::submit`]. `descs[i]` must be the
789    /// descriptor for the buffer behind `bindings[i].buffer` (equal length alone is
790    /// not enough). A usage mismatch returns [`BackendError::PermissionDenied`].
791    pub fn validate_for_submit(
792        bindings: &[Self],
793        descs: &[BufferDesc],
794        max_bindings: u32,
795    ) -> Result<(), BackendError> {
796        validate_bindings(bindings, max_bindings)?;
797        if bindings.len() != descs.len() {
798            return Err(BackendError::InvalidArgument);
799        }
800        for (binding, desc) in bindings.iter().zip(descs.iter()) {
801            if !desc.allows_access(binding.access) {
802                return Err(BackendError::PermissionDenied);
803            }
804        }
805        Ok(())
806    }
807}
808
809/// Slot/count uniqueness helper for program bindings.
810///
811/// Structural only: nonempty, bounded by `max_bindings`, and unique slots. This is
812/// **incomplete** for pre-admission checks -- it does not enforce access/usage
813/// compatibility required before backend admission. Prefer
814/// [`BindingRef::validate_for_submit`] before [`Accelerator::submit`].
815pub fn validate_bindings<B>(
816    bindings: &[BindingRef<'_, B>],
817    max_bindings: u32,
818) -> Result<(), BackendError> {
819    if bindings.is_empty() || bindings.len() > max_bindings as usize {
820        return Err(BackendError::ResourceLimit);
821    }
822    for (index, binding) in bindings.iter().enumerate() {
823        if bindings[..index]
824            .iter()
825            .any(|prior| prior.slot == binding.slot)
826        {
827            return Err(BackendError::InvalidArgument);
828        }
829    }
830    Ok(())
831}
832
833#[derive(Clone, Copy, Debug, PartialEq, Eq)]
834pub enum EventState {
835    Pending,
836    Complete,
837    Failed(BackendError),
838    Cancelled,
839}
840
841/// Submission failure that makes the provider acceptance boundary explicit.
842#[derive(Debug)]
843pub enum SubmitFailure<E> {
844    /// The backend guarantees execution was not accepted and no resources were retained.
845    Rejected(BackendError),
846    /// Acceptance is uncertain; the event owns the resources until it reaches a terminal state.
847    Indeterminate { error: BackendError, event: E },
848}
849
850/// Failure to release a provider handle.
851#[derive(Debug)]
852pub enum ReleaseFailure<R> {
853    /// The backend rejected the release and returns the still-live resource for retry.
854    Rejected { error: BackendError, resource: R },
855    /// The resource state is unknown. The adapter must invalidate its ID and request device reset.
856    Indeterminate { error: BackendError },
857}
858
859impl<R> ReleaseFailure<R> {
860    pub const fn error(&self) -> BackendError {
861        match self {
862            Self::Rejected { error, .. } | Self::Indeterminate { error } => *error,
863        }
864    }
865}
866
867/// Native accelerator lifecycle over provider-owned handle types.
868///
869/// The reference command engine is generic over this trait, so its calls are statically dispatched
870/// and native handles need no boxing. The trait imposes no `Send` or `Sync` bounds: a provider may
871/// preserve thread-affine handles, while a provider that opts into those auto traits must make the
872/// corresponding shared calls safe. Callers must not overlap a mutable borrow, a consumed handle,
873/// or destruction with another use of the same resource.
874///
875/// Borrowed arguments are valid only for the duration of a call and must not be retained as Rust
876/// references. Destructive methods consume handles. A caller must reject parent destruction while
877/// child objects or in-flight events still exist; it must not use `Drop` timing as lifecycle state.
878///
879/// The only operations that explicitly transfer buffer contents are [`Self::write_buffer`] and
880/// [`Self::read_buffer`]. Allocation, submission, polling, and release must not hide full-range
881/// staging copies. In particular, `submit` binds the exact provider allocation directly or rejects
882/// it as [`BackendError::Incompatible`].
883///
884/// Dynamic loading, a stable binary interface, and erased cross-boundary handle ownership are not
885/// defined here. An integration that needs dynamic dispatch must fix one concrete handle family in
886/// an adapter without weakening this trait's borrowing, acceptance, or release contracts.
887pub trait Accelerator {
888    /// Owned context handle. It may be a native value and need not be boxed, cloneable, or thread
889    /// safe.
890    type Context;
891    /// Owned handle for the exact allocation described by its accompanying [`BufferInfo`].
892    type Buffer;
893    /// Owned resident-program handle with no borrow of its source artifact.
894    type Program;
895    /// Owned accelerator execution-queue handle.
896    type Queue;
897    /// Owned submission and completion token with no borrow of the submitted binding slice.
898    type Event;
899
900    /// Return immutable identity, capability, and limit metadata.
901    ///
902    /// - **Ownership/lifetime:** no ownership changes; a successful value must remain stable for
903    ///   the lifetime of this backend instance.
904    /// - **Progress/concurrency:** discovery may perform bounded synchronous provider work but must
905    ///   not wait for resource progress. Concurrent calls are permitted only when the concrete
906    ///   backend is `Sync`.
907    /// - **Failure/retry:** an error creates no resource and may be retried; callers validate and
908    ///   cache the first successful result before invoking resource methods.
909    /// - **Allocation/copies:** the call must not allocate resource backing or copy bulk content.
910    fn device_info(&self) -> Result<DeviceInfo, BackendError>;
911
912    /// Create one context from prevalidated intent.
913    ///
914    /// - **Ownership/lifetime:** `desc` is consumed by value and not retained by reference; success
915    ///   returns one owned context. All current nonempty context flags are unsupported.
916    /// - **Progress/concurrency:** provider setup may synchronously block, but must not wait for
917    ///   unrelated resource progress. Independent creation may overlap only when concrete types
918    ///   permit it.
919    /// - **Failure/retry:** `Err` guarantees that no context resource was retained and the request
920    ///   may be retried.
921    /// - **Allocation/copies:** context bookkeeping may be allocated; no buffer content is copied.
922    fn create_context(&self, desc: ContextDesc) -> Result<Self::Context, BackendError>;
923
924    /// Destroy an empty context.
925    ///
926    /// - **Ownership/lifetime:** the handle is consumed and must have no live child resources.
927    /// - **Progress/concurrency:** release may synchronously block, but must not wait for children
928    ///   or in-flight work; no use of this context may overlap the call.
929    /// - **Failure/retry:** [`ReleaseFailure::Rejected`] returns the live handle for retry;
930    ///   [`ReleaseFailure::Indeterminate`] invalidates it and forbids retry.
931    /// - **Allocation/copies:** the call releases provider bookkeeping and copies no content.
932    fn destroy_context(&self, context: Self::Context) -> Result<(), ReleaseFailure<Self::Context>>;
933
934    /// Allocate one exact provider-owned buffer backing.
935    ///
936    /// - **Ownership/lifetime:** `context` is borrowed only for this call. Success returns an owned
937    ///   handle plus metadata for the actual backing; neither may borrow `context`.
938    /// - **Progress/concurrency:** allocation may synchronously block. Independent contexts may be
939    ///   used concurrently only when the concrete backend and handles permit it.
940    /// - **Failure/retry:** `Err` guarantees that no buffer backing was retained and may be retried.
941    /// - **Allocation/copies:** this is the buffer-allocation boundary. Program-visible requests
942    ///   allocate directly bindable backing here or fail; they must not reserve a submission-time
943    ///   bounce allocation or copy buffer content.
944    fn allocate_buffer(
945        &self,
946        context: &Self::Context,
947        desc: BufferDesc,
948    ) -> Result<AllocatedBuffer<Self::Buffer>, BackendError>;
949
950    /// Perform one explicit host-to-buffer transfer.
951    ///
952    /// - **Ownership/lifetime:** `buffer` is exclusively borrowed and `data` is borrowed only for
953    ///   this call. The provider must not retain either reference.
954    /// - **Progress/concurrency:** the call may synchronously block until the explicit transfer is
955    ///   complete. The exclusive buffer borrow prevents overlapping access without forcing
956    ///   interior synchronization; unrelated buffers may progress when concrete types permit it.
957    /// - **Failure/retry:** on `Err`, the requested range may be partially modified but the handle
958    ///   remains live. A later successful full-range write replaces it; device loss is not
959    ///   retryable on the same backend instance.
960    /// - **Allocation/copies:** this is an explicit content-copy boundary. Segmented input should
961    ///   flow into final backing without frame-sized coalescing. Device-local backing may use
962    ///   bounded temporary staging during this call.
963    fn write_buffer(
964        &self,
965        buffer: &mut Self::Buffer,
966        offset: u64,
967        data: &dyn ByteSource,
968    ) -> Result<(), BackendError>;
969    /// Perform one explicit buffer-to-host transfer.
970    ///
971    /// - **Ownership/lifetime:** `buffer` is shared-borrowed and `data` is exclusively borrowed only
972    ///   for this call. The provider must not retain either reference.
973    /// - **Progress/concurrency:** the call may synchronously block until the explicit transfer is
974    ///   complete. Shared reads may overlap only when the concrete buffer is `Sync` and the
975    ///   provider supports that access.
976    /// - **Failure/retry:** `Err` leaves the destination potentially partially initialized; the
977    ///   caller must not publish it. The buffer is unchanged and a complete read may be retried
978    ///   unless the backend is lost. `Ok(())` guarantees every destination byte was initialized.
979    /// - **Allocation/copies:** this is an explicit content-copy boundary. The provider should
980    ///   write directly across segmented destinations; device-local backing may use bounded
981    ///   temporary staging during this call.
982    fn read_buffer(
983        &self,
984        buffer: &Self::Buffer,
985        offset: u64,
986        data: &mut dyn ByteSink,
987    ) -> Result<(), BackendError>;
988
989    /// Release an unreferenced buffer and its exact backing allocation.
990    ///
991    /// - **Ownership/lifetime:** the handle is consumed and must not be bound to an in-flight event.
992    /// - **Progress/concurrency:** release may synchronously block but must not wait for references
993    ///   to disappear; no access to this buffer may overlap the call.
994    /// - **Failure/retry:** rejected release returns the live handle for retry; indeterminate
995    ///   release invalidates it and requires recovery.
996    /// - **Allocation/copies:** backing is deallocated without copying its contents or allocating a
997    ///   replacement.
998    fn free_buffer(&self, buffer: Self::Buffer) -> Result<(), ReleaseFailure<Self::Buffer>>;
999
1000    /// Create a resident program from an opaque, possibly segmented artifact.
1001    ///
1002    /// - **Ownership/lifetime:** `context`, `artifact.payload`, and the envelope are borrowed only
1003    ///   for this call. Success returns an owned program with no source borrow.
1004    /// - **Progress/concurrency:** program creation may synchronously block. Independent lifecycle
1005    ///   work may overlap only when the concrete backend and context permit it.
1006    /// - **Failure/retry:** `Err` guarantees that no program resource was retained and may be
1007    ///   retried with a still-live context and artifact.
1008    /// - **Allocation/copies:** resident program storage may be allocated but all storage retained
1009    ///   by the returned handle must fit `artifact.resident_bytes`. Segmented bytes should stream
1010    ///   into final resident storage rather than require one artifact-sized coalescing copy.
1011    fn load_program(
1012        &self,
1013        context: &Self::Context,
1014        artifact: ArtifactRef<'_>,
1015    ) -> Result<Self::Program, BackendError>;
1016
1017    /// Release an unreferenced resident program.
1018    ///
1019    /// - **Ownership/lifetime:** the program is consumed and must not be referenced by an event.
1020    /// - **Progress/concurrency:** release may synchronously block but must not wait for in-flight
1021    ///   references; no use of this program may overlap the call.
1022    /// - **Failure/retry:** rejected release returns the live handle for retry; indeterminate
1023    ///   release invalidates it and requires recovery.
1024    /// - **Allocation/copies:** resident storage is released without copying buffer contents or
1025    ///   allocating replacement state.
1026    fn unload_program(&self, program: Self::Program) -> Result<(), ReleaseFailure<Self::Program>>;
1027
1028    /// Create one accelerator execution queue.
1029    ///
1030    /// - **Ownership/lifetime:** `context` is borrowed only for this call and success returns an
1031    ///   owned queue. All current nonempty queue flags are unsupported.
1032    /// - **Progress/concurrency:** queue setup may synchronously block. Independent creation may
1033    ///   overlap only when concrete types permit it.
1034    /// - **Failure/retry:** `Err` guarantees that no queue resource was retained and may be retried.
1035    /// - **Allocation/copies:** queue bookkeeping may be allocated; no program or buffer content is
1036    ///   copied.
1037    fn create_queue(
1038        &self,
1039        context: &Self::Context,
1040        desc: QueueDesc,
1041    ) -> Result<Self::Queue, BackendError>;
1042
1043    /// Release an unreferenced execution queue.
1044    ///
1045    /// - **Ownership/lifetime:** the queue is consumed and must not be referenced by an event.
1046    /// - **Progress/concurrency:** release may synchronously block but must not wait for submitted
1047    ///   work; no use of this queue may overlap the call.
1048    /// - **Failure/retry:** rejected release returns the live handle for retry; indeterminate
1049    ///   release invalidates it and requires recovery.
1050    /// - **Allocation/copies:** queue state is released without copying buffer content or allocating
1051    ///   replacement state.
1052    fn destroy_queue(&self, queue: Self::Queue) -> Result<(), ReleaseFailure<Self::Queue>>;
1053
1054    /// Attempt to admit one program execution and return its event.
1055    ///
1056    /// Hosts must reject an [`AccessMode`] incompatible with each buffer's [`BufferUsage`] before
1057    /// calling this method (see [`BindingRef::validate_for_submit`]). Providers may repeat the check
1058    /// as defense in depth, but host-side rejection is required by Wire ABI section 4.4.
1059    ///
1060    /// - **Ownership/lifetime:** queue, program, buffers, and the binding slice are borrowed only
1061    ///   during admission and must not be retained as Rust references. The caller keeps every
1062    ///   referenced handle alive until the returned event is terminal and destroyed.
1063    /// - **Progress/concurrency:** synchronous work is limited to validation and admission; the call
1064    ///   must not wait for execution to finish. Concurrent submission requires concrete `Sync`
1065    ///   handles and provider support; the trait requires no lock or atomic operation by itself.
1066    /// - **Failure/retry:** [`SubmitFailure::Rejected`] guarantees no acceptance and permits retry.
1067    ///   Success or [`SubmitFailure::Indeterminate`] transfers invocation ownership to the event and
1068    ///   must not be retried as though rejected.
1069    /// - **Allocation/copies:** the borrowed slice requires no per-binding box or owned mirror.
1070    ///   Providers may use amortized event storage, but must directly bind each exact allocation and
1071    ///   reject incompatibility instead of allocating or copying through hidden bounce buffers.
1072    fn submit(
1073        &self,
1074        queue: &Self::Queue,
1075        program: &Self::Program,
1076        bindings: &[BindingRef<'_, Self::Buffer>],
1077        timeout: Timeout,
1078    ) -> Result<Self::Event, SubmitFailure<Self::Event>>;
1079
1080    /// Observe event state without blocking or driving an executor.
1081    ///
1082    /// - **Ownership/lifetime:** the event is borrowed only for this call and remains live.
1083    /// - **Progress/concurrency:** polling is bounded, nonblocking, and safe to race with provider
1084    ///   completion when the concrete event is `Sync`.
1085    /// - **Failure/retry:** errors do not make an event terminal; polling may be retried unless the
1086    ///   backend is lost. Once observed, a terminal state is stable across every later success.
1087    /// - **Allocation/copies:** polling allocates no per-call state and copies no bulk content.
1088    fn poll_event(&self, event: &Self::Event) -> Result<EventState, BackendError>;
1089
1090    /// Attempt to make a pending event terminal as [`EventState::Cancelled`].
1091    ///
1092    /// - **Ownership/lifetime:** the event is borrowed only for this call and remains live.
1093    /// - **Progress/concurrency:** cancellation is bounded and nonblocking. It may race with
1094    ///   completion; the provider chooses exactly one terminal result without requiring a lock in
1095    ///   the handle contract.
1096    /// - **Failure/retry:** `Ok(())` means cancellation won. [`BackendError::Busy`] means completion
1097    ///   won and the caller should poll. The default `Unsupported` implementation is conformant only
1098    ///   when [`Capabilities::EVENT_CANCELLATION`] is absent.
1099    /// - **Allocation/copies:** cancellation allocates no per-call state and copies no bulk content.
1100    fn cancel_event(&self, _event: &Self::Event) -> Result<(), BackendError> {
1101        Err(BackendError::Unsupported)
1102    }
1103
1104    /// Release one terminal event and its provider invocation state.
1105    ///
1106    /// - **Ownership/lifetime:** the event is consumed. Every referenced queue, program, and buffer
1107    ///   must remain live until this release succeeds or becomes indeterminate.
1108    /// - **Progress/concurrency:** release may synchronously block but must not wait for a pending
1109    ///   event to finish; no poll or cancellation may overlap this call.
1110    /// - **Failure/retry:** rejected release returns the live event for retry; indeterminate release
1111    ///   invalidates it and requires recovery.
1112    /// - **Allocation/copies:** invocation state is released without copying buffer content or
1113    ///   allocating replacement state.
1114    fn destroy_event(&self, event: Self::Event) -> Result<(), ReleaseFailure<Self::Event>>;
1115}
1116
1117#[cfg(test)]
1118mod tests {
1119    use super::*;
1120
1121    #[derive(Debug)]
1122    struct TransportBytes([u8; 4]);
1123
1124    impl ReadableBytes for TransportBytes {
1125        fn len(&self) -> u64 {
1126            self.0.as_slice().len() as u64
1127        }
1128
1129        fn read_at(&self, offset: u64, target: &mut [u8]) -> Result<(), ByteAccessError> {
1130            let start = usize::try_from(offset).map_err(|_| ByteAccessError::OutOfBounds)?;
1131            let end = start
1132                .checked_add(target.len())
1133                .filter(|end| *end <= self.0.as_slice().len())
1134                .ok_or(ByteAccessError::OutOfBounds)?;
1135            target.copy_from_slice(&self.0[start..end]);
1136            Ok(())
1137        }
1138    }
1139
1140    impl WritableBytes for TransportBytes {
1141        fn len(&self) -> u64 {
1142            self.0.as_slice().len() as u64
1143        }
1144
1145        fn write_at(&mut self, offset: u64, source: &[u8]) -> Result<(), ByteAccessError> {
1146            let start = usize::try_from(offset).map_err(|_| ByteAccessError::OutOfBounds)?;
1147            let end = start
1148                .checked_add(source.len())
1149                .filter(|end| *end <= self.0.as_slice().len())
1150                .ok_or(ByteAccessError::OutOfBounds)?;
1151            self.0[start..end].copy_from_slice(source);
1152            Ok(())
1153        }
1154    }
1155
1156    fn valid_device_info() -> DeviceInfo {
1157        DeviceInfo {
1158            identity: DeviceIdentity {
1159                uuid: [0; 16],
1160                class: AcceleratorClass::OTHER,
1161                vendor_id: 0,
1162                device_id: 0,
1163            },
1164            capabilities: Capabilities::HOST_VISIBLE_MEMORY,
1165            limits: DeviceLimits {
1166                max_contexts: 1,
1167                max_buffers_per_context: 1,
1168                max_programs_per_context: 1,
1169                max_queues_per_context: 1,
1170                max_events_per_context: 1,
1171                max_bindings_per_submission: 1,
1172                max_buffer_bytes: 1,
1173                max_artifact_bytes: 1,
1174            },
1175        }
1176    }
1177
1178    #[test]
1179    fn transport_byte_adapters_preserve_segment_ports_without_copying() {
1180        let mut bytes = TransportBytes(*b"abcd");
1181        let source = TransportByteSource::new(&bytes);
1182        let mut read = [0; 2];
1183        ByteSource::read_at(&source, 1, &mut read).unwrap();
1184        assert_eq!(&read, b"bc");
1185
1186        let mut sink = TransportByteSink::new(&mut bytes);
1187        ByteSink::write_at(&mut sink, 2, b"xy").unwrap();
1188        assert_eq!(&bytes.0, b"abxy");
1189    }
1190
1191    #[test]
1192    fn buffer_descriptors_reject_invalid_alignment() {
1193        assert!(BufferDesc::new(1, 0, MemoryDomain::Host, BufferUsage::empty()).is_err());
1194        assert!(BufferDesc::new(1, 3, MemoryDomain::Host, BufferUsage::TRANSFER_SOURCE).is_err());
1195        assert!(BufferDesc::new(1, 1, MemoryDomain::Host, BufferUsage::empty()).is_err());
1196        assert_eq!(
1197            BufferDesc::new(64, 16, MemoryDomain::Shared, BufferUsage::PROGRAM_INPUT)
1198                .unwrap()
1199                .alignment(),
1200            16
1201        );
1202    }
1203
1204    #[test]
1205    fn buffer_usage_defines_submission_access_compatibility() {
1206        let input =
1207            BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::PROGRAM_INPUT).unwrap();
1208        assert!(input.allows_access(AccessMode::Read));
1209        assert!(!input.allows_access(AccessMode::Write));
1210        assert!(!input.allows_access(AccessMode::ReadWrite));
1211
1212        let output =
1213            BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::PROGRAM_OUTPUT).unwrap();
1214        assert!(!output.allows_access(AccessMode::Read));
1215        assert!(output.allows_access(AccessMode::Write));
1216        assert!(!output.allows_access(AccessMode::ReadWrite));
1217
1218        let mutable =
1219            BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::MUTABLE_STATE).unwrap();
1220        assert!(mutable.allows_access(AccessMode::Read));
1221        assert!(mutable.allows_access(AccessMode::Write));
1222        assert!(mutable.allows_access(AccessMode::ReadWrite));
1223    }
1224
1225    #[test]
1226    fn allocation_properties_reject_hidden_submission_staging() {
1227        let host_input =
1228            BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::PROGRAM_INPUT).unwrap();
1229        assert_eq!(
1230            BufferInfo::new(host_input, 64, 16, BufferProperties::HOST_VISIBLE),
1231            Err(BackendError::Incompatible)
1232        );
1233        assert!(
1234            BufferInfo::new(
1235                host_input,
1236                64,
1237                16,
1238                BufferProperties::HOST_VISIBLE | BufferProperties::DIRECT_BINDING
1239            )
1240            .is_ok()
1241        );
1242
1243        let shared =
1244            BufferDesc::new(64, 16, MemoryDomain::Shared, BufferUsage::TRANSFER_SOURCE).unwrap();
1245        assert_eq!(
1246            BufferInfo::new(shared, 64, 16, BufferProperties::HOST_VISIBLE),
1247            Err(BackendError::Incompatible)
1248        );
1249        assert_eq!(
1250            BufferInfo::new(
1251                shared,
1252                63,
1253                16,
1254                BufferProperties::HOST_VISIBLE | BufferProperties::DIRECT_BINDING
1255            ),
1256            Err(BackendError::Incompatible)
1257        );
1258        assert_eq!(
1259            BufferInfo::new(
1260                shared,
1261                64,
1262                8,
1263                BufferProperties::HOST_VISIBLE | BufferProperties::DIRECT_BINDING
1264            ),
1265            Err(BackendError::Incompatible)
1266        );
1267    }
1268
1269    #[test]
1270    fn capabilities_report_memory_domains_independently() {
1271        let capabilities = Capabilities::HOST_VISIBLE_MEMORY | Capabilities::SHARED_MEMORY;
1272        assert!(capabilities.supports_memory_domain(MemoryDomain::Host));
1273        assert!(capabilities.supports_memory_domain(MemoryDomain::Shared));
1274        assert!(!capabilities.supports_memory_domain(MemoryDomain::Device));
1275    }
1276
1277    #[test]
1278    fn device_information_rejects_unusable_provider_contracts() {
1279        let valid = valid_device_info();
1280        assert_eq!(valid.validate(), Ok(()));
1281
1282        let mut reserved = valid;
1283        reserved.capabilities |= Capabilities::EXTERNAL_MEMORY;
1284        assert_eq!(
1285            reserved.validate(),
1286            Err(DeviceInfoError::ReservedCapabilities)
1287        );
1288
1289        let mut no_memory = valid;
1290        no_memory.capabilities = Capabilities::EVENT_CANCELLATION;
1291        assert_eq!(
1292            no_memory.validate(),
1293            Err(DeviceInfoError::MissingMemoryDomain)
1294        );
1295
1296        let mut zero_limit = valid;
1297        zero_limit.limits.max_bindings_per_submission = 0;
1298        assert_eq!(zero_limit.validate(), Err(DeviceInfoError::ZeroLimit));
1299
1300        let mut unknown = valid;
1301        unknown.capabilities |= Capabilities::from_bits_retain(1 << 63);
1302        assert_eq!(unknown.validate(), Ok(()));
1303    }
1304
1305    #[test]
1306    fn reserved_operations_are_rejected_before_provider_invocation() {
1307        let mut info = valid_device_info();
1308        assert_eq!(info.validate_context_desc(ContextDesc::default()), Ok(()));
1309        assert_eq!(info.validate_queue_desc(QueueDesc::default()), Ok(()));
1310        assert_eq!(
1311            info.validate_context_desc(ContextDesc {
1312                flags: ContextFlags::SECURE,
1313            }),
1314            Err(BackendError::Unsupported)
1315        );
1316        assert_eq!(
1317            info.validate_queue_desc(QueueDesc {
1318                flags: QueueFlags::IN_ORDER,
1319            }),
1320            Err(BackendError::Unsupported)
1321        );
1322        assert_eq!(
1323            info.validate_event_cancellation(),
1324            Err(BackendError::Unsupported)
1325        );
1326
1327        info.capabilities |= Capabilities::EVENT_CANCELLATION;
1328        assert_eq!(info.validate_event_cancellation(), Ok(()));
1329    }
1330
1331    #[test]
1332    fn bindings_are_nonempty_bounded_and_unique() {
1333        let buffer = ();
1334        let range = BufferRange::new(0, 16).unwrap();
1335        let binding = BindingRef {
1336            slot: 3,
1337            buffer: &buffer,
1338            range,
1339            access: AccessMode::Read,
1340        };
1341        assert!(validate_bindings(&[binding], 1).is_ok());
1342
1343        let duplicate = [
1344            BindingRef {
1345                slot: 3,
1346                buffer: &buffer,
1347                range,
1348                access: AccessMode::Read,
1349            },
1350            BindingRef {
1351                slot: 3,
1352                buffer: &buffer,
1353                range,
1354                access: AccessMode::Write,
1355            },
1356        ];
1357        assert_eq!(
1358            validate_bindings(&duplicate, 2),
1359            Err(BackendError::InvalidArgument)
1360        );
1361        assert_eq!(
1362            validate_bindings::<()>(&[], 1),
1363            Err(BackendError::ResourceLimit)
1364        );
1365    }
1366
1367    #[test]
1368    fn binding_access_rejects_usage_mismatch_with_unique_slots() {
1369        let buffer = ();
1370        let range = BufferRange::new(0, 16).unwrap();
1371        let bindings = [BindingRef {
1372            slot: 0,
1373            buffer: &buffer,
1374            range,
1375            access: AccessMode::Write,
1376        }];
1377        let input =
1378            BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::PROGRAM_INPUT).unwrap();
1379        assert!(!input.allows_access(AccessMode::Write));
1380        // Slot-only checks still pass; the usage gate lives on validate_for_submit.
1381        assert!(validate_bindings(&bindings, 1).is_ok());
1382        assert_eq!(
1383            BindingRef::validate_for_submit(&bindings, &[input], 1),
1384            Err(BackendError::PermissionDenied)
1385        );
1386
1387        let read_bindings = [BindingRef {
1388            slot: 0,
1389            buffer: &buffer,
1390            range,
1391            access: AccessMode::Read,
1392        }];
1393        assert!(BindingRef::validate_for_submit(&read_bindings, &[input], 1).is_ok());
1394        assert_eq!(
1395            BindingRef::validate_for_submit(&read_bindings, &[], 1),
1396            Err(BackendError::InvalidArgument)
1397        );
1398    }
1399
1400    #[test]
1401    fn wire_timeouts_are_relative_and_zero_is_infinite() {
1402        assert_eq!(Timeout::from_wire_ns(0), Timeout::Infinite);
1403        assert_eq!(Timeout::from_wire_ns(42).to_wire_ns(), 42);
1404    }
1405}