axvm 0.5.21

Virtual Machine resource management crate for ArceOS's hypervisor variant.
use alloc::vec::Vec;

use axdevice::{FwCfgInterruptConfig, FwCfgPciConfig, FwCfgSerialConfig};
use axvmconfig::{AxVMCrateConfig, EmulatedDeviceType};

use super::{
    FirmwareDevices, FlashDevice, GedDevice, GuestPlatform, InterruptTopology, IrqMmioDevice,
    MemoryRegion, MmioRegion, PciHost, SerialDevice,
};

pub struct GuestPlatformBuilder {
    ram_regions: Vec<MemoryRegion>,
    fw_cfg: MmioRegion,
    serial: Option<SerialDevice>,
    pci: Option<PciHost>,
    interrupt: Option<InterruptTopology>,
    firmware_devices: Option<FirmwareDevices>,
    irq_routes: Vec<GuestIrqRoute>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct GuestIrqRoute {
    pub physical_irq: usize,
    pub guest_vector: usize,
}

impl GuestPlatformBuilder {
    pub fn new(ram_regions: Vec<MemoryRegion>, config: &AxVMCrateConfig) -> Self {
        Self {
            ram_regions,
            fw_cfg: fw_cfg_region(config),
            serial: None,
            pci: None,
            interrupt: None,
            firmware_devices: None,
            irq_routes: Vec::new(),
        }
    }

    pub fn apply_host_acpi(mut self) -> Self {
        if let Some(result) = ax_driver::probe::acpi::with_acpi(host_acpi_resources) {
            match result {
                Ok(resources) => self.apply_host_resources(resources),
                Err(err) => warn!("failed to collect LoongArch host ACPI resources: {err:?}"),
            }
        }
        self
    }

    pub fn build(self) -> GuestPlatform {
        let defaults = QemuVirtDefaults::new();
        let serial = self.serial.unwrap_or(defaults.serial);
        let pci = self.pci.unwrap_or(defaults.pci);
        let interrupt = self.interrupt.unwrap_or(defaults.interrupt);

        GuestPlatform {
            ram_regions: self.ram_regions,
            serial,
            pci,
            interrupt,
            fw_cfg: self.fw_cfg,
            firmware_devices: self.firmware_devices.unwrap_or(defaults.firmware_devices),
            irq_routes: if self.irq_routes.is_empty() {
                defaults.irq_routes
            } else {
                self.irq_routes
            },
        }
    }

    fn apply_host_resources(&mut self, resources: HostResources) {
        if let Some(serial) = resources.serial {
            self.serial = Some(serial);
        }
        if let Some(pci) = resources.pci {
            self.pci = Some(pci);
        }
        if let Some(interrupt) = resources.interrupt {
            self.interrupt = Some(interrupt);
        }
        if let Some(firmware_devices) = resources.firmware_devices {
            self.firmware_devices = Some(firmware_devices);
        }
        self.irq_routes = resources.irq_routes;
    }
}

struct HostResources {
    serial: Option<SerialDevice>,
    pci: Option<PciHost>,
    interrupt: Option<InterruptTopology>,
    firmware_devices: Option<FirmwareDevices>,
    irq_routes: Vec<GuestIrqRoute>,
}

fn host_acpi_resources(acpi: &ax_driver::probe::acpi::System) -> ax_errno::AxResult<HostResources> {
    let defaults = QemuVirtDefaults::new();
    let interrupt = acpi
        .routing()
        .pch_pics()
        .first()
        .map(|pch_pic| InterruptTopology {
            eiointc_irq: defaults.interrupt.eiointc_irq,
            pch_pic: MmioRegion {
                base: pch_pic.address,
                size: effective_pch_pic_size(pch_pic.mmio_size),
            },
            pch_pic_gsi_base: 0,
            pch_msi: defaults.interrupt.pch_msi,
            pch_msi_start: defaults.interrupt.pch_msi_start,
            pch_msi_count: defaults.interrupt.pch_msi_count,
            acpi_gsi_base: pch_pic.gsi_base,
            acpi_msi_start: pch_pic.gsi_base,
            acpi_msi_count: defaults.interrupt.acpi_msi_count,
        });

    let serial = acpi
        .serial_console_memory_range()
        .map(|range| SerialDevice {
            mmio: MmioRegion {
                base: range.base,
                size: range.size,
            },
            irq: defaults.serial.irq,
            clock_hz: defaults.serial.clock_hz,
            baud: defaults.serial.baud,
        });

    let pci = acpi.pci_ecam_regions().first().map(|ecam| PciHost {
        ecam: MmioRegion {
            base: ecam.base_address,
            size: ecam.size() as u64,
        },
        mmio: defaults.pci.mmio,
        io_base: defaults.pci.io_base,
        io_size: defaults.pci.io_size,
        intx_base: defaults.pci.intx_base,
    });

    let irq_routes = guest_irq_routes(
        interrupt.as_ref().unwrap_or(&defaults.interrupt),
        &serial,
        &pci,
    );
    let firmware_devices = Some(find_firmware_devices(acpi, defaults.firmware_devices));

    Ok(HostResources {
        serial,
        pci,
        interrupt,
        firmware_devices,
        irq_routes,
    })
}

fn effective_pch_pic_size(size: u16) -> u64 {
    if size == 0 { 0x1000 } else { u64::from(size) }
}

fn find_firmware_devices(
    acpi: &ax_driver::probe::acpi::System,
    mut devices: FirmwareDevices,
) -> FirmwareDevices {
    if let Some(rtc) = find_rtc(acpi) {
        devices.rtc = rtc;
    }
    devices
}

fn find_rtc(acpi: &ax_driver::probe::acpi::System) -> Option<IrqMmioDevice> {
    let devices = acpi.resource_devices().ok()?;
    devices.into_iter().find_map(|device| {
        let is_rtc = device.hid.as_deref() == Some("LOON0001")
            || device.cids.iter().any(|cid| cid == "LOON0001")
            || device.path.contains("RTC");
        if !is_rtc {
            return None;
        }
        let range = device.memory_ranges.first()?;
        let irq = device
            .irq_routes
            .first()
            .map(|route| u32::from(route.controller_input))
            .unwrap_or(defaults_rtc_irq());
        Some(IrqMmioDevice {
            mmio: MmioRegion {
                base: range.base,
                size: range.size,
            },
            irq,
        })
    })
}

fn defaults_rtc_irq() -> u32 {
    6
}

fn guest_irq_routes(
    interrupt: &InterruptTopology,
    serial: &Option<SerialDevice>,
    pci: &Option<PciHost>,
) -> Vec<GuestIrqRoute> {
    let defaults = QemuVirtDefaults::new();
    let serial = serial.unwrap_or(defaults.serial);
    let pci = pci.unwrap_or(defaults.pci);

    let mut routes = Vec::from([GuestIrqRoute {
        physical_irq: serial.irq as usize,
        guest_vector: serial.irq as usize,
    }]);
    routes.extend((0..4).map(|idx| GuestIrqRoute {
        physical_irq: pci.intx_base as usize + idx,
        guest_vector: pci.intx_base as usize + idx,
    }));

    let _ = interrupt;
    routes
}

fn fw_cfg_region(config: &AxVMCrateConfig) -> MmioRegion {
    if let Some(fw_cfg) = config
        .devices
        .emu_devices
        .iter()
        .find(|device| device.emu_type == EmulatedDeviceType::FwCfg)
    {
        return MmioRegion {
            base: fw_cfg.base_gpa as u64,
            size: fw_cfg.length as u64,
        };
    }

    QemuVirtDefaults::new().fw_cfg
}

struct QemuVirtDefaults {
    serial: SerialDevice,
    pci: PciHost,
    interrupt: InterruptTopology,
    fw_cfg: MmioRegion,
    firmware_devices: FirmwareDevices,
    irq_routes: Vec<GuestIrqRoute>,
}

impl QemuVirtDefaults {
    fn new() -> Self {
        let serial = SerialDevice {
            mmio: MmioRegion {
                base: FwCfgSerialConfig::default().base,
                size: FwCfgSerialConfig::default().size,
            },
            irq: 2,
            clock_hz: FwCfgSerialConfig::default().clock_hz,
            baud: FwCfgSerialConfig::default().baud,
        };
        let pci = PciHost {
            ecam: MmioRegion {
                base: FwCfgPciConfig::default().ecam_base,
                size: FwCfgPciConfig::default().ecam_size,
            },
            mmio: MmioRegion {
                base: FwCfgPciConfig::default().mmio_base,
                size: FwCfgPciConfig::default().mmio_size,
            },
            io_base: FwCfgPciConfig::default().io_base,
            io_size: u64::from(FwCfgPciConfig::default().io_size),
            intx_base: 16,
        };
        let interrupt = InterruptTopology {
            eiointc_irq: FwCfgInterruptConfig::default().eiointc_irq as u32,
            pch_pic: MmioRegion {
                base: FwCfgInterruptConfig::default().pch_pic_base,
                size: u64::from(FwCfgInterruptConfig::default().pch_pic_size),
            },
            pch_pic_gsi_base: 0,
            pch_msi: MmioRegion {
                base: FwCfgInterruptConfig::default().pch_msi_base,
                size: 0x8,
            },
            pch_msi_start: 0x20,
            pch_msi_count: 0xe0,
            acpi_gsi_base: u32::from(FwCfgInterruptConfig::default().pch_pic_gsi_base),
            acpi_msi_start: FwCfgInterruptConfig::default().pch_msi_start,
            acpi_msi_count: FwCfgInterruptConfig::default().pch_msi_count,
        };
        let irq_routes = Vec::from([
            GuestIrqRoute {
                physical_irq: serial.irq as usize,
                guest_vector: serial.irq as usize,
            },
            GuestIrqRoute {
                physical_irq: pci.intx_base as usize,
                guest_vector: pci.intx_base as usize,
            },
            GuestIrqRoute {
                physical_irq: pci.intx_base as usize + 1,
                guest_vector: pci.intx_base as usize + 1,
            },
            GuestIrqRoute {
                physical_irq: pci.intx_base as usize + 2,
                guest_vector: pci.intx_base as usize + 2,
            },
            GuestIrqRoute {
                physical_irq: pci.intx_base as usize + 3,
                guest_vector: pci.intx_base as usize + 3,
            },
        ]);
        Self {
            serial,
            pci,
            interrupt,
            fw_cfg: MmioRegion {
                base: 0x1e02_0000,
                size: 0x18,
            },
            firmware_devices: FirmwareDevices {
                rtc: IrqMmioDevice {
                    mmio: MmioRegion {
                        base: 0x100d_0100,
                        size: 0x100,
                    },
                    irq: defaults_rtc_irq(),
                },
                flash: FlashDevice {
                    banks: [
                        MmioRegion {
                            base: 0x1c00_0000,
                            size: 0x0100_0000,
                        },
                        MmioRegion {
                            base: 0x1d00_0000,
                            size: 0x0100_0000,
                        },
                    ],
                    bank_width: 4,
                },
                ged: GedDevice {
                    mmio: MmioRegion {
                        base: 0x100e_001c,
                        size: 3,
                    },
                    poweroff_offset: 0,
                    poweroff_value: 0x34,
                    reboot_offset: 2,
                    reboot_value: 0x42,
                },
            },
            irq_routes,
        }
    }
}