use alloc::format;
use alloc::string::{String, ToString};
use alloc::sync::{Arc, Weak};
use alloc::vec::Vec;
use core::fmt;
use crate::core::device::{Device, DeviceClass, RealizeCtx, ResetKind};
use crate::core::error::{BusError, Error, Result};
use crate::core::space::{AccessConstraints, AddressSpace, MemAttrs, MemOps, MemResult};
use crate::core::space::{Region, RegionRef, RequesterId};
use crate::core::state::{ChunkReader, ChunkWriter, Sink, Source};
use crate::core::sync::{LockRank, Mutex};
use crate::core::value::{Endian, Width};
use crate::core::wire::{Level, WireSource};
use crate::machine::realize::{BindCtx, Instance};
use super::super::dt::{DtSource, NodeSpec};
use super::queue::{Descriptor, Layout, QUEUE_SIZE_MAX, Queue};
use super::{Backend, VENDOR_ID};
pub const REGISTER_WINDOW_LEN: u64 = 0x1000;
pub const CONFIG_OFFSET: u64 = 0x100;
pub const MAGIC: u32 = 0x7472_6976;
pub const VERSION: u32 = 2;
const STATUS_ACKNOWLEDGE: u32 = 1;
const STATUS_DRIVER: u32 = 2;
const STATUS_DRIVER_OK: u32 = 4;
const STATUS_FEATURES_OK: u32 = 8;
const STATUS_NEEDS_RESET: u32 = 64;
const STATUS_FAILED: u32 = 128;
pub const F_VERSION_1: u64 = 1 << 32;
const INT_USED_BUFFER: u32 = 1;
const INT_CONFIG_CHANGE: u32 = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
struct State {
device_features_sel: u32,
driver_features_sel: u32,
driver_features: [u32; 2],
queue_sel: u32,
queues: Vec<QueueState>,
status: u32,
interrupt_status: u32,
config_generation: u32,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
struct QueueState {
layout: Layout,
last_avail: u16,
used_idx: u16,
}
impl State {
fn new(queues: usize) -> State {
State {
device_features_sel: 0,
driver_features_sel: 0,
driver_features: [0; 2],
queue_sel: 0,
queues: alloc::vec![QueueState::default(); queues],
status: 0,
interrupt_status: 0,
config_generation: 0,
}
}
}
struct Registers {
state: Mutex<State>,
links: Mutex<Links>,
backend: Arc<dyn Backend>,
}
#[derive(Debug, Default)]
struct Links {
out: Option<WireSource>,
space: Option<Arc<AddressSpace>>,
requester: RequesterId,
irq_wire: Option<crate::core::wire::WireId>,
}
impl fmt::Debug for Registers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut s = f.debug_struct("Registers");
s.field("backend", &self.backend);
match self.state.try_lock() {
Some(state) => s.field("status", &state.status).finish(),
None => s.field("state", &"<in use>").finish(),
}
}
}
#[derive(Debug)]
pub struct VirtioMmio {
regs: Arc<Registers>,
region: RegionRef,
class: &'static DeviceClass,
}
impl VirtioMmio {
#[must_use]
pub fn new(backend: Arc<dyn Backend>, class: &'static DeviceClass) -> VirtioMmio {
let queues = backend.queue_count();
let regs = Arc::new(Registers {
state: Mutex::with_rank(LockRank::DEVICE, State::new(queues)),
links: Mutex::with_rank(LockRank::LEAF, Links::default()),
backend,
});
let region: RegionRef = Arc::new(Region::io(
"virtio.mmio",
REGISTER_WINDOW_LEN,
Arc::clone(®s) as Arc<dyn MemOps>,
));
VirtioMmio {
regs,
region,
class,
}
}
#[must_use]
pub fn backend(&self) -> &Arc<dyn Backend> {
&self.regs.backend
}
#[must_use]
pub fn status(&self) -> u32 {
self.regs.state.lock().status
}
#[must_use]
pub fn irq_asserted(&self) -> bool {
self.regs.state.lock().interrupt_status != 0
}
pub fn attach_space(&self, space: Arc<AddressSpace>, requester: RequesterId) {
let mut links = self.regs.links.lock();
links.space = Some(space);
links.requester = requester;
}
pub fn notify(&self, index: u32) {
self.regs.notify(index);
}
pub fn signal_config_change(&self) {
{
let mut state = self.regs.state.lock();
state.config_generation = state.config_generation.wrapping_add(1);
state.interrupt_status |= INT_CONFIG_CHANGE;
}
self.regs.drive(true);
}
}
impl Registers {
fn drive(&self, asserted: bool) {
let out = self.links.lock().out.clone();
if let Some(out) = out {
out.set(Level::from_bool(asserted));
}
}
fn device_features(&self, sel: u32) -> u32 {
let all = self.backend.features() | F_VERSION_1;
match sel {
0 => all as u32,
1 => (all >> 32) as u32,
_ => 0,
}
}
fn notify(&self, index: u32) {
let (space, requester) = {
let links = self.links.lock();
(links.space.clone(), links.requester)
};
let Some(space) = space else {
return;
};
let Some(queue) = self.live_queue(index) else {
return;
};
let q = Queue::new(queue.layout, &space, requester);
let Ok(avail) = q.avail_idx() else {
return;
};
let mut last = queue.last_avail;
let mut used = queue.used_idx;
let mut did_work = false;
while last != avail {
let Ok(head) = q.avail_head(last) else {
break;
};
let Ok(chain) = q.chain(head) else {
break;
};
let written = self.backend.handle(index as usize, &q, &chain);
let Ok(next) = q.publish(used, head, written) else {
break;
};
used = next;
last = last.wrapping_add(1);
did_work = true;
}
let raise = {
let mut state = self.state.lock();
let Some(slot) = state.queues.get_mut(index as usize) else {
return;
};
slot.last_avail = last;
slot.used_idx = used;
if did_work {
state.interrupt_status |= INT_USED_BUFFER;
}
state.interrupt_status != 0
};
if did_work {
self.drive(raise);
}
}
fn live_queue(&self, index: u32) -> Option<QueueState> {
let state = self.state.lock();
if state.status & STATUS_DRIVER_OK == 0 {
return None;
}
let slot = state.queues.get(index as usize).copied()?;
slot.layout.is_live().then_some(slot)
}
fn reset(&self) {
{
let mut state = self.state.lock();
*state = State::new(state.queues.len());
}
self.backend.reset();
self.drive(false);
}
fn read_register(&self, offset: u64) -> u32 {
let mut state = self.state.lock();
match offset {
0x000 => MAGIC,
0x004 => VERSION,
0x008 => self.backend.device_id(),
0x00c => VENDOR_ID,
0x010 => self.device_features(state.device_features_sel),
0x034 => QUEUE_SIZE_MAX,
0x044 => u32::from(
state
.queues
.get(state.queue_sel as usize)
.is_some_and(|q| q.layout.ready),
),
0x060 => state.interrupt_status,
0x070 => state.status,
0x0fc => state.config_generation,
_ => {
let _ = &mut state;
0
}
}
}
fn write_register(&self, offset: u64, value: u32) {
enum After {
Nothing,
Notify(u32),
Reset,
Interrupt(bool),
}
let after = {
let mut state = self.state.lock();
let sel = state.queue_sel as usize;
match offset {
0x014 => {
state.device_features_sel = value;
After::Nothing
}
0x020 => {
let word = state.driver_features_sel.min(1) as usize;
if state.driver_features_sel < 2 {
state.driver_features[word] = value;
}
After::Nothing
}
0x024 => {
state.driver_features_sel = value;
After::Nothing
}
0x030 => {
state.queue_sel = value;
After::Nothing
}
0x038 => {
if let Some(q) = state.queues.get_mut(sel) {
let size = value.min(QUEUE_SIZE_MAX);
q.layout.size = if size.is_power_of_two() { size } else { 0 };
}
After::Nothing
}
0x044 => {
if let Some(q) = state.queues.get_mut(sel) {
q.layout.ready = value & 1 != 0;
if !q.layout.ready {
q.last_avail = 0;
q.used_idx = 0;
}
}
After::Nothing
}
0x050 => After::Notify(value),
0x064 => {
state.interrupt_status &= !value;
After::Interrupt(state.interrupt_status != 0)
}
0x070 => {
if value == 0 {
After::Reset
} else {
let mut status = value;
if status & STATUS_FEATURES_OK != 0 {
let accepted = u64::from(state.driver_features[0])
| (u64::from(state.driver_features[1]) << 32);
if accepted & F_VERSION_1 == 0 {
status &= !STATUS_FEATURES_OK;
}
}
state.status = status
& (STATUS_ACKNOWLEDGE
| STATUS_DRIVER
| STATUS_DRIVER_OK
| STATUS_FEATURES_OK
| STATUS_NEEDS_RESET
| STATUS_FAILED);
After::Nothing
}
}
0x080 => {
set_low(&mut state, sel, |l| &mut l.desc, value);
After::Nothing
}
0x084 => {
set_high(&mut state, sel, |l| &mut l.desc, value);
After::Nothing
}
0x090 => {
set_low(&mut state, sel, |l| &mut l.avail, value);
After::Nothing
}
0x094 => {
set_high(&mut state, sel, |l| &mut l.avail, value);
After::Nothing
}
0x0a0 => {
set_low(&mut state, sel, |l| &mut l.used, value);
After::Nothing
}
0x0a4 => {
set_high(&mut state, sel, |l| &mut l.used, value);
After::Nothing
}
_ => After::Nothing,
}
};
match after {
After::Nothing => {}
After::Notify(index) => self.notify(index),
After::Reset => self.reset(),
After::Interrupt(level) => self.drive(level),
}
}
}
fn set_low(state: &mut State, sel: usize, field: fn(&mut Layout) -> &mut u64, value: u32) {
if let Some(q) = state.queues.get_mut(sel) {
let slot = field(&mut q.layout);
*slot = (*slot & 0xffff_ffff_0000_0000) | u64::from(value);
}
}
fn set_high(state: &mut State, sel: usize, field: fn(&mut Layout) -> &mut u64, value: u32) {
if let Some(q) = state.queues.get_mut(sel) {
let slot = field(&mut q.layout);
*slot = (*slot & 0xffff_ffff) | (u64::from(value) << 32);
}
}
impl MemOps for Registers {
fn read(&self, offset: u64, dst: &mut [u8], _attrs: MemAttrs) -> MemResult {
if offset >= CONFIG_OFFSET {
self.backend.config_read(offset - CONFIG_OFFSET, dst);
return Ok(());
}
if dst.len() != 4 || !offset.is_multiple_of(4) {
return Err(BusError::BadAccess);
}
dst.copy_from_slice(&self.read_register(offset).to_le_bytes());
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
if attrs.debug {
return Err(BusError::BadAccess);
}
if offset >= CONFIG_OFFSET {
self.backend.config_write(offset - CONFIG_OFFSET, src);
return Ok(());
}
if src.len() != 4 || !offset.is_multiple_of(4) {
return Err(BusError::BadAccess);
}
self.write_register(offset, u32::from_le_bytes([src[0], src[1], src[2], src[3]]));
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints {
min: Width::U8,
max: Width::U64,
natural_alignment: true,
endian: Endian::Little,
allow_bulk: false,
secure_only: false,
privileged_only: false,
drives_data_bus: true,
}
}
}
impl DtSource for Registers {
fn dt_spec(&self) -> NodeSpec {
let mut spec = NodeSpec::peripheral("virtio_mmio", &["virtio,mmio"]);
spec.irq_wire = self.links.lock().irq_wire;
spec
}
}
impl Device for VirtioMmio {
fn class(&self) -> &'static DeviceClass {
self.class
}
fn realize(&self, ctx: &mut RealizeCtx<'_>) -> Result<()> {
super::super::dt::publish(
ctx.hosts(),
&self.region,
Arc::downgrade(&self.regs) as Weak<dyn DtSource>,
)
}
fn reset(&self, _kind: ResetKind) {
self.regs.reset();
}
fn region(&self, name: &str) -> Option<RegionRef> {
matches!(name, "" | "regs").then(|| Arc::clone(&self.region))
}
fn connect(&self, port: &str, source: WireSource) -> Result<()> {
if port != "irq" {
return Err(Error::Config {
at: port.to_string(),
message: String::from("a virtio-mmio device drives one pin, `irq`"),
});
}
let mut links = self.regs.links.lock();
links.irq_wire = Some(source.id());
links.out = Some(source);
Ok(())
}
fn announce(&self, port: &str) {
if port == "irq" {
let asserted = self.regs.state.lock().interrupt_status != 0;
self.regs.drive(asserted);
}
}
fn save(&self, w: &mut ChunkWriter<'_>) -> Result<()> {
let state = self.regs.state.lock();
w.write_u32(state.device_features_sel)?;
w.write_u32(state.driver_features_sel)?;
w.write_u32(state.driver_features[0])?;
w.write_u32(state.driver_features[1])?;
w.write_u32(state.queue_sel)?;
w.write_u32(state.status)?;
w.write_u32(state.interrupt_status)?;
w.write_u32(state.config_generation)?;
w.write_seq_len(state.queues.len() as u64)?;
for q in &state.queues {
w.write_u32(q.layout.size)?;
w.write_u64(q.layout.desc)?;
w.write_u64(q.layout.avail)?;
w.write_u64(q.layout.used)?;
w.write_bool(q.layout.ready)?;
w.write_u16(q.last_avail)?;
w.write_u16(q.used_idx)?;
}
drop(state);
self.regs.backend.save(w)
}
fn load(&self, r: &mut ChunkReader<'_>) -> Result<()> {
let queues = self.regs.state.lock().queues.len();
let mut state = State::new(queues);
state.device_features_sel = r.read_u32()?;
state.driver_features_sel = r.read_u32()?;
state.driver_features[0] = r.read_u32()?;
state.driver_features[1] = r.read_u32()?;
state.queue_sel = r.read_u32()?;
state.status = r.read_u32()?;
state.interrupt_status = r.read_u32()?;
state.config_generation = r.read_u32()?;
let count = r.read_seq_len(29)? as usize;
if count != queues {
return Err(Error::State(format!(
"snapshot has {count} virtqueue(s), this device has {queues}"
)));
}
for q in &mut state.queues {
q.layout.size = r.read_u32()?;
q.layout.desc = r.read_u64()?;
q.layout.avail = r.read_u64()?;
q.layout.used = r.read_u64()?;
q.layout.ready = r.read_bool()?;
q.last_avail = r.read_u16()?;
q.used_idx = r.read_u16()?;
}
let asserted = state.interrupt_status != 0;
*self.regs.state.lock() = state;
self.regs.backend.load(r)?;
self.regs.drive(asserted);
Ok(())
}
}
impl Instance for VirtioMmio {
fn bind(&self, ctx: &BindCtx<'_>) -> Result<()> {
let space = ctx.space().ok_or_else(|| Error::Config {
at: ctx.path().to_string(),
message: String::from(
"a virtio device is a bus master and needs the address space its \
descriptors live in (`space = mem`)",
),
})?;
self.attach_space(Arc::clone(space), ctx.requester());
Ok(())
}
}
#[must_use]
pub fn chain_lengths(chain: &[Descriptor]) -> (u64, u64) {
(Queue::readable_len(chain), Queue::writable_len(chain))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::space::{RamStore, Region as CoreRegion};
use crate::core::value::Width as W;
#[derive(Debug, Default)]
struct Echo {
calls: Mutex<u32>,
}
impl Backend for Echo {
fn device_id(&self) -> u32 {
0xbeef
}
fn queue_count(&self) -> usize {
1
}
fn config_read(&self, _offset: u64, dst: &mut [u8]) {
dst.fill(0xa5);
}
fn handle(&self, _queue: usize, q: &Queue<'_>, chain: &[Descriptor]) -> u32 {
*self.calls.lock() += 1;
q.write_chain(chain, 0, b"ok").unwrap_or(0) as u32
}
fn reset(&self) {
*self.calls.lock() = 0;
}
}
struct Fixture {
device: VirtioMmio,
space: Arc<AddressSpace>,
echo: Arc<Echo>,
}
static ECHO_CLASS: DeviceClass = DeviceClass {
name: "virtio.test",
version: 1,
summary: "a virtio device for the transport's own tests",
properties: &[],
construct: |_| Err(Error::Unimplemented("test only")),
};
const DESC: u64 = 0x1000;
const AVAIL: u64 = 0x2000;
const USED: u64 = 0x3000;
const BUF: u64 = 0x4000;
impl Fixture {
fn new() -> Fixture {
let echo = Arc::new(Echo::default());
let device = VirtioMmio::new(Arc::clone(&echo) as Arc<dyn Backend>, &ECHO_CLASS);
let space = AddressSpace::new("mem", 64);
space
.topology()
.map(CoreRegion::ram("ram", Arc::new(RamStore::new(0x1_0000))), 0)
.unwrap();
let space = Arc::new(space);
device.attach_space(Arc::clone(&space), RequesterId(2));
Fixture {
device,
space,
echo,
}
}
fn read(&self, offset: u64) -> u32 {
let mut bytes = [0u8; 4];
self.device
.regs
.read(offset, &mut bytes, MemAttrs::DEFAULT)
.expect("a word read is legal");
u32::from_le_bytes(bytes)
}
fn write(&self, offset: u64, value: u32) {
self.device
.regs
.write(offset, &value.to_le_bytes(), MemAttrs::DEFAULT)
.expect("a word write is legal");
}
fn poke(&self, at: u64, width: W, value: u64) {
self.space
.write(at, width, value, MemAttrs::DEFAULT)
.unwrap();
}
fn bring_up(&self) {
self.write(0x070, STATUS_ACKNOWLEDGE);
self.write(0x070, STATUS_ACKNOWLEDGE | STATUS_DRIVER);
self.write(0x024, 1);
self.write(0x020, 1); self.write(
0x070,
STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_FEATURES_OK,
);
self.write(0x030, 0);
self.write(0x038, 8);
self.write(0x080, DESC as u32);
self.write(0x084, 0);
self.write(0x090, AVAIL as u32);
self.write(0x094, 0);
self.write(0x0a0, USED as u32);
self.write(0x0a4, 0);
self.write(0x044, 1);
self.write(
0x070,
STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_FEATURES_OK | STATUS_DRIVER_OK,
);
}
fn offer(&self, idx: u16) {
self.poke(DESC, W::U64, BUF);
self.poke(DESC + 8, W::U32, 8);
self.poke(
DESC + 12,
W::U16,
u64::from(super::super::queue::DESC_F_WRITE),
);
self.poke(DESC + 14, W::U16, 0);
self.poke(AVAIL + 4, W::U16, 0);
self.poke(AVAIL + 2, W::U16, u64::from(idx));
}
}
#[test]
fn the_identity_registers_are_what_a_driver_probes_for() {
let f = Fixture::new();
assert_eq!(f.read(0x000), MAGIC);
assert_eq!(f.read(0x004), VERSION, "modern, not legacy");
assert_eq!(f.read(0x008), 0xbeef);
assert_eq!(f.read(0x00c), VENDOR_ID);
assert_eq!(f.read(0x034), QUEUE_SIZE_MAX);
}
#[test]
fn the_feature_words_are_selected_one_at_a_time() {
let f = Fixture::new();
f.write(0x014, 0);
assert_eq!(f.read(0x010), 0, "nothing in the low word");
f.write(0x014, 1);
assert_eq!(f.read(0x010), 1, "VIRTIO_F_VERSION_1 is bit 32");
f.write(0x014, 2);
assert_eq!(f.read(0x010), 0, "and nothing above 63");
}
#[test]
fn features_ok_is_refused_unless_the_driver_accepted_version_1() {
let f = Fixture::new();
f.write(0x070, STATUS_ACKNOWLEDGE | STATUS_DRIVER);
f.write(
0x070,
STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_FEATURES_OK,
);
assert_eq!(f.read(0x070) & STATUS_FEATURES_OK, 0, "refused");
f.write(0x024, 1);
f.write(0x020, 1);
f.write(
0x070,
STATUS_ACKNOWLEDGE | STATUS_DRIVER | STATUS_FEATURES_OK,
);
assert_eq!(f.read(0x070) & STATUS_FEATURES_OK, STATUS_FEATURES_OK);
}
#[test]
fn a_notify_before_driver_ok_does_nothing() {
let f = Fixture::new();
f.offer(1);
f.write(0x050, 0);
assert_eq!(*f.echo.calls.lock(), 0);
}
#[test]
fn a_notify_runs_every_available_chain_and_raises_the_interrupt() {
let f = Fixture::new();
f.bring_up();
f.offer(1);
f.write(0x050, 0);
assert_eq!(*f.echo.calls.lock(), 1);
assert_eq!(f.read(0x060), INT_USED_BUFFER);
assert!(f.device.irq_asserted());
let head = f.space.read(USED + 4, W::U32, MemAttrs::DEBUG).unwrap();
let len = f.space.read(USED + 8, W::U32, MemAttrs::DEBUG).unwrap();
assert_eq!((head, len), (0, 2));
assert_eq!(f.space.read(USED + 2, W::U16, MemAttrs::DEBUG).unwrap(), 1);
f.write(0x064, INT_USED_BUFFER);
assert_eq!(f.read(0x060), 0);
assert!(!f.device.irq_asserted());
}
#[test]
fn a_second_notify_with_nothing_new_does_no_work() {
let f = Fixture::new();
f.bring_up();
f.offer(1);
f.write(0x050, 0);
f.write(0x064, INT_USED_BUFFER);
f.write(0x050, 0);
assert_eq!(*f.echo.calls.lock(), 1, "the ring has not moved");
assert!(!f.device.irq_asserted());
}
#[test]
fn a_queue_size_that_is_not_a_power_of_two_is_refused() {
let f = Fixture::new();
f.write(0x030, 0);
f.write(0x038, 7);
f.write(0x044, 1);
assert_eq!(f.read(0x044), 1, "ready is what the driver wrote");
f.write(0x070, STATUS_DRIVER_OK);
f.write(0x050, 0);
assert_eq!(*f.echo.calls.lock(), 0);
}
#[test]
fn writing_zero_to_status_resets_everything() {
let f = Fixture::new();
f.bring_up();
f.offer(1);
f.write(0x050, 0);
assert!(f.device.irq_asserted());
f.write(0x070, 0);
assert_eq!(f.read(0x070), 0);
assert_eq!(f.read(0x060), 0);
assert!(!f.device.irq_asserted());
assert_eq!(*f.echo.calls.lock(), 0, "and the backend was told");
}
#[test]
fn the_configuration_space_is_the_backends_and_is_byte_addressable() {
let f = Fixture::new();
let mut byte = [0u8; 1];
f.device
.regs
.read(CONFIG_OFFSET + 3, &mut byte, MemAttrs::DEFAULT)
.unwrap();
assert_eq!(byte[0], 0xa5);
}
#[test]
fn a_register_access_that_is_not_an_aligned_word_is_refused() {
let f = Fixture::new();
assert!(
f.device
.regs
.read(0x002, &mut [0u8; 4], MemAttrs::DEFAULT)
.is_err()
);
assert!(
f.device
.regs
.read(0x000, &mut [0u8; 2], MemAttrs::DEFAULT)
.is_err()
);
assert!(
f.device
.regs
.write(0x070, &[0u8; 4], MemAttrs::DEBUG)
.is_err(),
"and a debug write is refused outright"
);
}
#[test]
fn a_snapshot_round_trips_the_transport() {
use crate::core::state::{MachineShape, Migrations, StateReader, StateWriter};
let saved = Fixture::new();
saved.bring_up();
saved.offer(1);
saved.write(0x050, 0);
let mut shape = MachineShape::new();
shape.add_device("vio", ECHO_CLASS.name).unwrap();
let mut w = StateWriter::new(shape);
{
let mut chunk = w.chunk("vio", ECHO_CLASS.name, ECHO_CLASS.version).unwrap();
saved.device.save(&mut chunk).unwrap();
}
let bytes = w.to_vec().unwrap();
let restored = Fixture::new();
let reader = StateReader::new(&bytes).unwrap();
let chunk = reader
.load(
"vio",
ECHO_CLASS.name,
ECHO_CLASS.version,
&Migrations::new(),
)
.unwrap();
restored.device.load(&mut chunk.reader()).unwrap();
assert_eq!(restored.read(0x070), saved.read(0x070));
assert_eq!(restored.read(0x060), INT_USED_BUFFER);
assert!(restored.device.irq_asserted());
restored.offer(1);
restored.write(0x050, 0);
assert_eq!(*restored.echo.calls.lock(), 0);
}
}