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