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}