#![no_std]
#![forbid(unsafe_code)]
use bitflags::bitflags;
use core::fmt;
use core::num::{NonZeroU32, NonZeroU64};
use virtio_accel_transport::{ByteAccessError, ReadableBytes, WritableBytes};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BackendError {
Unsupported,
Incompatible,
InvalidArgument,
OutOfBounds,
Busy,
OutOfMemory,
ResourceLimit,
DeadlineExpired,
DeviceLost,
PermissionDenied,
External {
domain: u32,
code: i64,
},
}
impl fmt::Display for BackendError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{self:?}")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(transparent)]
pub struct AcceleratorClass(u16);
impl AcceleratorClass {
pub const OTHER: Self = Self(0);
pub const NPU: Self = Self(1);
pub const GPU: Self = Self(2);
pub const DSP: Self = Self(3);
pub const fn new(value: u16) -> Self {
Self(value)
}
pub const fn get(self) -> u16 {
self.0
}
}
bitflags! {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Capabilities: u64 {
const HOST_VISIBLE_MEMORY = 1 << 0;
const DEVICE_LOCAL_MEMORY = 1 << 1;
const EVENT_CANCELLATION = 1 << 2;
const EXTERNAL_MEMORY = 1 << 3;
const SECURE_CONTEXTS = 1 << 4;
const SHARED_MEMORY = 1 << 5;
const MEMORY_DOMAINS = Self::HOST_VISIBLE_MEMORY.bits()
| Self::DEVICE_LOCAL_MEMORY.bits()
| Self::SHARED_MEMORY.bits();
const RESERVED = Self::EXTERNAL_MEMORY.bits() | Self::SECURE_CONTEXTS.bits();
}
}
impl Capabilities {
pub const fn supports_memory_domain(self, domain: MemoryDomain) -> bool {
match domain {
MemoryDomain::Host => self.contains(Self::HOST_VISIBLE_MEMORY),
MemoryDomain::Device => self.contains(Self::DEVICE_LOCAL_MEMORY),
MemoryDomain::Shared => self.contains(Self::SHARED_MEMORY),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DeviceIdentity {
pub uuid: [u8; 16],
pub class: AcceleratorClass,
pub vendor_id: u32,
pub device_id: u32,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DeviceLimits {
pub max_contexts: u32,
pub max_buffers_per_context: u32,
pub max_programs_per_context: u32,
pub max_queues_per_context: u32,
pub max_events_per_context: u32,
pub max_bindings_per_submission: u32,
pub max_buffer_bytes: u64,
pub max_artifact_bytes: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DeviceInfo {
pub identity: DeviceIdentity,
pub capabilities: Capabilities,
pub limits: DeviceLimits,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DeviceInfoError {
ReservedCapabilities,
MissingMemoryDomain,
ZeroLimit,
}
bitflags! {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ContextFlags: u32 {
const SECURE = 1 << 0;
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ContextDesc {
pub flags: ContextFlags,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum MemoryDomain {
Host = 1,
Device = 2,
Shared = 3,
}
impl TryFrom<u8> for MemoryDomain {
type Error = BackendError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(Self::Host),
2 => Ok(Self::Device),
3 => Ok(Self::Shared),
_ => Err(BackendError::InvalidArgument),
}
}
}
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BufferUsage: u32 {
const TRANSFER_SOURCE = 1 << 0;
const TRANSFER_DESTINATION = 1 << 1;
const PROGRAM_INPUT = 1 << 2;
const PROGRAM_OUTPUT = 1 << 3;
const MUTABLE_STATE = 1 << 4;
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BufferDesc {
bytes: NonZeroU64,
alignment: NonZeroU64,
pub domain: MemoryDomain,
pub usage: BufferUsage,
}
impl BufferDesc {
pub fn new(
bytes: u64,
alignment: u64,
domain: MemoryDomain,
usage: BufferUsage,
) -> Result<Self, BackendError> {
let bytes = NonZeroU64::new(bytes).ok_or(BackendError::InvalidArgument)?;
let alignment = NonZeroU64::new(alignment).ok_or(BackendError::InvalidArgument)?;
if !alignment.get().is_power_of_two()
|| usage.is_empty()
|| !BufferUsage::all().contains(usage)
{
return Err(BackendError::InvalidArgument);
}
Ok(Self {
bytes,
alignment,
domain,
usage,
})
}
pub const fn bytes(self) -> u64 {
self.bytes.get()
}
pub const fn alignment(self) -> u64 {
self.alignment.get()
}
pub const fn allows_access(self, access: AccessMode) -> bool {
match access {
AccessMode::Read => self
.usage
.intersects(BufferUsage::PROGRAM_INPUT.union(BufferUsage::MUTABLE_STATE)),
AccessMode::Write => self
.usage
.intersects(BufferUsage::PROGRAM_OUTPUT.union(BufferUsage::MUTABLE_STATE)),
AccessMode::ReadWrite => self.usage.contains(BufferUsage::MUTABLE_STATE),
}
}
pub const fn is_program_visible(self) -> bool {
self.usage.intersects(
BufferUsage::PROGRAM_INPUT
.union(BufferUsage::PROGRAM_OUTPUT)
.union(BufferUsage::MUTABLE_STATE),
)
}
}
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BufferProperties: u32 {
const HOST_VISIBLE = 1 << 0;
const DEVICE_LOCAL = 1 << 1;
const DIRECT_BINDING = 1 << 2;
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BufferInfo {
desc: BufferDesc,
allocation_bytes: NonZeroU64,
alignment: NonZeroU64,
properties: BufferProperties,
}
impl BufferInfo {
pub fn new(
desc: BufferDesc,
allocation_bytes: u64,
alignment: u64,
properties: BufferProperties,
) -> Result<Self, BackendError> {
let allocation_bytes =
NonZeroU64::new(allocation_bytes).ok_or(BackendError::InvalidArgument)?;
let alignment = NonZeroU64::new(alignment).ok_or(BackendError::InvalidArgument)?;
if !BufferProperties::all().contains(properties) {
return Err(BackendError::InvalidArgument);
}
if allocation_bytes.get() < desc.bytes()
|| !alignment.get().is_power_of_two()
|| alignment.get() < desc.alignment()
{
return Err(BackendError::Incompatible);
}
let required = match desc.domain {
MemoryDomain::Host => BufferProperties::HOST_VISIBLE,
MemoryDomain::Device => BufferProperties::DEVICE_LOCAL,
MemoryDomain::Shared => {
BufferProperties::HOST_VISIBLE.union(BufferProperties::DIRECT_BINDING)
}
};
if !properties.contains(required)
|| (desc.is_program_visible() && !properties.contains(BufferProperties::DIRECT_BINDING))
{
return Err(BackendError::Incompatible);
}
Ok(Self {
desc,
allocation_bytes,
alignment,
properties,
})
}
pub const fn desc(self) -> BufferDesc {
self.desc
}
pub const fn allocation_bytes(self) -> u64 {
self.allocation_bytes.get()
}
pub const fn alignment(self) -> u64 {
self.alignment.get()
}
pub const fn properties(self) -> BufferProperties {
self.properties
}
}
impl DeviceInfo {
pub const fn validate(self) -> Result<(), DeviceInfoError> {
if self.capabilities.intersects(Capabilities::RESERVED) {
return Err(DeviceInfoError::ReservedCapabilities);
}
if !self.capabilities.intersects(Capabilities::MEMORY_DOMAINS) {
return Err(DeviceInfoError::MissingMemoryDomain);
}
if self.limits.max_contexts == 0
|| self.limits.max_buffers_per_context == 0
|| self.limits.max_programs_per_context == 0
|| self.limits.max_queues_per_context == 0
|| self.limits.max_events_per_context == 0
|| self.limits.max_bindings_per_submission == 0
|| self.limits.max_buffer_bytes == 0
|| self.limits.max_artifact_bytes == 0
{
return Err(DeviceInfoError::ZeroLimit);
}
Ok(())
}
pub fn validate_context_desc(self, desc: ContextDesc) -> Result<(), BackendError> {
if desc.flags.is_empty() {
Ok(())
} else {
Err(BackendError::Unsupported)
}
}
pub fn validate_buffer_desc(self, desc: BufferDesc) -> Result<(), BackendError> {
if desc.bytes() > self.limits.max_buffer_bytes {
return Err(BackendError::ResourceLimit);
}
if !self.capabilities.supports_memory_domain(desc.domain) {
return Err(BackendError::Unsupported);
}
Ok(())
}
pub fn validate_buffer_info(
self,
requested: BufferDesc,
actual: BufferInfo,
) -> Result<(), BackendError> {
self.validate_buffer_desc(requested)?;
if actual.desc() != requested {
return Err(BackendError::Incompatible);
}
Ok(())
}
pub fn validate_queue_desc(self, desc: QueueDesc) -> Result<(), BackendError> {
if desc.flags.is_empty() {
Ok(())
} else {
Err(BackendError::Unsupported)
}
}
pub fn validate_event_cancellation(self) -> Result<(), BackendError> {
if self.capabilities.contains(Capabilities::EVENT_CANCELLATION) {
Ok(())
} else {
Err(BackendError::Unsupported)
}
}
}
#[derive(Debug)]
pub struct AllocatedBuffer<B> {
buffer: B,
info: BufferInfo,
}
impl<B> AllocatedBuffer<B> {
pub const fn new(buffer: B, info: BufferInfo) -> Self {
Self { buffer, info }
}
pub const fn buffer(&self) -> &B {
&self.buffer
}
pub fn buffer_mut(&mut self) -> &mut B {
&mut self.buffer
}
pub const fn info(&self) -> BufferInfo {
self.info
}
pub fn into_parts(self) -> (B, BufferInfo) {
(self.buffer, self.info)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BufferRange {
pub offset: u64,
bytes: NonZeroU64,
}
impl BufferRange {
pub fn new(offset: u64, bytes: u64) -> Result<Self, BackendError> {
let bytes = NonZeroU64::new(bytes).ok_or(BackendError::InvalidArgument)?;
offset
.checked_add(bytes.get())
.ok_or(BackendError::OutOfBounds)?;
Ok(Self { offset, bytes })
}
pub const fn bytes(self) -> u64 {
self.bytes.get()
}
pub const fn end(self) -> u64 {
self.offset + self.bytes.get()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum AccessMode {
Read = 1,
Write = 2,
ReadWrite = 3,
}
impl TryFrom<u8> for AccessMode {
type Error = BackendError;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
1 => Ok(Self::Read),
2 => Ok(Self::Write),
3 => Ok(Self::ReadWrite),
_ => Err(BackendError::InvalidArgument),
}
}
}
pub trait ByteSource: fmt::Debug {
fn len(&self) -> u64;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn read_at(&self, offset: u64, target: &mut [u8]) -> Result<(), BackendError>;
fn as_contiguous(&self) -> Option<&[u8]> {
None
}
}
#[derive(Debug)]
pub struct TransportByteSource<'a, T: ?Sized>(&'a T);
impl<'a, T: ?Sized> TransportByteSource<'a, T> {
pub const fn new(source: &'a T) -> Self {
Self(source)
}
pub const fn into_inner(self) -> &'a T {
self.0
}
}
impl<T: ReadableBytes + ?Sized> ByteSource for TransportByteSource<'_, T> {
fn len(&self) -> u64 {
ReadableBytes::len(self.0)
}
fn read_at(&self, offset: u64, target: &mut [u8]) -> Result<(), BackendError> {
ReadableBytes::read_at(self.0, offset, target).map_err(backend_error_from_byte_access)
}
fn as_contiguous(&self) -> Option<&[u8]> {
ReadableBytes::as_contiguous(self.0)
}
}
impl ByteSource for [u8] {
fn len(&self) -> u64 {
self.len() as u64
}
fn read_at(&self, offset: u64, target: &mut [u8]) -> Result<(), BackendError> {
let start = usize::try_from(offset).map_err(|_| BackendError::OutOfBounds)?;
let end = start
.checked_add(target.len())
.filter(|end| *end <= self.len())
.ok_or(BackendError::OutOfBounds)?;
target.copy_from_slice(&self[start..end]);
Ok(())
}
fn as_contiguous(&self) -> Option<&[u8]> {
Some(self)
}
}
impl<const N: usize> ByteSource for [u8; N] {
fn len(&self) -> u64 {
N as u64
}
fn read_at(&self, offset: u64, target: &mut [u8]) -> Result<(), BackendError> {
ByteSource::read_at(self.as_slice(), offset, target)
}
fn as_contiguous(&self) -> Option<&[u8]> {
Some(self)
}
}
pub trait ByteSink: fmt::Debug {
fn len(&self) -> u64;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn write_at(&mut self, offset: u64, source: &[u8]) -> Result<(), BackendError>;
fn as_contiguous_mut(&mut self) -> Option<&mut [u8]> {
None
}
}
#[derive(Debug)]
pub struct TransportByteSink<'a, T: ?Sized>(&'a mut T);
impl<'a, T: ?Sized> TransportByteSink<'a, T> {
pub const fn new(sink: &'a mut T) -> Self {
Self(sink)
}
pub fn into_inner(self) -> &'a mut T {
self.0
}
}
impl<T: WritableBytes + ?Sized> ByteSink for TransportByteSink<'_, T> {
fn len(&self) -> u64 {
WritableBytes::len(self.0)
}
fn write_at(&mut self, offset: u64, source: &[u8]) -> Result<(), BackendError> {
WritableBytes::write_at(self.0, offset, source).map_err(backend_error_from_byte_access)
}
fn as_contiguous_mut(&mut self) -> Option<&mut [u8]> {
WritableBytes::as_contiguous_mut(self.0)
}
}
const fn backend_error_from_byte_access(error: ByteAccessError) -> BackendError {
match error {
ByteAccessError::OutOfBounds => BackendError::OutOfBounds,
ByteAccessError::Busy | ByteAccessError::Reset => BackendError::Busy,
ByteAccessError::Access => BackendError::DeviceLost,
}
}
impl ByteSink for [u8] {
fn len(&self) -> u64 {
self.len() as u64
}
fn write_at(&mut self, offset: u64, source: &[u8]) -> Result<(), BackendError> {
let start = usize::try_from(offset).map_err(|_| BackendError::OutOfBounds)?;
let end = start
.checked_add(source.len())
.filter(|end| *end <= self.len())
.ok_or(BackendError::OutOfBounds)?;
self[start..end].copy_from_slice(source);
Ok(())
}
fn as_contiguous_mut(&mut self) -> Option<&mut [u8]> {
Some(self)
}
}
impl<const N: usize> ByteSink for [u8; N] {
fn len(&self) -> u64 {
N as u64
}
fn write_at(&mut self, offset: u64, source: &[u8]) -> Result<(), BackendError> {
ByteSink::write_at(self.as_mut_slice(), offset, source)
}
fn as_contiguous_mut(&mut self) -> Option<&mut [u8]> {
Some(self)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(transparent)]
pub struct ArtifactFormat(NonZeroU32);
impl ArtifactFormat {
pub const fn new(value: u32) -> Option<Self> {
match NonZeroU32::new(value) {
Some(value) => Some(Self(value)),
None => None,
}
}
pub const fn get(self) -> u32 {
self.0.get()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(transparent)]
pub struct TargetIdentity(pub [u32; 12]);
#[derive(Clone, Copy, Debug)]
pub struct ArtifactRef<'a> {
pub format: ArtifactFormat,
pub target: TargetIdentity,
pub payload: &'a dyn ByteSource,
pub resident_bytes: u64,
}
bitflags! {
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct QueueFlags: u32 {
const IN_ORDER = 1 << 0;
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct QueueDesc {
pub flags: QueueFlags,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Timeout {
Infinite,
AfterNs(NonZeroU64),
}
impl Timeout {
pub const fn from_wire_ns(value: u64) -> Self {
match NonZeroU64::new(value) {
Some(value) => Self::AfterNs(value),
None => Self::Infinite,
}
}
pub const fn to_wire_ns(self) -> u64 {
match self {
Self::Infinite => 0,
Self::AfterNs(value) => value.get(),
}
}
}
#[derive(Debug)]
pub struct BindingRef<'a, B> {
pub slot: u32,
pub buffer: &'a B,
pub range: BufferRange,
pub access: AccessMode,
}
impl<'a, B> BindingRef<'a, B> {
pub fn validate_for_submit(
bindings: &[Self],
descs: &[BufferDesc],
max_bindings: u32,
) -> Result<(), BackendError> {
validate_bindings(bindings, max_bindings)?;
if bindings.len() != descs.len() {
return Err(BackendError::InvalidArgument);
}
for (binding, desc) in bindings.iter().zip(descs.iter()) {
if !desc.allows_access(binding.access) {
return Err(BackendError::PermissionDenied);
}
}
Ok(())
}
}
pub fn validate_bindings<B>(
bindings: &[BindingRef<'_, B>],
max_bindings: u32,
) -> Result<(), BackendError> {
if bindings.is_empty() || bindings.len() > max_bindings as usize {
return Err(BackendError::ResourceLimit);
}
for (index, binding) in bindings.iter().enumerate() {
if bindings[..index]
.iter()
.any(|prior| prior.slot == binding.slot)
{
return Err(BackendError::InvalidArgument);
}
}
Ok(())
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EventState {
Pending,
Complete,
Failed(BackendError),
Cancelled,
}
#[derive(Debug)]
pub enum SubmitFailure<E> {
Rejected(BackendError),
Indeterminate { error: BackendError, event: E },
}
#[derive(Debug)]
pub enum ReleaseFailure<R> {
Rejected { error: BackendError, resource: R },
Indeterminate { error: BackendError },
}
impl<R> ReleaseFailure<R> {
pub const fn error(&self) -> BackendError {
match self {
Self::Rejected { error, .. } | Self::Indeterminate { error } => *error,
}
}
}
pub trait Accelerator {
type Context;
type Buffer;
type Program;
type Queue;
type Event;
fn device_info(&self) -> Result<DeviceInfo, BackendError>;
fn create_context(&self, desc: ContextDesc) -> Result<Self::Context, BackendError>;
fn destroy_context(&self, context: Self::Context) -> Result<(), ReleaseFailure<Self::Context>>;
fn allocate_buffer(
&self,
context: &Self::Context,
desc: BufferDesc,
) -> Result<AllocatedBuffer<Self::Buffer>, BackendError>;
fn write_buffer(
&self,
buffer: &mut Self::Buffer,
offset: u64,
data: &dyn ByteSource,
) -> Result<(), BackendError>;
fn read_buffer(
&self,
buffer: &Self::Buffer,
offset: u64,
data: &mut dyn ByteSink,
) -> Result<(), BackendError>;
fn free_buffer(&self, buffer: Self::Buffer) -> Result<(), ReleaseFailure<Self::Buffer>>;
fn load_program(
&self,
context: &Self::Context,
artifact: ArtifactRef<'_>,
) -> Result<Self::Program, BackendError>;
fn unload_program(&self, program: Self::Program) -> Result<(), ReleaseFailure<Self::Program>>;
fn create_queue(
&self,
context: &Self::Context,
desc: QueueDesc,
) -> Result<Self::Queue, BackendError>;
fn destroy_queue(&self, queue: Self::Queue) -> Result<(), ReleaseFailure<Self::Queue>>;
fn submit(
&self,
queue: &Self::Queue,
program: &Self::Program,
bindings: &[BindingRef<'_, Self::Buffer>],
timeout: Timeout,
) -> Result<Self::Event, SubmitFailure<Self::Event>>;
fn poll_event(&self, event: &Self::Event) -> Result<EventState, BackendError>;
fn cancel_event(&self, _event: &Self::Event) -> Result<(), BackendError> {
Err(BackendError::Unsupported)
}
fn destroy_event(&self, event: Self::Event) -> Result<(), ReleaseFailure<Self::Event>>;
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug)]
struct TransportBytes([u8; 4]);
impl ReadableBytes for TransportBytes {
fn len(&self) -> u64 {
self.0.as_slice().len() as u64
}
fn read_at(&self, offset: u64, target: &mut [u8]) -> Result<(), ByteAccessError> {
let start = usize::try_from(offset).map_err(|_| ByteAccessError::OutOfBounds)?;
let end = start
.checked_add(target.len())
.filter(|end| *end <= self.0.as_slice().len())
.ok_or(ByteAccessError::OutOfBounds)?;
target.copy_from_slice(&self.0[start..end]);
Ok(())
}
}
impl WritableBytes for TransportBytes {
fn len(&self) -> u64 {
self.0.as_slice().len() as u64
}
fn write_at(&mut self, offset: u64, source: &[u8]) -> Result<(), ByteAccessError> {
let start = usize::try_from(offset).map_err(|_| ByteAccessError::OutOfBounds)?;
let end = start
.checked_add(source.len())
.filter(|end| *end <= self.0.as_slice().len())
.ok_or(ByteAccessError::OutOfBounds)?;
self.0[start..end].copy_from_slice(source);
Ok(())
}
}
fn valid_device_info() -> DeviceInfo {
DeviceInfo {
identity: DeviceIdentity {
uuid: [0; 16],
class: AcceleratorClass::OTHER,
vendor_id: 0,
device_id: 0,
},
capabilities: Capabilities::HOST_VISIBLE_MEMORY,
limits: DeviceLimits {
max_contexts: 1,
max_buffers_per_context: 1,
max_programs_per_context: 1,
max_queues_per_context: 1,
max_events_per_context: 1,
max_bindings_per_submission: 1,
max_buffer_bytes: 1,
max_artifact_bytes: 1,
},
}
}
#[test]
fn transport_byte_adapters_preserve_segment_ports_without_copying() {
let mut bytes = TransportBytes(*b"abcd");
let source = TransportByteSource::new(&bytes);
let mut read = [0; 2];
ByteSource::read_at(&source, 1, &mut read).unwrap();
assert_eq!(&read, b"bc");
let mut sink = TransportByteSink::new(&mut bytes);
ByteSink::write_at(&mut sink, 2, b"xy").unwrap();
assert_eq!(&bytes.0, b"abxy");
}
#[test]
fn buffer_descriptors_reject_invalid_alignment() {
assert!(BufferDesc::new(1, 0, MemoryDomain::Host, BufferUsage::empty()).is_err());
assert!(BufferDesc::new(1, 3, MemoryDomain::Host, BufferUsage::TRANSFER_SOURCE).is_err());
assert!(BufferDesc::new(1, 1, MemoryDomain::Host, BufferUsage::empty()).is_err());
assert_eq!(
BufferDesc::new(64, 16, MemoryDomain::Shared, BufferUsage::PROGRAM_INPUT)
.unwrap()
.alignment(),
16
);
}
#[test]
fn buffer_usage_defines_submission_access_compatibility() {
let input =
BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::PROGRAM_INPUT).unwrap();
assert!(input.allows_access(AccessMode::Read));
assert!(!input.allows_access(AccessMode::Write));
assert!(!input.allows_access(AccessMode::ReadWrite));
let output =
BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::PROGRAM_OUTPUT).unwrap();
assert!(!output.allows_access(AccessMode::Read));
assert!(output.allows_access(AccessMode::Write));
assert!(!output.allows_access(AccessMode::ReadWrite));
let mutable =
BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::MUTABLE_STATE).unwrap();
assert!(mutable.allows_access(AccessMode::Read));
assert!(mutable.allows_access(AccessMode::Write));
assert!(mutable.allows_access(AccessMode::ReadWrite));
}
#[test]
fn allocation_properties_reject_hidden_submission_staging() {
let host_input =
BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::PROGRAM_INPUT).unwrap();
assert_eq!(
BufferInfo::new(host_input, 64, 16, BufferProperties::HOST_VISIBLE),
Err(BackendError::Incompatible)
);
assert!(
BufferInfo::new(
host_input,
64,
16,
BufferProperties::HOST_VISIBLE | BufferProperties::DIRECT_BINDING
)
.is_ok()
);
let shared =
BufferDesc::new(64, 16, MemoryDomain::Shared, BufferUsage::TRANSFER_SOURCE).unwrap();
assert_eq!(
BufferInfo::new(shared, 64, 16, BufferProperties::HOST_VISIBLE),
Err(BackendError::Incompatible)
);
assert_eq!(
BufferInfo::new(
shared,
63,
16,
BufferProperties::HOST_VISIBLE | BufferProperties::DIRECT_BINDING
),
Err(BackendError::Incompatible)
);
assert_eq!(
BufferInfo::new(
shared,
64,
8,
BufferProperties::HOST_VISIBLE | BufferProperties::DIRECT_BINDING
),
Err(BackendError::Incompatible)
);
}
#[test]
fn capabilities_report_memory_domains_independently() {
let capabilities = Capabilities::HOST_VISIBLE_MEMORY | Capabilities::SHARED_MEMORY;
assert!(capabilities.supports_memory_domain(MemoryDomain::Host));
assert!(capabilities.supports_memory_domain(MemoryDomain::Shared));
assert!(!capabilities.supports_memory_domain(MemoryDomain::Device));
}
#[test]
fn device_information_rejects_unusable_provider_contracts() {
let valid = valid_device_info();
assert_eq!(valid.validate(), Ok(()));
let mut reserved = valid;
reserved.capabilities |= Capabilities::EXTERNAL_MEMORY;
assert_eq!(
reserved.validate(),
Err(DeviceInfoError::ReservedCapabilities)
);
let mut no_memory = valid;
no_memory.capabilities = Capabilities::EVENT_CANCELLATION;
assert_eq!(
no_memory.validate(),
Err(DeviceInfoError::MissingMemoryDomain)
);
let mut zero_limit = valid;
zero_limit.limits.max_bindings_per_submission = 0;
assert_eq!(zero_limit.validate(), Err(DeviceInfoError::ZeroLimit));
let mut unknown = valid;
unknown.capabilities |= Capabilities::from_bits_retain(1 << 63);
assert_eq!(unknown.validate(), Ok(()));
}
#[test]
fn reserved_operations_are_rejected_before_provider_invocation() {
let mut info = valid_device_info();
assert_eq!(info.validate_context_desc(ContextDesc::default()), Ok(()));
assert_eq!(info.validate_queue_desc(QueueDesc::default()), Ok(()));
assert_eq!(
info.validate_context_desc(ContextDesc {
flags: ContextFlags::SECURE,
}),
Err(BackendError::Unsupported)
);
assert_eq!(
info.validate_queue_desc(QueueDesc {
flags: QueueFlags::IN_ORDER,
}),
Err(BackendError::Unsupported)
);
assert_eq!(
info.validate_event_cancellation(),
Err(BackendError::Unsupported)
);
info.capabilities |= Capabilities::EVENT_CANCELLATION;
assert_eq!(info.validate_event_cancellation(), Ok(()));
}
#[test]
fn bindings_are_nonempty_bounded_and_unique() {
let buffer = ();
let range = BufferRange::new(0, 16).unwrap();
let binding = BindingRef {
slot: 3,
buffer: &buffer,
range,
access: AccessMode::Read,
};
assert!(validate_bindings(&[binding], 1).is_ok());
let duplicate = [
BindingRef {
slot: 3,
buffer: &buffer,
range,
access: AccessMode::Read,
},
BindingRef {
slot: 3,
buffer: &buffer,
range,
access: AccessMode::Write,
},
];
assert_eq!(
validate_bindings(&duplicate, 2),
Err(BackendError::InvalidArgument)
);
assert_eq!(
validate_bindings::<()>(&[], 1),
Err(BackendError::ResourceLimit)
);
}
#[test]
fn binding_access_rejects_usage_mismatch_with_unique_slots() {
let buffer = ();
let range = BufferRange::new(0, 16).unwrap();
let bindings = [BindingRef {
slot: 0,
buffer: &buffer,
range,
access: AccessMode::Write,
}];
let input =
BufferDesc::new(64, 16, MemoryDomain::Host, BufferUsage::PROGRAM_INPUT).unwrap();
assert!(!input.allows_access(AccessMode::Write));
assert!(validate_bindings(&bindings, 1).is_ok());
assert_eq!(
BindingRef::validate_for_submit(&bindings, &[input], 1),
Err(BackendError::PermissionDenied)
);
let read_bindings = [BindingRef {
slot: 0,
buffer: &buffer,
range,
access: AccessMode::Read,
}];
assert!(BindingRef::validate_for_submit(&read_bindings, &[input], 1).is_ok());
assert_eq!(
BindingRef::validate_for_submit(&read_bindings, &[], 1),
Err(BackendError::InvalidArgument)
);
}
#[test]
fn wire_timeouts_are_relative_and_zero_is_infinite() {
assert_eq!(Timeout::from_wire_ns(0), Timeout::Infinite);
assert_eq!(Timeout::from_wire_ns(42).to_wire_ns(), 42);
}
}