use alloc::{format, sync::Arc};
use ax_sync::SpinLock;
use axdevice_base::{AccessWidth, DeviceError, DeviceResult};
use crate::{
GuestMemory, NoGuestMemoryAccessor, VirtioDeviceID, VirtioError, VirtioQueue, map_virtio_error,
pci::{InterruptTransition, VirtioPciInterruptCoordinator},
};
mod access;
mod queue;
mod reset;
mod state;
mod transition;
pub use state::ActivityPermit;
use state::{QueueActivity, QueueState, TransportState};
use transition::InterruptPublicationKind;
pub use transition::{
InterruptPublicationRequest, InterruptTransitionIntent, InterruptTransitionRequest,
QueueNotification, VirtioQueueGeneration,
};
pub(super) const COMMON_CONFIG_SIZE: u64 = 0x38;
pub(super) const NOTIFY_CONFIG_OFFSET: u64 = 0x100;
pub(super) const ISR_CONFIG_OFFSET: u64 = 0x200;
pub(super) const DEVICE_CONFIG_OFFSET: u64 = 0x300;
pub(super) const RESET_DRAIN_SPIN_LIMIT: usize = 1 << 20;
pub(super) const DEVICE_FEATURE_SELECT: u64 = 0x00;
pub(super) const DEVICE_FEATURE: u64 = 0x04;
pub(super) const DRIVER_FEATURE_SELECT: u64 = 0x08;
pub(super) const DRIVER_FEATURE: u64 = 0x0c;
pub(super) const MSIX_CONFIG: u64 = 0x10;
pub(super) const NUM_QUEUES: u64 = 0x12;
pub(super) const DEVICE_STATUS: u64 = 0x14;
pub(super) const CONFIG_GENERATION: u64 = 0x15;
pub(super) const QUEUE_SELECT: u64 = 0x16;
pub(super) const QUEUE_SIZE: u64 = 0x18;
pub(super) const QUEUE_MSIX_VECTOR: u64 = 0x1a;
pub(super) const QUEUE_ENABLE: u64 = 0x1c;
pub(super) const QUEUE_NOTIFY_OFF: u64 = 0x1e;
pub(super) const QUEUE_DESC: u64 = 0x20;
pub(super) const QUEUE_DRIVER: u64 = 0x28;
pub(super) const QUEUE_DEVICE: u64 = 0x30;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueueNotifyOutcome {
Idle,
Completed {
notify: bool,
},
Deferred {
notify: bool,
},
}
pub trait VirtioDeviceCore: Send + Sync {
fn device_type(&self) -> VirtioDeviceID;
fn device_features(&self) -> u64;
fn queue_num_max(&self) -> u16 {
1
}
fn queue_size_max(&self) -> u16;
fn device_config_size(&self) -> u32;
fn read_device_config(&self, offset: u64, width: AccessWidth) -> DeviceResult<u64>;
fn write_device_config(&self, offset: u64, width: AccessWidth, value: u64) -> DeviceResult;
fn notify_queue(
&self,
queue: &mut VirtioQueue<NoGuestMemoryAccessor>,
memory: &mut dyn GuestMemory,
) -> DeviceResult<QueueNotifyOutcome>;
fn requires_deferred_processing(&self) -> bool {
false
}
fn reset(&self) -> DeviceResult {
Ok(())
}
}
pub enum VirtioPciWriteOutcome {
None,
QueueNotified(QueueNotification),
Reset {
interrupt: InterruptTransition,
},
Fault {
error: DeviceError,
publication: InterruptPublicationRequest,
},
}
pub struct VirtioPciTransport<D: VirtioDeviceCore> {
core: D,
state: SpinLock<TransportState>,
interrupts: Arc<VirtioPciInterruptCoordinator>,
activity: Arc<QueueActivity>,
device_config_size: u32,
#[cfg(test)]
notify_admission_hook: SpinLock<Option<Arc<dyn Fn() + Send + Sync>>>,
#[cfg(test)]
reset_before_core_hook: SpinLock<Option<Arc<dyn Fn() + Send + Sync>>>,
}
impl<D: VirtioDeviceCore> VirtioPciTransport<D> {
pub fn try_new(core: D) -> DeviceResult<Self> {
let queue_num_max = core.queue_num_max();
let queue_size_max = core.queue_size_max();
if queue_num_max != 1 {
return Err(DeviceError::InvalidInput {
operation: "create VirtIO PCI transport",
detail: "commit 4 supports exactly one queue".into(),
});
}
if !queue_size_max.is_power_of_two() {
return Err(DeviceError::InvalidInput {
operation: "create VirtIO PCI transport",
detail: "queue size must be a power of two".into(),
});
}
if core.requires_deferred_processing() {
return Err(DeviceError::Unsupported {
operation: "create VirtIO PCI transport",
detail: "deferred queue processing is not supported by the synchronous PCI adapter"
.into(),
});
}
Ok(Self {
device_config_size: core.device_config_size(),
state: SpinLock::new(TransportState::new(queue_num_max, queue_size_max)),
interrupts: Arc::new(VirtioPciInterruptCoordinator::new()),
activity: Arc::new(QueueActivity::new()),
core,
#[cfg(test)]
notify_admission_hook: SpinLock::new(None),
#[cfg(test)]
reset_before_core_hook: SpinLock::new(None),
})
}
pub fn core(&self) -> &D {
&self.core
}
pub fn device_type(&self) -> VirtioDeviceID {
self.core.device_type()
}
pub fn device_features(&self) -> u64 {
self.core.device_features()
}
pub fn status(&self) -> u8 {
self.state.lock().status
}
pub fn driver_features(&self) -> u64 {
self.state.lock().driver_features
}
pub fn queue_generation(&self) -> VirtioQueueGeneration {
VirtioQueueGeneration(self.state.lock().queue_generation)
}
pub fn interrupt_pending(&self) -> bool {
self.interrupts.pending()
}
#[cfg(test)]
pub(crate) fn record_interrupt(&self, configuration_change: bool) -> InterruptTransition {
if configuration_change {
self.interrupts.record_config_change()
} else {
self.interrupts.record_queue_completion(true)
}
}
#[cfg(test)]
pub(crate) fn set_notify_admission_hook<F>(&self, hook: F)
where
F: Fn() + Send + Sync + 'static,
{
*self.notify_admission_hook.lock() = Some(Arc::new(hook));
}
#[cfg(test)]
pub(super) fn run_notify_admission_hook(&self) {
let hook = self.notify_admission_hook.lock().clone();
if let Some(hook) = hook {
hook();
}
}
#[cfg(test)]
pub(crate) fn set_reset_before_core_hook<F>(&self, hook: F)
where
F: Fn() + Send + Sync + 'static,
{
*self.reset_before_core_hook.lock() = Some(Arc::new(hook));
}
#[cfg(test)]
pub(super) fn run_reset_before_core_hook(&self) {
let hook = self.reset_before_core_hook.lock().clone();
if let Some(hook) = hook {
hook();
}
}
pub fn update_interrupt_disabled_logical(&self, disabled: bool) -> InterruptTransitionIntent {
let generation = self.queue_generation();
let transition = self.interrupts.set_disabled(disabled);
InterruptTransitionIntent::new(transition, generation)
}
pub fn admit_interrupt_transition(
&self,
intent: InterruptTransitionIntent,
) -> DeviceResult<Option<InterruptTransitionRequest>> {
let Some(activity) = self.activity.acquire(intent.generation()) else {
self.interrupts.cancel_transition(intent.transition());
return Ok(None);
};
if self.queue_generation() != intent.generation() {
self.interrupts
.suppress_stale_transition(intent.transition());
drop(activity);
return Ok(None);
}
Ok(Some(InterruptTransitionRequest::new(
Arc::clone(&self.interrupts),
intent.transition(),
Some(activity),
)))
}
pub fn set_interrupt_disabled(
&self,
disabled: bool,
) -> DeviceResult<InterruptTransitionRequest> {
let intent = self.update_interrupt_disabled_logical(disabled);
self.admit_interrupt_transition(intent)?
.ok_or(DeviceError::InvalidState {
operation: "update VirtIO PCI interrupt state",
detail: "transport reset is in progress or the transition is stale".into(),
})
}
pub fn complete_interrupt_transition(
&self,
transition: InterruptTransition,
success: bool,
) -> InterruptTransition {
self.interrupts.complete_transition(transition, success)
}
pub fn suppress_stale_interrupt_transition(&self, transition: InterruptTransition) {
self.interrupts.suppress_stale_transition(transition);
}
pub fn resynchronize_interrupt(&self) -> InterruptTransition {
self.interrupts.resynchronize()
}
fn acquire_control_activity(&self) -> DeviceResult<ActivityPermit> {
self.activity
.acquire(self.queue_generation())
.ok_or(DeviceError::InvalidState {
operation: "access VirtIO PCI transport control state",
detail: "transport reset is in progress".into(),
})
}
}
fn require_width(actual: AccessWidth, expected: AccessWidth) -> DeviceResult {
if actual == expected {
Ok(())
} else {
Err(DeviceError::InvalidWidth { expected, actual })
}
}
fn access_in_region(offset: u64, width: AccessWidth, start: u64, length: u64) -> bool {
offset >= start
&& offset
.checked_add(width.size() as u64)
.is_some_and(|end| end <= start + length)
}
fn feature_word(features: u64, selector: u32) -> DeviceResult<u64> {
if selector > 1 {
Ok(0)
} else {
Ok((features >> (selector * 32)) & u32::MAX as u64)
}
}
fn invalid_queue(index: u16) -> DeviceError {
DeviceError::InvalidInput {
operation: "virtio-pci queue",
detail: format!("queue index {index} is not exposed"),
}
}
fn map_pci_error(error: VirtioError) -> DeviceError {
map_virtio_error(error, "virtio-pci queue")
}
fn reject_processing_queue(queue: &QueueState) -> DeviceResult {
if queue.processing {
Err(DeviceError::ResourceBusy {
operation: "configure VirtIO queue",
resource: "queue processing lease".into(),
})
} else {
Ok(())
}
}
#[cfg(test)]
mod tests;