use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use arcbox_hv::{ExceptionClass, HvVcpu, MmioInfo, VcpuExit};
use super::hvc_blk::{
ARCBOX_HVC_BLK_CAPACITY, ARCBOX_HVC_BLK_FLUSH, ARCBOX_HVC_BLK_READ, ARCBOX_HVC_BLK_WRITE,
ARCBOX_HVC_PROBE, handle_hvc_blk_capacity, handle_hvc_blk_flush, handle_hvc_blk_io,
};
use super::psci::{CpuPower, PsciExit, handle_psci};
use super::{HvVcpuIds, Pl011, Pl031, VcpuThreadHandles};
pub(super) mod reg {
pub use arcbox_hv::reg::{
HV_REG_CPSR as CPSR, HV_REG_PC as PC, HV_REG_X0 as X0, HV_REG_X1 as X1, HV_REG_X2 as X2,
HV_REG_X3 as X3,
};
}
const CPSR_EL1H: u64 = 0x3C5;
const SCTLR_EL1_RESET: u64 = (1 << 11) | (1 << 20) | (1 << 22) | (1 << 23) | (1 << 28) | (1 << 29);
pub(super) struct VcpuContext {
pub device_manager: Arc<crate::device::DeviceManager>,
pub running: Arc<AtomicBool>,
pub reset_requested: Arc<AtomicBool>,
pub paused: Arc<AtomicBool>,
pub pl011: Arc<std::sync::Mutex<Pl011>>,
pub pl031: Arc<std::sync::Mutex<Pl031>>,
pub cpu_power: Option<CpuPower>,
pub vcpu_thread_handles: VcpuThreadHandles,
pub hv_vcpu_ids: HvVcpuIds,
pub hvc_blk_fds: Arc<Vec<(i32, u32, u64)>>,
pub stats: Arc<crate::vcpu_stats::VcpuStats>,
}
fn read_mmio_write_reg(vcpu: &HvVcpu, vcpu_id: u32, register: u8) -> Option<u64> {
if register == 31 {
return Some(0);
}
match vcpu.get_reg(u32::from(register)) {
Ok(v) => Some(v),
Err(e) => {
tracing::error!("vCPU {vcpu_id}: get_reg(X{register}) failed: {e}");
None
}
}
}
fn sign_extend_mmio(value: u64, access_size: u8, sixty_four: bool) -> u64 {
let bits = u32::from(access_size) * 8;
if bits == 0 || bits >= 64 {
return value;
}
if sixty_four {
let shift = 64 - bits;
#[allow(clippy::cast_possible_wrap)]
let signed = (value << shift) as i64 >> shift;
signed as u64
} else if bits >= 32 {
u64::from(value as u32)
} else {
let shift = 32 - bits;
#[allow(clippy::cast_possible_wrap, clippy::cast_possible_truncation)]
let signed = ((value as u32) << shift) as i32 >> shift;
u64::from(signed as u32)
}
}
fn complete_mmio_read(vcpu: &HvVcpu, vcpu_id: u32, mmio: &MmioInfo, raw: u64) {
if mmio.register == 31 {
return;
}
let value = if mmio.sign_extend {
sign_extend_mmio(raw, mmio.access_size, mmio.sixty_four)
} else {
raw
};
if let Err(e) = vcpu.set_reg(u32::from(mmio.register), value) {
tracing::error!("vCPU {vcpu_id}: set_reg(X{}) failed: {e}", mmio.register);
}
}
#[derive(Debug, PartialEq, Eq)]
enum VcpuLoopExit {
Shutdown,
CpuOff,
}
pub(super) enum VcpuBoot {
Immediate { entry: u64, x0: u64 },
AwaitCpuOn(std::sync::mpsc::Receiver<super::psci::CpuOnRequest>),
}
pub(super) fn vcpu_run_loop(vcpu_id: u32, boot: VcpuBoot, ctx: VcpuContext) {
let VcpuContext {
device_manager,
running,
reset_requested,
paused,
pl011,
pl031,
cpu_power,
vcpu_thread_handles,
hv_vcpu_ids,
hvc_blk_fds,
stats,
} = ctx;
let vcpu = match HvVcpu::new() {
Ok(v) => v,
Err(e) => {
tracing::error!("vCPU {vcpu_id}: creation failed: {e}");
return;
}
};
let mpidr = u64::from(vcpu_id) & 0xFF;
if let Err(e) = vcpu.set_sys_reg(arcbox_hv::sys_reg::HV_SYS_REG_MPIDR_EL1, mpidr) {
tracing::error!("vCPU {vcpu_id}: set MPIDR_EL1 failed: {e}; aborting boot");
running.store(false, Ordering::SeqCst);
return;
}
match vcpu.get_sys_reg(arcbox_hv::sys_reg::HV_SYS_REG_MPIDR_EL1) {
Ok(v) if v & 0xFF == u64::from(vcpu_id) => {}
Ok(v) => {
tracing::error!(
"vCPU {vcpu_id}: MPIDR_EL1 Aff0 readback {:#x} != {vcpu_id}; aborting boot \
(GIC affinity would be wrong)",
v & 0xFF,
);
running.store(false, Ordering::SeqCst);
return;
}
Err(e) => {
tracing::error!("vCPU {vcpu_id}: MPIDR_EL1 readback failed: {e}; aborting boot");
running.store(false, Ordering::SeqCst);
return;
}
}
{
let mut ids = hv_vcpu_ids
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
ids.push(vcpu.raw_handle());
}
{
let mut handles = vcpu_thread_handles
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
handles.push(std::thread::current());
}
let (cpu_on_rx, mut entry_addr, mut x0_value) = match boot {
VcpuBoot::Immediate { entry, x0 } => (None, entry, x0),
VcpuBoot::AwaitCpuOn(rx) => match rx.recv() {
Ok(req) => {
tracing::info!(
"vCPU {vcpu_id}: received CPU_ON, starting at {:#x}",
req.entry_point
);
let (entry, x0) = (req.entry_point, req.context_id);
(Some(rx), entry, x0)
}
Err(_) => {
tracing::debug!("vCPU {vcpu_id}: power channel closed, never started");
return;
}
},
};
loop {
if let Err(e) = vcpu.set_reg(reg::PC, entry_addr) {
tracing::error!("vCPU {vcpu_id}: set PC failed: {e}");
return;
}
if let Err(e) = vcpu.set_reg(reg::X0, x0_value) {
tracing::error!("vCPU {vcpu_id}: set X0 failed: {e}");
return;
}
let _ = vcpu.set_reg(reg::X1, 0);
let _ = vcpu.set_reg(reg::X2, 0);
let _ = vcpu.set_reg(reg::X3, 0);
if let Err(e) = vcpu.set_reg(reg::CPSR, CPSR_EL1H) {
tracing::error!("vCPU {vcpu_id}: set CPSR failed: {e}");
return;
}
if let Err(e) = vcpu.set_sys_reg(arcbox_hv::sys_reg::HV_SYS_REG_SCTLR_EL1, SCTLR_EL1_RESET)
{
tracing::warn!("vCPU {vcpu_id}: set SCTLR_EL1 failed: {e}");
}
let _ = vcpu.set_vtimer_mask(false);
tracing::info!(
"vCPU {vcpu_id}: starting at PC={:#x}, X0={:#x}, SCTLR={:#x}",
entry_addr,
x0_value,
SCTLR_EL1_RESET,
);
let mut loop_exit = VcpuLoopExit::Shutdown;
loop {
if !running.load(Ordering::Relaxed) {
tracing::info!("vCPU {vcpu_id}: shutdown requested");
break;
}
while paused.load(Ordering::Acquire) && running.load(Ordering::Relaxed) {
std::thread::park();
}
if !running.load(Ordering::Relaxed) {
tracing::info!("vCPU {vcpu_id}: shutdown observed after pause");
break;
}
if vcpu_id == 0 && device_manager.poll_bridge_rx() {
if let Some(bid) = device_manager.bridge_device_id() {
device_manager.raise_interrupt_for_device(bid, 1);
}
}
if !running.load(Ordering::Relaxed) {
tracing::trace!("vCPU {vcpu_id}: iteration during shutdown (before vcpu.run)");
}
let exit = match vcpu.run() {
Ok(e) => e,
Err(e) => {
tracing::error!("vCPU {vcpu_id}: run failed: {e}");
running.store(false, Ordering::SeqCst);
break;
}
};
if !running.load(Ordering::Relaxed) {
tracing::trace!(
"vCPU {vcpu_id}: vcpu.run returned during shutdown, exit={:?}",
core::mem::discriminant(&exit)
);
}
match exit {
VcpuExit::Exception {
class: ExceptionClass::DataAbort(ref mmio),
..
} => {
crate::vcpu_stats::VcpuStats::bump(if mmio.is_write {
&stats.mmio_writes
} else {
&stats.mmio_reads
});
if !mmio.isv {
tracing::error!(
"vCPU {vcpu_id}: undecodable MMIO {} at {:#x} (ESR ISV=0) — skipping instruction",
if mmio.is_write { "write" } else { "read" },
mmio.address,
);
let pc = vcpu.get_reg(reg::PC).unwrap_or(0);
let _ = vcpu.set_reg(reg::PC, pc.wrapping_add(4));
continue;
}
let handled_by_pl011 = {
let uart_match = {
let guard = pl011.lock().unwrap();
guard.contains(mmio.address)
};
if uart_match {
if mmio.is_write {
let value =
read_mmio_write_reg(&vcpu, vcpu_id, mmio.register).unwrap_or(0);
pl011.lock().unwrap().write(
mmio.address,
mmio.access_size as usize,
value,
);
} else {
let value = pl011
.lock()
.unwrap()
.read(mmio.address, mmio.access_size as usize);
complete_mmio_read(&vcpu, vcpu_id, mmio, value);
}
true
} else {
false
}
};
let handled_by_pl031 = !handled_by_pl011 && {
let rtc_match = {
let guard = pl031.lock().unwrap();
guard.contains(mmio.address)
};
if rtc_match {
if mmio.is_write {
let value =
read_mmio_write_reg(&vcpu, vcpu_id, mmio.register).unwrap_or(0);
pl031.lock().unwrap().write(
mmio.address,
mmio.access_size as usize,
value,
);
} else {
let value = pl031
.lock()
.unwrap()
.read(mmio.address, mmio.access_size as usize);
complete_mmio_read(&vcpu, vcpu_id, mmio, value);
}
true
} else {
false
}
};
if !handled_by_pl011 && !handled_by_pl031 {
if mmio.is_write {
let Some(value) = read_mmio_write_reg(&vcpu, vcpu_id, mmio.register)
else {
let pc = vcpu.get_reg(reg::PC).unwrap_or(0);
let _ = vcpu.set_reg(reg::PC, pc + 4);
continue;
};
tracing::trace!(
"MMIO write: addr={:#x} offset={:#x} X{}={:#x} size={}",
mmio.address,
mmio.address.saturating_sub(
mmio.address & !0xFFF ),
mmio.register,
value,
mmio.access_size,
);
if let Err(e) = device_manager.handle_mmio_write(
mmio.address,
mmio.access_size as usize,
value,
) {
tracing::warn!(
"vCPU {vcpu_id}: MMIO write {:#x} failed: {e}",
mmio.address
);
}
} else {
let value = match device_manager
.handle_mmio_read(mmio.address, mmio.access_size as usize)
{
Ok(v) => v,
Err(e) => {
tracing::warn!(
"vCPU {vcpu_id}: MMIO read {:#x} failed: {e}",
mmio.address
);
0 }
};
complete_mmio_read(&vcpu, vcpu_id, mmio, value);
}
}
let pc = vcpu.get_reg(reg::PC).unwrap_or(0);
let _ = vcpu.set_reg(reg::PC, pc + 4);
}
VcpuExit::Exception {
class: ExceptionClass::WaitForInterrupt,
..
} => {
crate::vcpu_stats::VcpuStats::bump(&stats.wfi);
let wfi_has_bridge = device_manager.poll_bridge_rx();
if wfi_has_bridge {
if let Some(bid) = device_manager.bridge_device_id() {
device_manager.raise_interrupt_for_device(bid, 1);
}
continue; }
std::thread::park_timeout(std::time::Duration::from_millis(1));
if !running.load(Ordering::Relaxed) {
break;
}
}
VcpuExit::Exception {
class: ExceptionClass::HypercallHvc(_imm),
..
} => {
crate::vcpu_stats::VcpuStats::bump(&stats.hvc);
let func_id = match vcpu.get_reg(reg::X0) {
Ok(v) => v,
Err(_) => continue,
};
match func_id {
ARCBOX_HVC_PROBE => {
let _ = vcpu.set_reg(reg::X0, hvc_blk_fds.len() as u64);
}
ARCBOX_HVC_BLK_READ => {
let result =
handle_hvc_blk_io(&vcpu, &hvc_blk_fds, &device_manager, false);
let _ = vcpu.set_reg(reg::X0, result);
}
ARCBOX_HVC_BLK_WRITE => {
let result =
handle_hvc_blk_io(&vcpu, &hvc_blk_fds, &device_manager, true);
let _ = vcpu.set_reg(reg::X0, result);
}
ARCBOX_HVC_BLK_FLUSH => {
let result = handle_hvc_blk_flush(&vcpu, &hvc_blk_fds);
let _ = vcpu.set_reg(reg::X0, result);
}
ARCBOX_HVC_BLK_CAPACITY => {
let result = handle_hvc_blk_capacity(&vcpu, &hvc_blk_fds);
let _ = vcpu.set_reg(reg::X0, result);
}
_ => {
let psci_exit = handle_psci(
vcpu_id,
func_id,
&vcpu,
&running,
&reset_requested,
cpu_power.as_ref(),
);
if !running.load(Ordering::Relaxed) {
break;
}
if psci_exit == PsciExit::CpuOff {
loop_exit = VcpuLoopExit::CpuOff;
break;
}
}
}
}
VcpuExit::Exception {
class: ExceptionClass::SmcCall(_),
..
} => {
crate::vcpu_stats::VcpuStats::bump(&stats.smc);
let func_id = match vcpu.get_reg(reg::X0) {
Ok(v) => v,
Err(_) => continue,
};
let psci_exit = handle_psci(
vcpu_id,
func_id,
&vcpu,
&running,
&reset_requested,
cpu_power.as_ref(),
);
if !running.load(Ordering::Relaxed) {
break;
}
if psci_exit == PsciExit::CpuOff {
loop_exit = VcpuLoopExit::CpuOff;
break;
}
}
VcpuExit::VtimerActivated => {
crate::vcpu_stats::VcpuStats::bump(&stats.vtimer);
let _ = vcpu.set_vtimer_mask(false);
}
VcpuExit::Canceled => {
crate::vcpu_stats::VcpuStats::bump(&stats.kicks_received);
if running.load(Ordering::Relaxed) {
continue;
}
tracing::info!("vCPU {vcpu_id}: canceled (shutdown)");
break;
}
VcpuExit::Exception {
class:
ExceptionClass::SystemRegister {
op0,
op1,
crn,
crm,
op2,
is_write,
rt,
},
..
} => {
crate::vcpu_stats::VcpuStats::bump(&stats.sysreg);
if !is_write && rt != 31 {
let _ = vcpu.set_reg(u32::from(rt), 0);
}
if let Ok(pc) = vcpu.get_reg(reg::PC) {
let _ = vcpu.set_reg(reg::PC, pc.wrapping_add(4));
}
tracing::trace!(
vcpu_id,
is_write,
encoding = %format_args!("S{op0}_{op1}_C{crn}_C{crm}_{op2}"),
rt,
"sysreg access treated as RAZ/WI; PC advanced"
);
}
VcpuExit::Exception {
class: ref other, ..
} => {
crate::vcpu_stats::VcpuStats::bump(&stats.other);
tracing::warn!("vCPU {vcpu_id}: unhandled exception: {other:?}");
}
VcpuExit::Unknown(reason) => {
crate::vcpu_stats::VcpuStats::bump(&stats.other);
tracing::warn!("vCPU {vcpu_id}: unknown exit reason {reason}");
}
}
}
pl011.lock().unwrap().flush();
if loop_exit != VcpuLoopExit::CpuOff {
tracing::info!("vCPU {vcpu_id}: exited");
return;
}
tracing::info!("vCPU {vcpu_id}: offline (CPU_OFF), awaiting CPU_ON");
let Some(rx) = cpu_on_rx.as_ref() else {
tracing::error!("vCPU {vcpu_id}: CPU_OFF without a power channel");
return;
};
match rx.recv() {
Ok(req) => {
tracing::info!(
"vCPU {vcpu_id}: received CPU_ON, starting at {:#x}",
req.entry_point
);
entry_addr = req.entry_point;
x0_value = req.context_id;
}
Err(_) => {
tracing::debug!("vCPU {vcpu_id}: power channel closed while offline");
return;
}
}
}
}
#[cfg(test)]
mod tests {
use super::sign_extend_mmio;
#[test]
fn ldrsb_to_x_sign_extends_to_64() {
assert_eq!(sign_extend_mmio(0x80, 1, true), 0xFFFF_FFFF_FFFF_FF80);
assert_eq!(sign_extend_mmio(0x7F, 1, true), 0x7F);
}
#[test]
fn ldrsb_to_w_sign_extends_within_32_then_zero_extends() {
assert_eq!(sign_extend_mmio(0x80, 1, false), 0x0000_0000_FFFF_FF80);
assert_eq!(sign_extend_mmio(0x7F, 1, false), 0x7F);
}
#[test]
fn ldrsh_sign_extends_halfword() {
assert_eq!(sign_extend_mmio(0x8000, 2, true), 0xFFFF_FFFF_FFFF_8000);
assert_eq!(sign_extend_mmio(0x8000, 2, false), 0x0000_0000_FFFF_8000);
assert_eq!(sign_extend_mmio(0x1234, 2, true), 0x1234);
}
#[test]
fn ldrsw_sign_extends_word_to_64() {
assert_eq!(
sign_extend_mmio(0x8000_0000, 4, true),
0xFFFF_FFFF_8000_0000
);
assert_eq!(sign_extend_mmio(0x0000_0001, 4, true), 0x1);
}
#[test]
fn full_and_wide_widths_are_passthrough() {
assert_eq!(
sign_extend_mmio(0xFFFF_FFFF_FFFF_FF80, 8, true),
0xFFFF_FFFF_FFFF_FF80
);
assert_eq!(
sign_extend_mmio(0x8000_0000, 4, false),
0x0000_0000_8000_0000
);
}
}