Skip to main content

virtio_accel_proto/
lib.rs

1//! Pointer-free, little-endian wire structures for portable virtio-accel protocol 1.0.
2//!
3//! The versioned byte contract is a candidate for independent implementation and final audit. It
4//! intentionally does not assign or claim a Virtio device ID or standardize provider artifact
5//! contents.
6
7#![no_std]
8#![forbid(unsafe_code)]
9
10use bitflags::bitflags;
11use zerocopy::byteorder::{LE, U16, U32, U64};
12use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
13
14pub type Le16 = U16<LE>;
15pub type Le32 = U32<LE>;
16pub type Le64 = U64<LE>;
17
18/// Candidate portable protocol major version.
19pub const PROTOCOL_MAJOR: u16 = 1;
20/// Candidate portable protocol minor version.
21pub const PROTOCOL_MINOR: u16 = 0;
22/// Index of the baseline command virtqueue.
23///
24/// This transport queue carries protocol requests and responses. It is distinct from an
25/// accelerator execution queue created with [`KnownOpcode::CreateQueue`].
26pub const COMMAND_QUEUE: u16 = 0;
27/// Number of command virtqueues in the baseline device model.
28pub const BASELINE_COMMAND_QUEUES: u16 = 1;
29/// Protocol-wide upper bound for flattened descriptors in one command chain.
30pub const HARD_MAX_CHAIN_DESCRIPTORS: u16 = 256;
31/// Protocol-wide upper bound for one complete request frame, including its header.
32pub const HARD_MAX_REQUEST_BYTES: u32 = 16 * 1024 * 1024;
33/// Protocol-wide upper bound for one complete response frame, including its header.
34pub const HARD_MAX_RESPONSE_BYTES: u32 = 16 * 1024 * 1024;
35/// Protocol-wide upper bound for bindings carried by one submission.
36pub const HARD_MAX_BINDINGS: u32 = 4_096;
37/// Smallest request-frame limit that can carry every baseline opcode.
38pub const MIN_MAX_REQUEST_BYTES: u32 = 97;
39/// Smallest response-frame limit that can carry every baseline response.
40pub const MIN_MAX_RESPONSE_BYTES: u32 = 92;
41
42/// Mask of buffer-usage bits assigned by protocol 1.0.
43pub const KNOWN_BUFFER_USAGE_BITS: u32 = 0x1f;
44/// Request flags accepted by protocol 1.0.
45pub const KNOWN_REQUEST_FLAG_BITS: u16 = 0;
46/// Context flags accepted by protocol 1.0.
47pub const KNOWN_CONTEXT_FLAG_BITS: u32 = 0;
48/// Program-load flags accepted by protocol 1.0.
49pub const KNOWN_PROGRAM_FLAG_BITS: u32 = 0;
50/// Accelerator execution-queue flags accepted by protocol 1.0.
51pub const KNOWN_QUEUE_FLAG_BITS: u32 = 0;
52/// Submission flags accepted by protocol 1.0.
53pub const KNOWN_SUBMIT_FLAG_BITS: u32 = 0;
54/// Former draft assignment retained as a reserved-zero request bit.
55pub const RESERVED_REQUEST_FLAG_NO_WAIT: u16 = 1 << 0;
56
57bitflags! {
58    /// Device-specific feature bits. Virtio transport feature bits are deliberately separate.
59    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
60    pub struct FeatureBits: u64 {
61        const MULTI_QUEUE = 1 << 0;
62        const EVENT_QUEUE = 1 << 1;
63        const EXTERNAL_MEMORY = 1 << 2;
64        const TIMELINE_FENCES = 1 << 3;
65        const SECURE_CONTEXTS = 1 << 4;
66    }
67}
68
69/// Feature set required by every implementation of portable protocol 1.0.
70///
71/// The baseline deliberately requires no device-specific feature bits. The commands and
72/// object lifecycle described by the specification remain available; feature bits are reserved
73/// for behavior that changes transport framing or synchronization.
74pub const BASELINE_FEATURES: FeatureBits = FeatureBits::empty();
75
76/// Reserved feature bits that a protocol 1.0 implementation must leave unadvertised.
77///
78/// Defining their numeric positions preserves the reviewed namespace without assigning protocol
79/// semantics. Advertising any of these bits is a protocol error.
80pub const RESERVED_FEATURES: FeatureBits = FeatureBits::from_bits_retain(
81    FeatureBits::MULTI_QUEUE.bits()
82        | FeatureBits::EVENT_QUEUE.bits()
83        | FeatureBits::EXTERNAL_MEMORY.bits()
84        | FeatureBits::TIMELINE_FENCES.bits()
85        | FeatureBits::SECURE_CONTEXTS.bits(),
86);
87
88bitflags! {
89    /// Per-request flags.
90    ///
91    /// Protocol 1.0 defines no request flags. Receivers must reject every nonzero raw flag value.
92    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
93    pub struct RequestFlags: u16 {}
94}
95
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97#[repr(u16)]
98pub enum KnownOpcode {
99    GetDeviceInfo = 0x0001,
100    CreateContext = 0x0100,
101    DestroyContext = 0x0101,
102    AllocateBuffer = 0x0200,
103    FreeBuffer = 0x0201,
104    WriteBuffer = 0x0202,
105    ReadBuffer = 0x0203,
106    LoadProgram = 0x0300,
107    UnloadProgram = 0x0301,
108    CreateQueue = 0x0400,
109    DestroyQueue = 0x0401,
110    Submit = 0x0500,
111    PollEvent = 0x0501,
112    CancelEvent = 0x0502,
113    DestroyEvent = 0x0503,
114}
115
116#[derive(Clone, Copy, Debug, PartialEq, Eq)]
117pub struct UnknownOpcode(pub u16);
118
119impl TryFrom<u16> for KnownOpcode {
120    type Error = UnknownOpcode;
121
122    fn try_from(value: u16) -> Result<Self, Self::Error> {
123        match value {
124            0x0001 => Ok(Self::GetDeviceInfo),
125            0x0100 => Ok(Self::CreateContext),
126            0x0101 => Ok(Self::DestroyContext),
127            0x0200 => Ok(Self::AllocateBuffer),
128            0x0201 => Ok(Self::FreeBuffer),
129            0x0202 => Ok(Self::WriteBuffer),
130            0x0203 => Ok(Self::ReadBuffer),
131            0x0300 => Ok(Self::LoadProgram),
132            0x0301 => Ok(Self::UnloadProgram),
133            0x0400 => Ok(Self::CreateQueue),
134            0x0401 => Ok(Self::DestroyQueue),
135            0x0500 => Ok(Self::Submit),
136            0x0501 => Ok(Self::PollEvent),
137            0x0502 => Ok(Self::CancelEvent),
138            0x0503 => Ok(Self::DestroyEvent),
139            _ => Err(UnknownOpcode(value)),
140        }
141    }
142}
143
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145#[repr(transparent)]
146pub struct StatusCode(pub u16);
147
148impl StatusCode {
149    pub const OK: Self = Self(0);
150    pub const UNSUPPORTED: Self = Self(1);
151    pub const INCOMPATIBLE: Self = Self(2);
152    pub const INVALID_ARGUMENT: Self = Self(3);
153    pub const OUT_OF_BOUNDS: Self = Self(4);
154    pub const BUSY: Self = Self(5);
155    pub const OUT_OF_MEMORY: Self = Self(6);
156    pub const RESOURCE_LIMIT: Self = Self(7);
157    pub const DEADLINE_EXPIRED: Self = Self(8);
158    pub const DEVICE_LOST: Self = Self(9);
159    pub const PERMISSION_DENIED: Self = Self(10);
160    pub const STALE_OBJECT: Self = Self(11);
161    pub const INTERNAL_ERROR: Self = Self(0xffff);
162
163    /// Returns whether this value has assigned protocol 1.0 semantics.
164    pub const fn is_known(self) -> bool {
165        matches!(self.0, 0..=11 | 0xffff)
166    }
167
168    pub const fn is_success(self) -> bool {
169        self.0 == Self::OK.0
170    }
171}
172
173#[derive(Clone, Copy, Debug, PartialEq, Eq)]
174#[repr(u16)]
175pub enum KnownEventState {
176    Pending = 0,
177    Complete = 1,
178    Failed = 2,
179    Cancelled = 3,
180}
181
182#[derive(Clone, Copy, Debug, PartialEq, Eq)]
183pub struct UnknownEventState(pub u16);
184
185impl TryFrom<u16> for KnownEventState {
186    type Error = UnknownEventState;
187
188    fn try_from(value: u16) -> Result<Self, Self::Error> {
189        match value {
190            0 => Ok(Self::Pending),
191            1 => Ok(Self::Complete),
192            2 => Ok(Self::Failed),
193            3 => Ok(Self::Cancelled),
194            _ => Err(UnknownEventState(value)),
195        }
196    }
197}
198
199#[derive(
200    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
201)]
202#[repr(C)]
203pub struct WireConfig {
204    pub protocol_major: Le16,
205    pub protocol_minor: Le16,
206    pub command_queue_count: Le16,
207    pub max_chain_descriptors: Le16,
208    pub max_request_bytes: Le32,
209    pub max_response_bytes: Le32,
210}
211
212#[derive(Clone, Copy, Debug, PartialEq, Eq)]
213pub enum ConfigError {
214    Version,
215    CommandQueueCount,
216    ChainDescriptorLimit,
217    RequestByteLimit,
218    ResponseByteLimit,
219}
220
221impl WireConfig {
222    /// Validates that this configuration can provide the protocol 1.0 baseline.
223    ///
224    /// A higher minor version is accepted and used with 1.0 behavior until separately negotiated
225    /// extensions are understood.
226    pub fn validate(&self) -> Result<(), ConfigError> {
227        if self.protocol_major.get() != PROTOCOL_MAJOR {
228            return Err(ConfigError::Version);
229        }
230        if self.command_queue_count.get() != BASELINE_COMMAND_QUEUES {
231            return Err(ConfigError::CommandQueueCount);
232        }
233        if !(2..=HARD_MAX_CHAIN_DESCRIPTORS).contains(&self.max_chain_descriptors.get()) {
234            return Err(ConfigError::ChainDescriptorLimit);
235        }
236        if !(MIN_MAX_REQUEST_BYTES..=HARD_MAX_REQUEST_BYTES).contains(&self.max_request_bytes.get())
237        {
238            return Err(ConfigError::RequestByteLimit);
239        }
240        if !(MIN_MAX_RESPONSE_BYTES..=HARD_MAX_RESPONSE_BYTES)
241            .contains(&self.max_response_bytes.get())
242        {
243            return Err(ConfigError::ResponseByteLimit);
244        }
245        Ok(())
246    }
247}
248
249#[derive(
250    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
251)]
252#[repr(C)]
253pub struct RequestHeader {
254    pub opcode: Le16,
255    pub flags: Le16,
256    pub payload_bytes: Le32,
257    pub request_id: Le64,
258}
259
260impl RequestHeader {
261    pub fn new(
262        opcode: KnownOpcode,
263        flags: RequestFlags,
264        payload_bytes: u32,
265        request_id: u64,
266    ) -> Self {
267        Self {
268            opcode: Le16::new(opcode as u16),
269            flags: Le16::new(flags.bits()),
270            payload_bytes: Le32::new(payload_bytes),
271            request_id: Le64::new(request_id),
272        }
273    }
274
275    pub fn known_opcode(&self) -> Result<KnownOpcode, UnknownOpcode> {
276        self.opcode.get().try_into()
277    }
278}
279
280#[derive(
281    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
282)]
283#[repr(C)]
284pub struct ResponseHeader {
285    pub status: Le16,
286    pub flags: Le16,
287    pub payload_bytes: Le32,
288    pub request_id: Le64,
289}
290
291impl ResponseHeader {
292    pub fn new(status: StatusCode, payload_bytes: u32, request_id: u64) -> Self {
293        Self {
294            status: Le16::new(status.0),
295            flags: Le16::new(0),
296            payload_bytes: Le32::new(payload_bytes),
297            request_id: Le64::new(request_id),
298        }
299    }
300}
301
302#[derive(
303    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
304)]
305#[repr(C)]
306pub struct WireDeviceInfo {
307    pub uuid: [u8; 16],
308    pub class: Le16,
309    pub reserved: Le16,
310    pub vendor_id: Le32,
311    pub device_id: Le32,
312    pub capabilities: Le64,
313    pub max_contexts: Le32,
314    pub max_buffers_per_context: Le32,
315    pub max_programs_per_context: Le32,
316    pub max_queues_per_context: Le32,
317    pub max_events_per_context: Le32,
318    pub max_bindings_per_submission: Le32,
319    pub max_buffer_bytes: Le64,
320    pub max_artifact_bytes: Le64,
321}
322
323#[derive(
324    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
325)]
326#[repr(C)]
327pub struct CreateContextRequest {
328    pub flags: Le32,
329    pub reserved: Le32,
330}
331
332#[derive(
333    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
334)]
335#[repr(C)]
336pub struct ObjectPayload {
337    pub object_id: Le64,
338}
339
340#[derive(
341    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
342)]
343#[repr(C)]
344pub struct AllocateBufferRequest {
345    pub context_id: Le64,
346    pub bytes: Le64,
347    pub alignment: Le64,
348    pub memory_domain: u8,
349    pub reserved0: [u8; 7],
350    pub usage: Le32,
351    pub reserved1: Le32,
352}
353
354#[derive(
355    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
356)]
357#[repr(C)]
358pub struct TransferBufferRequest {
359    pub buffer_id: Le64,
360    pub offset: Le64,
361    pub bytes: Le64,
362}
363
364#[derive(
365    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
366)]
367#[repr(C)]
368pub struct LoadProgramRequest {
369    pub context_id: Le64,
370    pub format: Le32,
371    pub flags: Le32,
372    pub target: [Le32; 12],
373    pub payload_bytes: Le64,
374    pub resident_bytes: Le64,
375}
376
377#[derive(
378    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
379)]
380#[repr(C)]
381pub struct CreateQueueRequest {
382    pub context_id: Le64,
383    pub flags: Le32,
384    pub reserved: Le32,
385}
386
387#[derive(
388    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
389)]
390#[repr(C)]
391pub struct SubmitRequest {
392    pub queue_id: Le64,
393    pub program_id: Le64,
394    pub binding_count: Le32,
395    pub flags: Le32,
396    /// Relative timeout from device admission. Zero means infinite.
397    pub timeout_ns: Le64,
398}
399
400#[derive(
401    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
402)]
403#[repr(C)]
404pub struct WireBinding {
405    pub buffer_id: Le64,
406    pub offset: Le64,
407    pub bytes: Le64,
408    pub slot: Le32,
409    pub access: u8,
410    pub reserved: [u8; 3],
411}
412
413/// Event identifier returned for an accepted or indeterminate submission.
414#[derive(
415    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
416)]
417#[repr(C)]
418pub struct SubmitResponse {
419    pub event_id: Le64,
420}
421
422#[derive(
423    Clone, Copy, Debug, PartialEq, Eq, FromBytes, IntoBytes, KnownLayout, Immutable, Unaligned,
424)]
425#[repr(C)]
426pub struct WireEventState {
427    pub state: Le16,
428    pub error: Le16,
429    pub reserved: Le32,
430}
431
432impl WireEventState {
433    pub fn known_state(&self) -> Result<KnownEventState, UnknownEventState> {
434        self.state.get().try_into()
435    }
436}
437
438#[derive(Clone, Copy, Debug, PartialEq, Eq)]
439pub enum DecodeError {
440    Size,
441    CountOverflow,
442}
443
444pub fn read_exact<T: FromBytes>(bytes: &[u8]) -> Result<T, DecodeError> {
445    T::read_from_bytes(bytes).map_err(|_| DecodeError::Size)
446}
447
448pub fn checked_array_bytes<T>(count: u32) -> Result<usize, DecodeError> {
449    core::mem::size_of::<T>()
450        .checked_mul(count as usize)
451        .ok_or(DecodeError::CountOverflow)
452}
453
454#[cfg(test)]
455mod tests {
456    use super::*;
457    use core::mem::size_of;
458    use zerocopy::IntoBytes;
459
460    #[test]
461    fn headers_are_fixed_little_endian_frames() {
462        let header = RequestHeader::new(KnownOpcode::Submit, RequestFlags::empty(), 32, 7);
463        assert_eq!(size_of::<RequestHeader>(), 16);
464        assert_eq!(header.as_bytes()[..2], 0x0500_u16.to_le_bytes());
465        let decoded = read_exact::<RequestHeader>(header.as_bytes()).unwrap();
466        assert_eq!(decoded.known_opcode(), Ok(KnownOpcode::Submit));
467        assert_eq!(decoded.request_id.get(), 7);
468    }
469
470    #[test]
471    fn reserved_features_are_not_baseline_requirements() {
472        assert!(BASELINE_FEATURES.is_empty());
473        assert!(!RESERVED_FEATURES.is_empty());
474        assert!(!BASELINE_FEATURES.intersects(RESERVED_FEATURES));
475    }
476}