diff --git a/os/arceos/modules/axruntime/runtime.ld b/os/arceos/modules/axruntime/runtime.ld index 3be0306aca..b6b08ce9db 100644 --- a/os/arceos/modules/axruntime/runtime.ld +++ b/os/arceos/modules/axruntime/runtime.ld @@ -12,6 +12,11 @@ SECTIONS { KEEP(*(.driver.register*)) __edriver_register = .; + . = ALIGN(0x10); + __saxdevice_factory = .; + KEEP(*(.axdevice.factory*)) + __eaxdevice_factory = .; + . = ALIGN(0x10); _ex_table_start = .; KEEP(*(__ex_table)) diff --git a/os/axvisor/configs/vms/qemu/aarch64/linux-smp1-gppt-gic.toml b/os/axvisor/configs/vms/qemu/aarch64/linux-smp1-gppt-gic.toml new file mode 100644 index 0000000000..570974bc03 --- /dev/null +++ b/os/axvisor/configs/vms/qemu/aarch64/linux-smp1-gppt-gic.toml @@ -0,0 +1,71 @@ +# Vm base info configs +# +[base] +# Guest vm id. +id = 1 +# Guest vm name. +name = "linux-qemu-gppt-gic" +# Virtualization type. +vm_type = 1 +# The number of virtual CPUs. +cpu_num = 1 +# Guest vm physical cpu sets. +phys_cpu_ids = [0] + +# +# Vm kernel configs +# +[kernel] +# The entry point of the kernel image. +entry_point = 0x8020_0000 +# The location of image: "memory" | "fs". +# Load from file system. +image_location = "fs" +# The file path of the kernel image. +kernel_path = "/guest/linux/linux-qemu" +# The load address of the kernel image. +kernel_load_addr = 0x8020_0000 +# The file path of the device tree blob (DTB). +# dtb_path = "tmp/linux-aarch64-qemu-smp1.dtb" +# The load address of the device tree blob (DTB). +dtb_load_addr = 0x8000_0000 + +# Memory regions with format (`base_paddr`, `size`, `flags`, `map_type`). +# For `map_type`, 0 means `MAP_ALLOC`, 1 means `MAP_IDENTICAL`, 2 means `MAP_RESERVED`. +memory_regions = [ + [0x8000_0000, 0x1000_0000, 0x7, 1], # System RAM 1G MAP_IDENTICAL +] + +# +# Device specifications +# +[devices] +# The interrupt mode. +interrupt_mode = "passthrough" + +# Pass-through devices. +# Name Base-Ipa Base-Pa Length Alloc-Irq. +passthrough_devices = [ + ["/"], + #["/timer"], +] + +# Passthrough addresses. +# Base-GPA Length. +passthrough_addresses = [ + #[0x28041000, 0x100_0000] +] + +# Devices that are not desired to be passed through to the guest +excluded_devices = [ + # ["/gic-v3"], +] + +# Emu_devices. +# Name Base-Ipa Ipa_len Alloc-Irq Emu-Type EmuConfig. +emu_devices = [ + # Keep this case focused on the first native DeviceOps migration target. + # ["gppt-gicd", 0x0800_0000, 0x1_0000, 0, 0x21, []], + ["gppt-gicr", 0x080a_0000, 0x2_0000, 0, 0x20, [1, 0x2_0000, 0]], # 1 vcpu, stride 0x20000, starts with pcpu 0 + # ["gppt-gits", 0x0808_0000, 0x2_0000, 0, 0x22, [0x0808_0000]], # host_gits_base +] diff --git a/study_records.md b/study_records.md new file mode 100644 index 0000000000..a8feac337a --- /dev/null +++ b/study_records.md @@ -0,0 +1,387 @@ +# 学习记录 + +## 设备抽象重构当前判断 + +AxVisor 当前设备框架已经进入从 legacy 设备模型迁移到统一 `DeviceOps` 模型的阶段。核心目标不是简单把 `AxVmDevices::init` 里的 `match` 移到另一个文件,而是把设备模型拆成几层职责: + +```text +设备本体:声明资源、能力、访问语义和生命周期 +factory:根据 VM 配置创建设备实例 +catalog:描述某个架构/平台可创建哪些设备 +registry:保存已创建设备并负责总线路由和资源冲突检查 +VM exit:只生成 BusAccess,不关心具体设备类型 +``` + +当前已经形成的基础设施: + +- `axdevice_base` 已承载基础模型:`DeviceOps`、`DeviceId`、`DeviceError`、`Resource`、`DeviceCapabilities`、`BusAccess`、`BusResponse`、`IrqSink` 等。 +- `axdevice` 已有 `DeviceRegistry`,可以根据 `Resource::Mmio/Pio/SysReg` 建立路由,并做重复 `DeviceId` 和 bus resource 冲突检查。 +- `LegacyDeviceAdapter` 已能把旧 `BaseMmioDeviceOps`、`BasePortDeviceOps`、`BaseSysRegDeviceOps` 适配成 `DeviceOps`。 +- `AxVmDevices::add_*_dev` 当前仍保留旧 `Vec`,同时把 legacy 设备注册进 `DeviceRegistry`。 +- 普通 MMIO/PIO/SysReg 访问入口已经可以收敛到 `DeviceRegistry::dispatch()`。 + +因此,后续新增 factory 时,不应该再把 factory API 设计成返回 `BaseDeviceOps`。`BaseDeviceOps` 是迁移期兼容层,不应该反向塑造新接口。 + +## 直接实现 DeviceOps 的原则 + +新的设备迁移原则是:**进入 factory 迁移范围的设备,默认直接由具体设备类型实现 `DeviceOps`**。 + +也就是说,目标形态应当是: + +```text +EmulatedDeviceConfig + -> DeviceFactory::build(ctx, config) + -> Rc + -> AxVmDevices::register_device() + -> DeviceRegistry +``` + +不推荐的形态是: + +```text +EmulatedDeviceConfig + -> DeviceFactory::build(...) + -> BaseDeviceOps + -> AxVmDevices 再包 LegacyDeviceAdapter +``` + +直接实现 `DeviceOps` 的好处: + +- 设备自己的资源和能力声明留在设备代码里,而不是散落在 factory 或 wrapper 中。 +- factory 只负责解析配置并调用设备构造函数,逻辑足够薄。 +- 新增设备时主要修改设备 crate 和对应平台 catalog,不需要修改 `AxVmDevices::init`、`DeviceRegistry` 或总线分发核心。 +- 可以逐步减少 `LegacyDeviceAdapter` 的使用范围,而不是让 legacy 过渡状态固化成新架构。 + +直接实现 `DeviceOps` 时,设备结构需要持有 registry metadata,例如: + +```text +DeviceId +name +Vec +DeviceCapabilities +``` + +为了减少重复样板,后续可以在 `axdevice_base` 中增加一个轻量 `DeviceMeta` helper。具体设备可以包含 `meta: DeviceMeta`,并在 `DeviceOps` 的 `id/name/resources/capabilities` 中直接转发。这个 helper 只是减少样板,不改变“设备本体直接实现 DeviceOps”的方向。 + +只有在少数场景下才考虑临时 wrapper: + +- 设备结构来自外部 crate,短期不方便改字段。 +- 设备内部状态生命周期和 registry metadata 确实不适合放在同一个结构里。 +- 迁移过程中需要极短期桥接,且计划明确后续删除。 + +wrapper 不是默认路线。 + +## legacy 路径的定位 + +迁移过程中仍然需要保留 legacy 路径,但它的定位要清楚: + +```text +LegacyDeviceAdapter:未迁移设备接入 DeviceRegistry 的兼容层 +旧 MMIO/PIO/SysReg Vec:typed control path 和迁移期兼容层 +``` + +它们不再是普通 bus 访问的目标路径。 + +旧 `Vec` 当前仍有实际用途: + +- AArch64 可能通过遍历 MMIO 设备找到 `VGicD` 后执行 `assign_irq()`。 +- RISC-V 可能通过查找 vPLIC 设备设置 pending。 +- x86 当前还有 IOAPIC/PIT/serial 的专用句柄和控制路径。 + +这些行为不是普通 MMIO/PIO/SysReg 访问,而是架构控制路径。后续应逐步迁到更明确的 `InterruptRouter`、`IrqSink` 或 platform service,而不是强行用 bus dispatch 表达。 + +## catalog 的位置 + +仍然需要维护一个“可构造设备集合”,也就是 factory catalog。原因很简单:配置里只有设备类型和参数,系统必须知道哪个构造函数能处理它。 + +但 catalog 不应该放进 `axdevice` 核心并重新形成中心化大表。建议边界是: + +```text +axdevice_base: + 定义 DeviceOps、Resource、BusAccess、DeviceFactory 等具体设备 crate 需要依赖的基础协议。 + +axdevice: + 定义 AxVmDevices、DeviceRegistry、注册流程、catalog 消费逻辑。 + 不直接知道 VGicD、Gits、vPLIC、IOAPIC 等具体类型。 + +架构/平台 glue: + 组合该平台支持的 factory catalog,例如 AArch64 catalog 包含 VGIC/GPPT 相关 factory。 + +具体设备 crate: + 直接实现 DeviceOps,并导出自己的 factory 或 factory entry。 +``` + +这样新增设备时,需要修改的是设备 crate 和平台 catalog,而不是修改核心 registry/router。 + +长期如果确实需要完全插件式自动发现,可以评估 linker-section 自动注册。但在 no_std/hypervisor 场景中,显式 catalog 更稳,链接脚本、LTO、多架构兼容和测试成本都更可控。 + +## 下一阶段路线 + +下一阶段建议围绕 AArch64 主线做 factory 和原生 `DeviceOps` 迁移: + +```text +1. 定义 DeviceFactory / DeviceFactoryCatalog / DeviceBuildContext。 +2. 让 factory 返回 Vec>,不暴露 BaseDeviceOps。 +3. 具体设备默认直接实现 DeviceOps;必要时补 DeviceMeta helper 减少样板。 +4. 由 AArch64 平台 glue 组合 VGIC/GPPT factory catalog。 +5. AxVmDevices::init 从“match 具体设备类型并构造”逐步变成“config -> catalog -> factory -> register_device”。 +6. 保留 legacy fallback 服务未迁移设备,直到对应设备完成原生化。 +``` + +AArch64 侧优先迁移这些当前 `AxVmDevices::init` 中的设备: + +- `EmulatedDeviceType::GPPTDistributor` -> `arm_vgic::v3::vgicd::VGicD` +- `EmulatedDeviceType::GPPTRedistributor` -> 多个 `arm_vgic::v3::vgicr::VGicR` +- `EmulatedDeviceType::GPPTITS` -> `arm_vgic::v3::gits::Gits` +- `EmulatedDeviceType::InterruptController` -> `arm_vgic::Vgic` + +其中 `GPPTRedistributor` 需要注意:一个 config 会根据 `cpu_num/stride/pcpu_id` 展开成多个设备实例,所以 factory 返回值必须支持多个 `Rc`。 + +`IVCChannel` 暂时不要塞进普通 `DeviceOps` factory。它不是 bus device,而是 VM 级 meta resource/allocator。可以后续单独设计 VM resource initializer,避免污染设备 factory 模型。 + +## AArch64 VGicR 迁移验证记录 + +本阶段已经以 AArch64 主线的 `GPPTRedistributor -> VGicR` 作为第一条设备迁移样例,验证了 factory/native `DeviceOps` 路径是可行的。 + +实际落地后的路径是: + +```text +EmulatedDeviceConfig(GPPTRedistributor) + -> AArch64 DeviceFactoryCatalog + -> GpptRedistributorFactory::build() + -> VGicR::new(DeviceMeta, ...) + -> Rc + -> AxVmDevices::register_device() + -> DeviceRegistry +``` + +这次迁移中确认了几个重要原则: + +- factory 直接返回 `Vec>`,不再返回 `BaseDeviceOps`。 +- `VGicR` 本体直接实现 `DeviceOps`,不再通过 wrapper 进入新模型。 +- `VGicR` 删除了 `legacy_meta`、`new_with_meta` 和 `impl BaseDeviceOps`。 +- 原 `BaseDeviceOps` 中的 `address_range/read/write` 逻辑改为 `VGicR` 自己的私有方法,再由 `DeviceOps::access()` 调用。 +- 设备 metadata 和资源声明由设备/factory 自己构造,核心容器只消费 `DeviceOps` 和 `Resource`。 +- `AxVmDevices::init` 对已迁移设备只负责 catalog 查找、factory build、registry register,不再关心具体设备构造细节。 + +为了验证这条迁移链路,新增了一个专用测试组: + +```text +test-suit/axvisor/aarch64-device-migration/qemu +``` + +对应命令: + +```bash +cargo xtask axvisor test qemu --arch aarch64 --test-group aarch64-device-migration --test-case smoke > tmp.log +``` + +当前验证配置只打开 `gppt-gicr`,暂时注释掉 `gppt-gicd` 和 `gppt-gits`,目的是避免 `VGicD` typed-control 路径和 `Gits` host ITS MMIO 初始化路径干扰 `VGicR` factory 迁移判断。 + +成功日志包括: + +```text +aarch64 device factory matched: type=GPPTRedistributor +GPPT Redistributor factory built native VGicR for vCPU 0 +aarch64 device factory built native device: id=DeviceId(0), name=gppt-gicr-0 +aarch64 device factory registered 1 native device(s) for type GPPTRedistributor +PASS smoke +``` + +因此可以认为:**设备创建、资源声明、factory/catalog 接入、registry 注册这条迁移路径已经跑通**。 + +但这次验证不等于所有设备问题都解决了。日志中仍然可能出现: + +```text +Failed to assign SPIs: No VGicD found in device list +``` + +这是 `VGicD` 仍被旧 typed-control 路径查找的结果,不属于 `VGicR` factory 迁移失败。它说明后续迁移 `VGicD` 时需要同时处理“设备控制面”问题,而不是只迁移普通 MMIO access。 + +## linker-section factory 自注册方案记录 + +在继续讨论“新增设备是否仍然要改 `device.rs`”时,确认了当前 `DeviceFactoryCatalog` 只解决了“构建设备逻辑不写在 `AxVmDevices::init` 里”的问题,还没有解决“factory 列表仍由核心代码维护”的问题。 + +当前 AArch64 路径中仍然存在类似下面的中心化注册点: + +```text +device.rs + -> 手写 factories: [&dyn DeviceFactory; N] + -> DeviceFactoryCatalog::new(&factories) + -> find(config.emu_type) +``` + +因此,新增一个 factory 仍然需要改 `device.rs` 或某个显式 platform catalog。这个状态比直接在 `match config.emu_type` 中构造设备更好,但还不是设备 crate 自注册。 + +项目里已经有一个可参考的 no_std linker-section 注册模式:`rdrive`/`ax-driver` 的 `.driver.register`。 + +现有模式大致是: + +```text +driver crate + -> #[link_section = ".driver.register"] static DriverRegister + +runtime.ld + -> __sdriver_register = . + -> KEEP(*(.driver.register*)) + -> __edriver_register = . + +axruntime/registers.rs + -> start/end symbol + -> &[DriverRegister] + -> rdrive::register_append(...) +``` + +这个机制的关键不是 `inventory` 或 `linkme` 本身,而是更基础的 linker-section 思路: + +```text +多个 crate 分散提交静态注册项 + -> 链接器把同名 section 合并 + -> 最终镜像通过 start/end 符号扫描注册项 + -> runtime/catalog 消费这些注册项 +``` + +如果把它复用到 axdevice factory,建议做成一套平行机制,例如 `.axdevice.factory`。 + +建议的职责边界: + +```text +axdevice_base: + 定义 DeviceFactoryRegister 或等价注册项类型。 + 提供 register_device_factory! 宏。 + 这样具体设备 crate 只依赖 axdevice_base,不依赖 axdevice,避免循环依赖。 + +具体设备 crate: + 定义自己的 factory。 + 通过 register_device_factory! 把 factory entry 放进 .axdevice.factory section。 + +axdevice: + 读取 __saxdevice_factory/__eaxdevice_factory。 + 将 linker section 中的注册项转换成 factory catalog。 + AxVmDevices::init 只消费 catalog,不再手写具体 factory 列表。 + +runtime/linker script: + 保留 .axdevice.factory* section。 + 导出 start/end symbol。 +``` + +一个可能的初始化路径: + +```text +arm_vgic::v3::vgicr + -> static GPPT_REDISTRIBUTOR_FACTORY + -> register_device_factory!("gppt-redistributor", GPPT_REDISTRIBUTOR_FACTORY) + +final ELF + -> .axdevice.factory + +axdevice + -> linker_device_factories() + -> DeviceFactoryCatalog + -> factory.build(ctx, config) + -> register_device() +``` + +需要特别注意依赖方向。注册宏如果放在 `axdevice`,`arm_vgic` 为了注册 factory 就要依赖 `axdevice`;但当前 `axdevice` 在 AArch64 下又依赖 `arm_vgic`,这会形成不合适的循环。因此注册项类型和宏更适合放在 `axdevice_base`。 + +no_std 下需要确认的正确性点: + +- `runtime.ld` 或最终链接脚本必须 `KEEP(*(.axdevice.factory*))`,否则 LTO/`--gc-sections` 可能裁掉注册项。 +- 需要导出 `__saxdevice_factory` 和 `__eaxdevice_factory`,并保证空 section 时 start/end 仍然可用。 +- section 应放在已加载、可读、地址映射正确的区域,通常跟 `.runtime` 或 `.rodata` 附近更合理。 +- section 起始地址要满足注册项类型的对齐要求,读取时要检查 section 字节长度是否是 `size_of::()` 的整数倍。 +- 注册项类型需要保持简单稳定,适合静态放入 section,例如名称、设备类型、factory 指针或构造函数指针。 +- 如果使用 `&'static dyn DeviceFactory`,它只应作为同一个 Rust 镜像内的布局约定使用,不应把它当跨语言 ABI。 +- 如果宏使用 `#[used(linker)]`,展开所在 crate 可能需要 `#![feature(used_with_arg)]`;如果为了降低接入成本,可以先使用稳定 `#[used]` 加 linker script `KEEP`。 +- 设备 crate 必须被最终镜像实际链接进来。只写一个注册 static 不代表 Cargo 一定会把 crate 拉进最终 ELF。 +- 不应依赖注册项顺序。若同一 `EmulatedDeviceType` 出现多个 factory,应定义清楚是报错、按 priority 选择,还是 first match。 +- 构建后需要用 `readelf -S/-s` 或启动日志验证 section、start/end symbol 和 factory 数量。 + +和 `inventory`/`linkme` 相比,复用 `.driver.register` 思路更贴合当前项目: + +- 不需要先引入新的分布式注册 crate。 +- 可以沿用现有 linker script/start-end symbol/slice 扫描经验。 +- 更容易在 Axvisor 的 no_std、多架构、定制链接脚本环境中审计。 + +这条路线能解决的是“新增 factory 不再改 `device.rs` 的 factory 数组”。它不能自动解决所有中心化修改: + +- 新增全新的 `EmulatedDeviceType` 仍然需要改 `axvm-types` 和配置解析。 +- 新设备 crate 仍然需要通过 feature/dependency 被最终镜像链接。 +- 有 typed-control path 的设备仍然需要后续 IRQ/control-plane 抽象配合迁移。 + +因此,这个方案适合作为显式 catalog 之后的下一阶段增强:先把 factory 构建路径稳定下来,再把 factory 列表维护从核心代码迁移到 linker-section 注册表。 + +## 当前结论 + +设备迁移可以分成两层: + +```text +设备创建和普通 bus access: + 当前方案已验证,可以继续按 VGicR 模板迁移。 + +设备控制面和中断注入: + 仍然散落在 VM/架构代码中,需要进入下一阶段设计。 +``` + +因此后续不需要继续把主要精力放在“证明 factory 是否可行”上。后续普通设备迁移可以按需推进,但方向二的主线应该转向 IRQ 路由和中断后端抽象。 + +## 后续计划:转向 IRQ 路由迁移 + +下一阶段建议把重心放在 VM 级中断模型,而不是继续机械迁移所有设备。 + +目标形态: + +```text +设备后端 + -> IrqSink / InterruptRouter + -> arch-specific interrupt backend + -> vLAPIC/vIOAPIC | VGIC | vPLIC/AIA | LoongArch virtual interrupt controller +``` + +核心原则: + +- 设备只表达中断语义,例如 `raise/lower/pulse/msi/eoi`。 +- 设备不直接调用 VGIC/vPLIC/vLAPIC/CSR/GCSR 等架构接口。 +- 设备不通过“写另一个设备 MMIO”间接注入中断。 +- VM 层提供统一 `InterruptRouter`,根据架构和 IRQ route 分发到实际后端。 +- route 表负责描述 `IrqLine -> IrqTarget`、SPI/PPI/SGI、MSI/MSI-X、vCPU target 等关系。 + +建议拆成几个小步: + +```text +1. 梳理现有中断注入路径 + AArch64: VGIC/GICH/ICH/LR 注入、VGicD::assign_irq、timer 中断 + RISC-V: vPLIC pending/claim/complete + x86_64: vLAPIC/vIOAPIC/MSI/pending event + LoongArch: CSR/GCSR 虚拟中断注入 + +2. 定义 VM 级 IRQ 概念层 + IrqLine + IrqTarget + IrqRoute + IrqEvent + IrqSink + InterruptRouter + InterruptControllerOps 或 ArchIrqBackend + +3. 先做最小 AArch64 backend + 从设备视角只需要 pulse/raise/lower 一个 SPI/PPI。 + router 内部再调用现有 VGIC 注入逻辑。 + 不急着一次性重写 VGICD/GICR/GITS。 + +4. 选择一个真实调用点迁移 + 优先选择 timer tick、简单虚拟设备 IRQ、或当前最清晰的一条 SPI 注入路径。 + 避免一开始就迁移完整 ITS/MSI。 + +5. 再回头处理 typed-control path + VGicD::assign_irq 这类路径应从“遍历 MMIO 设备 downcast”迁到明确的控制接口或 interrupt backend service。 +``` + +已有 `components/irq-framework` 更偏 host IRQ action/registry,可以借鉴它的 request/enable/dispatch 思想,但 VM 级 IRQ router 关注的是 guest interrupt injection,不应直接把两者混成一个对象。 + +短期里可以把后续工作拆成两条并行线: + +- **设备线**:继续按 `VGicR` 模板迁移简单设备;遇到有控制面依赖的设备先记录 blocker。 +- **IRQ 线**:优先设计和落地 VM 级 `InterruptRouter/IrqSink`,把架构特判从设备后端中抽出来。 + +我当前更倾向先推进 IRQ 线,因为设备创建路径已经被 `VGicR` 验证过,真正影响方向二收益的是“中断注入语义是否能统一”。 diff --git a/test-suit/axvisor/aarch64-device-migration/qemu/build-aarch64-unknown-none-softfloat.toml b/test-suit/axvisor/aarch64-device-migration/qemu/build-aarch64-unknown-none-softfloat.toml new file mode 100644 index 0000000000..24a2fdb10d --- /dev/null +++ b/test-suit/axvisor/aarch64-device-migration/qemu/build-aarch64-unknown-none-softfloat.toml @@ -0,0 +1,8 @@ +env = { AX_IP = "10.0.2.15", AX_GW = "10.0.2.2" } +features = [ + "ax-driver/virtio-blk", + "fs", +] +log = "Info" +target = "aarch64-unknown-none-softfloat" +vm_configs = ["os/axvisor/configs/vms/qemu/aarch64/linux-smp1-gppt-gic.toml"] diff --git a/test-suit/axvisor/aarch64-device-migration/qemu/smoke/qemu-aarch64.toml b/test-suit/axvisor/aarch64-device-migration/qemu/smoke/qemu-aarch64.toml new file mode 100644 index 0000000000..fcd6b3f48b --- /dev/null +++ b/test-suit/axvisor/aarch64-device-migration/qemu/smoke/qemu-aarch64.toml @@ -0,0 +1,28 @@ +args = [ + "-nographic", + "-cpu", + "cortex-a72", + "-machine", + "virt,virtualization=on,gic-version=3", + "-smp", + "4", + "-device", + "virtio-blk-device,drive=disk0", + "-drive", + "id=disk0,if=none,format=raw,file=${workspace}/tmp/axbuild/rootfs/rootfs-aarch64-alpine.img", + "-append", + "root=/dev/vda rw init=/bin/sh", + "-m", + "8g", +] +fail_regex = [ + "(?i)\\bpanic(?:ked)?\\b", + "(?i)kernel panic", + "(?i)login incorrect", + "(?i)permission denied", +] +success_regex = ["(?m)^guest test pass!\\s*$"] +shell_prefix = "~ #" +shell_init_cmd = "pwd && echo 'guest test pass!'" +to_bin = true +uefi = false diff --git a/virtualization/arm_vgic/src/v3/vgicr.rs b/virtualization/arm_vgic/src/v3/vgicr.rs index bef2bef13a..d5bfaa3d1f 100644 --- a/virtualization/arm_vgic/src/v3/vgicr.rs +++ b/virtualization/arm_vgic/src/v3/vgicr.rs @@ -12,13 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. +use alloc::sync::Arc; use core::ptr; use ax_kspin::SpinNoIrq; use ax_memory_addr::PhysAddr; -use axdevice_base::{AccessWidth, BaseDeviceOps, DeviceResult}; -use axvm_types::{GuestPhysAddr, GuestPhysAddrRange, HostPhysAddr}; -use log::{debug, trace}; +use axdevice_base::{ + AccessWidth, BaseDeviceOps, DeviceBundle, DeviceFactory, DeviceFactoryContext, + DeviceFactoryError, DeviceFactoryResult, DeviceRegistration, DeviceResult, MmioDeviceAdapter, +}; +use axvm_types::{ + EmulatedDeviceConfig, EmulatedDeviceType, GuestPhysAddr, GuestPhysAddrRange, HostPhysAddr, +}; +use log::{debug, info, trace}; use spin::Once; use super::{ @@ -222,6 +228,81 @@ impl BaseDeviceOps for VGicR { } } +/// Builds partial-passthrough redistributors declared by a VM configuration. +pub struct GpptRedistributorFactory; + +static GPPT_REDISTRIBUTOR_FACTORY: GpptRedistributorFactory = GpptRedistributorFactory; + +axdevice_base::register_device_factory!("gppt-redistributor", GPPT_REDISTRIBUTOR_FACTORY); + +impl DeviceFactory for GpptRedistributorFactory { + fn device_type(&self) -> EmulatedDeviceType { + EmulatedDeviceType::GPPTRedistributor + } + + fn build( + &self, + config: &EmulatedDeviceConfig, + _context: &dyn DeviceFactoryContext, + ) -> DeviceFactoryResult { + const EXPECTED_ARGS: &str = + "three arguments (cpu count, redistributor stride, physical CPU base)"; + + let cpu_count = config_argument(config, 0, EXPECTED_ARGS)?; + let stride = config_argument(config, 1, EXPECTED_ARGS)?; + let physical_cpu_base = config_argument(config, 2, EXPECTED_ARGS)?; + let mut bundle = DeviceBundle::new(); + + for vcpu_id in 0..cpu_count { + let offset = vcpu_id + .checked_mul(stride) + .ok_or_else(|| invalid_factory_config(config, "redistributor offset overflows"))?; + let base_gpa = config + .base_gpa + .checked_add(offset) + .ok_or_else(|| invalid_factory_config(config, "redistributor address overflows"))?; + let physical_cpu_id = physical_cpu_base + .checked_add(vcpu_id) + .ok_or_else(|| invalid_factory_config(config, "physical CPU ID overflows"))?; + let redistributor = Arc::new(VGicR::new( + base_gpa.into(), + Some(config.length), + physical_cpu_id, + )); + bundle.push(DeviceRegistration::Device(MmioDeviceAdapter::from_arc( + redistributor, + ))); + + info!( + "GPPT redistributor factory built vCPU {vcpu_id} device at {base_gpa:#x} with \ + length {:#x}", + config.length + ); + } + + Ok(bundle) + } +} + +fn config_argument( + config: &EmulatedDeviceConfig, + index: usize, + expected: &'static str, +) -> DeviceFactoryResult { + config + .cfg_list + .get(index) + .copied() + .ok_or_else(|| invalid_factory_config(config, expected)) +} + +fn invalid_factory_config(config: &EmulatedDeviceConfig, detail: &str) -> DeviceFactoryError { + DeviceFactoryError::InvalidConfig { + operation: "build GPPT redistributor", + detail: alloc::format!("device '{}': {detail}", config.name), + } +} + // todo: move the lpi prop table to arm-gic-driver, and find a good interface to use it. /// LPI property table for managing Locality-specific Peripheral Interrupts. pub struct LpiPropTable { diff --git a/virtualization/axdevice/src/device.rs b/virtualization/axdevice/src/device.rs index 12e9176aa9..d3bb122eb9 100644 --- a/virtualization/axdevice/src/device.rs +++ b/virtualization/axdevice/src/device.rs @@ -169,7 +169,6 @@ impl AxVmDevices { EmulatedDeviceType::InterruptController | EmulatedDeviceType::Console | EmulatedDeviceType::IVCChannel - | EmulatedDeviceType::GPPTRedistributor | EmulatedDeviceType::GPPTDistributor | EmulatedDeviceType::GPPTITS | EmulatedDeviceType::FwCfg @@ -510,11 +509,12 @@ impl AxVmDevices { /// already-registered devices in this bundle are rolled back via /// `pop()` + index-key removal. pub fn register_bundle(&mut self, bundle: DeviceBundle) -> DeviceManagerResult { - for (index, pollable) in bundle.pollable.iter().enumerate() { + let bundle = bundle.into_parts(); + for (index, pollable) in bundle.pollable_devices.iter().enumerate() { if self .pollable_devices .iter() - .chain(bundle.pollable[..index].iter()) + .chain(bundle.pollable_devices[..index].iter()) .any(|existing| Arc::ptr_eq(existing, pollable)) { return Err(DeviceManagerError::ResourceConflict { @@ -531,7 +531,7 @@ impl AxVmDevices { return Err(error.into()); } } - self.pollable_devices.extend(bundle.pollable); + self.pollable_devices.extend(bundle.pollable_devices); Ok(()) } diff --git a/virtualization/axdevice/src/error.rs b/virtualization/axdevice/src/error.rs index 9ff54b0cb0..d78a438562 100644 --- a/virtualization/axdevice/src/error.rs +++ b/virtualization/axdevice/src/error.rs @@ -2,7 +2,9 @@ use alloc::string::String; -use axdevice_base::{AccessWidth, BusKind, DeviceError, IrqError, RegistryError}; +use axdevice_base::{ + AccessWidth, BusKind, DeviceError, DeviceFactoryError, IrqError, RegistryError, +}; /// Result type returned by device manager operations. pub type DeviceManagerResult = Result; @@ -90,6 +92,18 @@ pub enum DeviceManagerError { Irq(#[from] IrqError), } +impl From for DeviceManagerError { + fn from(error: DeviceFactoryError) -> Self { + match error { + DeviceFactoryError::InvalidConfig { operation, detail } => { + Self::InvalidConfig { operation, detail } + } + DeviceFactoryError::Device(error) => Self::Device(error), + DeviceFactoryError::Irq(error) => Self::Irq(error), + } + } +} + impl From for DeviceError { fn from(error: DeviceManagerError) -> Self { match error { diff --git a/virtualization/axdevice/src/factory.rs b/virtualization/axdevice/src/factory.rs index 42a7bc2b31..e86f9e73b2 100644 --- a/virtualization/axdevice/src/factory.rs +++ b/virtualization/axdevice/src/factory.rs @@ -12,23 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Extensible construction of emulated devices from VM configuration. +//! Factory discovery and VM-owned construction context. use alloc::{sync::Arc, vec::Vec}; -use axdevice_base::{InterruptTriggerMode, IrqLine}; +use axdevice_base::{DeviceBundle, InterruptTriggerMode, IrqLine, IrqResult}; +pub use axdevice_base::{ + DeviceFactory, DeviceFactoryContext, DeviceFactoryError, DeviceFactoryRegister, + DeviceFactoryResult, +}; use axvm_types::{EmulatedDeviceConfig, EmulatedDeviceType}; -use crate::{DeviceBundle, DeviceManagerError, DeviceManagerResult}; +use crate::{DeviceManagerError, DeviceManagerResult}; /// Resolves a VM-local interrupt line for a device under construction. pub trait IrqResolver: Send + Sync { /// Resolves `line` with the requested trigger mode. - fn resolve_irq( - &self, - line: usize, - trigger: InterruptTriggerMode, - ) -> DeviceManagerResult; + fn resolve_irq(&self, line: usize, trigger: InterruptTriggerMode) -> IrqResult; } /// VM-owned services available while a device factory is building a device. @@ -43,43 +43,56 @@ impl<'a> DeviceBuildContext<'a> { } /// Resolves a VM-local interrupt line. - pub fn resolve_irq( + pub fn resolve_irq(&self, line: usize, trigger: InterruptTriggerMode) -> IrqResult { + self.irq_resolver.resolve_irq(line, trigger) + } +} + +impl DeviceFactoryContext for DeviceBuildContext<'_> { + fn resolve_irq( &self, line: usize, trigger: InterruptTriggerMode, - ) -> DeviceManagerResult { - self.irq_resolver.resolve_irq(line, trigger) + ) -> DeviceFactoryResult { + self.irq_resolver + .resolve_irq(line, trigger) + .map_err(DeviceFactoryError::from) } } -/// Builds all capabilities contributed by one emulated device type. -pub trait DeviceFactory: Send + Sync { - /// Returns the configuration type handled by this factory. - fn device_type(&self) -> EmulatedDeviceType; +enum RegisteredFactory { + Static(&'static dyn DeviceFactory), + Owned(Arc), +} - /// Builds a device without modifying the destination device registry. - fn build( - &self, - config: &EmulatedDeviceConfig, - context: &DeviceBuildContext<'_>, - ) -> DeviceManagerResult; +impl RegisteredFactory { + fn factory(&self) -> &dyn DeviceFactory { + match self { + Self::Static(factory) => *factory, + Self::Owned(factory) => factory.as_ref(), + } + } } -/// A registry containing at most one factory for each emulated device type. +/// Runtime catalog of factories discovered from the final image. +/// +/// The default VM initialization path populates this catalog from linker +/// registrations. [`Self::register`] remains available for tests and temporary +/// architecture adapters while their factories migrate to static registration. #[derive(Default)] pub struct DeviceFactoryRegistry { - factories: Vec<(EmulatedDeviceType, Arc)>, + factories: Vec<(EmulatedDeviceType, RegisteredFactory)>, } impl DeviceFactoryRegistry { - /// Creates an empty factory registry. + /// Creates an empty factory catalog. pub const fn new() -> Self { Self { factories: Vec::new(), } } - /// Registers a factory, rejecting a duplicate device type. + /// Registers an explicitly provided factory, rejecting a duplicate type. pub fn register(&mut self, factory: Arc) -> DeviceManagerResult { let device_type = factory.device_type(); if self.get(device_type).is_some() { @@ -90,7 +103,51 @@ impl DeviceFactoryRegistry { ), }); } - self.factories.push((device_type, factory)); + self.factories + .push((device_type, RegisteredFactory::Owned(factory))); + Ok(()) + } + + /// Adds statically linked factory registrations to this catalog. + /// + /// The full set is validated before insertion, so duplicate types leave the + /// catalog unchanged. + pub fn register_static_factories( + &mut self, + registers: &[DeviceFactoryRegister], + ) -> DeviceManagerResult { + for (index, register) in registers.iter().enumerate() { + let device_type = register.factory().device_type(); + if self.get(device_type).is_some() { + return Err(DeviceManagerError::ResourceConflict { + operation: "register static device factory", + detail: alloc::format!( + "factory '{}' duplicates an existing factory for device type {device_type}", + register.name() + ), + }); + } + if let Some(existing) = registers[..index] + .iter() + .find(|existing| existing.factory().device_type() == device_type) + { + return Err(DeviceManagerError::ResourceConflict { + operation: "register static device factory", + detail: alloc::format!( + "factories '{}' and '{}' both handle device type {device_type}", + existing.name(), + register.name() + ), + }); + } + } + + for register in registers { + self.factories.push(( + register.factory().device_type(), + RegisteredFactory::Static(register.factory()), + )); + } Ok(()) } @@ -99,10 +156,10 @@ impl DeviceFactoryRegistry { self.factories .iter() .find(|(registered_type, _)| *registered_type == device_type) - .map(|(_, factory)| factory.as_ref()) + .map(|(_, factory)| factory.factory()) } - /// Builds a bundle for `config`. + /// Builds a device bundle for one VM configuration entry. pub fn build( &self, config: &EmulatedDeviceConfig, @@ -118,12 +175,74 @@ impl DeviceFactoryRegistry { ), }); }; - factory.build(config, context) + factory.build(config, context).map_err(Into::into) + } +} + +/// Returns factory registrations collected from the final linker image. +#[cfg(any(target_os = "none", target_env = "musl"))] +pub fn linker_device_factory_registers() -> DeviceManagerResult<&'static [DeviceFactoryRegister]> { + unsafe extern "C" { + fn __saxdevice_factory(); + fn __eaxdevice_factory(); + } + + let start = __saxdevice_factory as *const () as usize; + let end = __eaxdevice_factory as *const () as usize; + if start > end { + return Err(DeviceManagerError::InvalidConfig { + operation: "read device factory linker section", + detail: alloc::format!("section start {start:#x} is after section end {end:#x}"), + }); + } + + let byte_len = end - start; + if byte_len == 0 { + return Ok(&[]); + } + + let alignment = core::mem::align_of::(); + if !start.is_multiple_of(alignment) { + return Err(DeviceManagerError::InvalidConfig { + operation: "read device factory linker section", + detail: alloc::format!("section start {start:#x} is not aligned to {alignment} bytes"), + }); + } + + let entry_size = core::mem::size_of::(); + if !byte_len.is_multiple_of(entry_size) { + return Err(DeviceManagerError::InvalidConfig { + operation: "read device factory linker section", + detail: alloc::format!( + "section length {byte_len:#x} is not a multiple of entry size {entry_size}" + ), + }); } + + let entry_count = byte_len / entry_size; + // SAFETY: `runtime.ld` places only `DeviceFactoryRegister` values between + // these symbols, keeps the input sections, and aligns their start. The + // checks above validate the remaining range and layout preconditions. + Ok(unsafe { core::slice::from_raw_parts(start as *const DeviceFactoryRegister, entry_count) }) +} + +/// Returns no linker registrations on targets without the runtime linker script. +#[cfg(not(any(target_os = "none", target_env = "musl")))] +pub fn linker_device_factory_registers() -> DeviceManagerResult<&'static [DeviceFactoryRegister]> { + Ok(&[]) +} + +/// Registers every factory collected from the final linker image. +pub fn register_linker_factories(registry: &mut DeviceFactoryRegistry) -> DeviceManagerResult { + registry.register_static_factories(linker_device_factory_registers()?) } struct MetaDeviceFactory; +static META_DEVICE_FACTORY: MetaDeviceFactory = MetaDeviceFactory; + +axdevice_base::register_device_factory!("meta", META_DEVICE_FACTORY); + impl DeviceFactory for MetaDeviceFactory { fn device_type(&self) -> EmulatedDeviceType { EmulatedDeviceType::Dummy @@ -132,13 +251,22 @@ impl DeviceFactory for MetaDeviceFactory { fn build( &self, _config: &EmulatedDeviceConfig, - _context: &DeviceBuildContext<'_>, - ) -> DeviceManagerResult { + _context: &dyn DeviceFactoryContext, + ) -> DeviceFactoryResult { Ok(DeviceBundle::new()) } } -/// Registers device factories that do not depend on an architecture backend. +/// Registers built-in factories on targets without the runtime linker script. +#[cfg(not(any(target_os = "none", target_env = "musl")))] pub fn register_builtin_factories(registry: &mut DeviceFactoryRegistry) -> DeviceManagerResult { - registry.register(Arc::new(MetaDeviceFactory)) + static META_REGISTER: DeviceFactoryRegister = + DeviceFactoryRegister::new("meta", &META_DEVICE_FACTORY); + registry.register_static_factories(core::slice::from_ref(&META_REGISTER)) +} + +/// ArceOS image built-ins are discovered from `.axdevice.factory`. +#[cfg(any(target_os = "none", target_env = "musl"))] +pub fn register_builtin_factories(_registry: &mut DeviceFactoryRegistry) -> DeviceManagerResult { + Ok(()) } diff --git a/virtualization/axdevice/src/legacy.rs b/virtualization/axdevice/src/legacy.rs new file mode 100644 index 0000000000..b72aea8b70 --- /dev/null +++ b/virtualization/axdevice/src/legacy.rs @@ -0,0 +1,188 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Adapters from the existing `BaseDeviceOps` traits into the new device model. + +use alloc::{string::String, sync::Arc, vec::Vec}; + +use axdevice_base::{ + BaseMmioDeviceOps, BasePortDeviceOps, BaseSysRegDeviceOps, BusAccess, BusAddress, BusKind, + BusOp, BusResponse, DeviceCapabilities, DeviceError, DeviceId, DeviceOps, DeviceResult, + Resource, +}; + +/// The old single-bus device object carried by a legacy adapter. +pub enum LegacyDeviceInner { + /// Existing MMIO device implementation. + Mmio(Arc), + /// Existing port I/O device implementation. + Pio(Arc), + /// Existing system register device implementation. + SysReg(Arc), +} + +impl LegacyDeviceInner { + fn kind(&self) -> BusKind { + match self { + Self::Mmio(_) => BusKind::Mmio, + Self::Pio(_) => BusKind::Pio, + Self::SysReg(_) => BusKind::SysReg, + } + } +} + +/// Adapter that exposes an existing `BaseDeviceOps` object as [`DeviceOps`]. +pub struct LegacyDeviceAdapter { + id: DeviceId, + name: String, + resources: Vec, + capabilities: DeviceCapabilities, + inner: LegacyDeviceInner, +} + +impl LegacyDeviceAdapter { + /// Creates an adapter from raw parts. + pub fn new( + id: DeviceId, + name: String, + resources: Vec, + capabilities: DeviceCapabilities, + inner: LegacyDeviceInner, + ) -> Self { + Self { + id, + name, + resources, + capabilities, + inner, + } + } + + /// Creates an MMIO legacy adapter. + pub fn mmio( + id: DeviceId, + name: String, + resources: Vec, + capabilities: DeviceCapabilities, + device: Arc, + ) -> Self { + Self::new( + id, + name, + resources, + capabilities, + LegacyDeviceInner::Mmio(device), + ) + } + + /// Creates a port I/O legacy adapter. + pub fn pio( + id: DeviceId, + name: String, + resources: Vec, + capabilities: DeviceCapabilities, + device: Arc, + ) -> Self { + Self::new( + id, + name, + resources, + capabilities, + LegacyDeviceInner::Pio(device), + ) + } + + /// Creates a system register legacy adapter. + pub fn sysreg( + id: DeviceId, + name: String, + resources: Vec, + capabilities: DeviceCapabilities, + device: Arc, + ) -> Self { + Self::new( + id, + name, + resources, + capabilities, + LegacyDeviceInner::SysReg(device), + ) + } +} + +impl DeviceOps for LegacyDeviceAdapter { + fn id(&self) -> DeviceId { + self.id + } + + fn name(&self) -> &str { + &self.name + } + + fn resources(&self) -> &[Resource] { + &self.resources + } + + fn capabilities(&self) -> DeviceCapabilities { + self.capabilities + } + + fn access(&self, access: BusAccess) -> DeviceResult { + if access.kind != access.addr.kind() || access.kind != self.inner.kind() { + return Err(DeviceError::BusAddressMismatch { + kind: self.inner.kind(), + address: access.addr, + }); + } + + match (&self.inner, access.addr, access.op) { + (LegacyDeviceInner::Mmio(device), BusAddress::Mmio(addr), BusOp::Read) => device + .handle_read(addr, access.width) + .map(|value| BusResponse::Read { value }) + .map_err(DeviceError::from), + (LegacyDeviceInner::Mmio(device), BusAddress::Mmio(addr), BusOp::Write { value }) => { + device + .handle_write(addr, access.width, value) + .map(|()| BusResponse::Write) + .map_err(DeviceError::from) + } + (LegacyDeviceInner::Pio(device), BusAddress::Pio(port), BusOp::Read) => device + .handle_read(port, access.width) + .map(|value| BusResponse::Read { value }) + .map_err(DeviceError::from), + (LegacyDeviceInner::Pio(device), BusAddress::Pio(port), BusOp::Write { value }) => { + device + .handle_write(port, access.width, value) + .map(|()| BusResponse::Write) + .map_err(DeviceError::from) + } + (LegacyDeviceInner::SysReg(device), BusAddress::SysReg(addr), BusOp::Read) => device + .handle_read(addr, access.width) + .map(|value| BusResponse::Read { value }) + .map_err(DeviceError::from), + ( + LegacyDeviceInner::SysReg(device), + BusAddress::SysReg(addr), + BusOp::Write { value }, + ) => device + .handle_write(addr, access.width, value) + .map(|()| BusResponse::Write) + .map_err(DeviceError::from), + _ => Err(DeviceError::BusAddressMismatch { + kind: self.inner.kind(), + address: access.addr, + }), + } + } +} diff --git a/virtualization/axdevice/src/lib.rs b/virtualization/axdevice/src/lib.rs index be23fcf454..649c786af7 100644 --- a/virtualization/axdevice/src/lib.rs +++ b/virtualization/axdevice/src/lib.rs @@ -50,8 +50,9 @@ pub use config::AxVmDeviceConfig; pub use device::AxVmDevices; pub use error::{DeviceManagerError, DeviceManagerResult}; pub use factory::{ - DeviceBuildContext, DeviceFactory, DeviceFactoryRegistry, IrqResolver, - register_builtin_factories, + DeviceBuildContext, DeviceFactory, DeviceFactoryContext, DeviceFactoryError, + DeviceFactoryRegister, DeviceFactoryRegistry, DeviceFactoryResult, IrqResolver, + linker_device_factory_registers, register_builtin_factories, register_linker_factories, }; pub use fw_cfg::{ FwCfg, FwCfgInterruptConfig, FwCfgPciConfig, FwCfgPlatformConfig, FwCfgRamRegion, diff --git a/virtualization/axdevice/src/registration.rs b/virtualization/axdevice/src/registration.rs index ef1ea0e65c..f4fc8ed82d 100644 --- a/virtualization/axdevice/src/registration.rs +++ b/virtualization/axdevice/src/registration.rs @@ -12,77 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! Transactional device registration types. +//! Compatibility exports for device factory registration capabilities. -use alloc::{sync::Arc, vec::Vec}; - -use axdevice_base::Device; - -use crate::DeviceManagerResult; - -/// A device capability that can be polled by the VM runtime. -pub trait PollableDeviceOps: Send + Sync { - /// Advances the device using the current monotonic time in nanoseconds. - fn poll(&self, now_ns: u64) -> DeviceManagerResult; -} - -/// One strongly typed capability contributed by a device. -#[non_exhaustive] -pub enum DeviceRegistration { - /// A device implementing the unified [`Device`] trait. - Device(Arc), - /// A capability that requires periodic polling. - Pollable(Arc), -} - -/// A set of device capabilities that must be registered atomically. -/// -/// The contained registration lists are private so callers cannot bypass -/// [`DeviceRegistration`] when adding future capability kinds. -#[derive(Default)] -pub struct DeviceBundle { - pub(crate) devices: Vec>, - pub(crate) pollable: Vec>, -} - -impl DeviceBundle { - /// Creates an empty bundle. - pub const fn new() -> Self { - Self { - devices: Vec::new(), - pollable: Vec::new(), - } - } - - /// Creates a bundle containing one registration. - pub fn from_registration(registration: DeviceRegistration) -> Self { - let mut bundle = Self::new(); - bundle.push(registration); - bundle - } - - /// Adds one capability to this bundle. - pub fn push(&mut self, registration: DeviceRegistration) { - match registration { - DeviceRegistration::Device(device) => self.devices.push(device), - DeviceRegistration::Pollable(device) => self.pollable.push(device), - } - } - - /// Adds one capability and returns the bundle for builder-style use. - pub fn with_registration(mut self, registration: DeviceRegistration) -> Self { - self.push(registration); - self - } - - /// Returns whether this bundle contains no capabilities. - pub fn is_empty(&self) -> bool { - self.devices.is_empty() && self.pollable.is_empty() - } -} - -impl From for DeviceBundle { - fn from(registration: DeviceRegistration) -> Self { - Self::from_registration(registration) - } -} +pub use axdevice_base::{DeviceBundle, DeviceRegistration, PollableDeviceOps}; diff --git a/virtualization/axdevice/src/registry.rs b/virtualization/axdevice/src/registry.rs new file mode 100644 index 0000000000..517c599b7e --- /dev/null +++ b/virtualization/axdevice/src/registry.rs @@ -0,0 +1,208 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Device registry and first-stage bus routing tables. + +use alloc::{rc::Rc, vec::Vec}; + +use axdevice_base::{ + BusAccess, BusAddress, BusResponse, DeviceAddrRange, DeviceError, DeviceId, DeviceOps, + DeviceResult, PortRange, Resource, SysRegAddrRange, +}; +use axvm_types::GuestPhysAddrRange; + +/// A route entry for MMIO accesses. +type MmioRoute = (GuestPhysAddrRange, DeviceId); +/// A route entry for port I/O accesses. +type PioRoute = (PortRange, DeviceId); +/// A route entry for system register accesses. +type SysRegRoute = (SysRegAddrRange, DeviceId); + +/// Registry for emulated devices and their bus-visible resources. +#[derive(Default)] +pub struct DeviceRegistry { + devices: Vec>, + mmio_routes: Vec, + pio_routes: Vec, + sysreg_routes: Vec, +} + +impl DeviceRegistry { + /// Creates an empty registry. + pub const fn new() -> Self { + Self { + devices: Vec::new(), + mmio_routes: Vec::new(), + pio_routes: Vec::new(), + sysreg_routes: Vec::new(), + } + } + + /// Registers a device and indexes its bus resources. + pub fn register_device(&mut self, device: Rc) -> DeviceResult { + let id = device.id(); + if self.devices.iter().any(|existing| existing.id() == id) { + return Err(DeviceError::DuplicateDeviceId { id }); + } + + self.check_resource_conflicts(device.resources())?; + + for resource in device.resources() { + match *resource { + Resource::Mmio(range) => self.mmio_routes.push((range, id)), + Resource::Pio(range) => self.pio_routes.push((range, id)), + Resource::SysReg(range) => self.sysreg_routes.push((range, id)), + Resource::Irq(_) + | Resource::Msi { .. } + | Resource::Dma + | Resource::PciBar { .. } => {} + } + } + + self.devices.push(device); + Ok(id) + } + + /// Finds a device by identifier. + pub fn find_device(&self, id: DeviceId) -> Option> { + self.devices + .iter() + .find(|device| device.id() == id) + .cloned() + } + + /// Dispatches a normalized bus access to the registered device that owns the route. + pub fn dispatch(&self, access: BusAccess) -> DeviceResult { + if access.kind != access.addr.kind() { + return Err(DeviceError::BusAddressMismatch { + kind: access.kind, + address: access.addr, + }); + } + + let device_id = match access.addr { + BusAddress::Mmio(addr) => self + .mmio_routes + .iter() + .find(|(range, _)| range.contains(addr)) + .map(|(_, id)| *id), + BusAddress::Pio(port) => self + .pio_routes + .iter() + .find(|(range, _)| range.contains(port)) + .map(|(_, id)| *id), + BusAddress::SysReg(addr) => self + .sysreg_routes + .iter() + .find(|(range, _)| range.contains(addr)) + .map(|(_, id)| *id), + } + .ok_or(DeviceError::DeviceNotFound { + kind: access.kind, + address: access.addr, + })?; + + let device = self + .find_device(device_id) + .ok_or(DeviceError::DeviceNotFound { + kind: access.kind, + address: access.addr, + })?; + + device.access(access) + } + + /// Returns the number of registered devices. + pub fn device_count(&self) -> usize { + self.devices.len() + } + + /// Returns the number of MMIO routes. + pub fn mmio_route_count(&self) -> usize { + self.mmio_routes.len() + } + + /// Returns the number of port I/O routes. + pub fn pio_route_count(&self) -> usize { + self.pio_routes.len() + } + + /// Returns the number of system register routes. + pub fn sysreg_route_count(&self) -> usize { + self.sysreg_routes.len() + } + + fn check_resource_conflicts(&self, resources: &[Resource]) -> DeviceResult { + for resource in resources { + match *resource { + Resource::Mmio(requested) => { + if let Some((existing, _)) = self + .mmio_routes + .iter() + .find(|(existing, _)| mmio_ranges_overlap(*existing, requested)) + { + return Err(DeviceError::ResourceConflict { + existing: Resource::Mmio(*existing), + requested: *resource, + }); + } + } + Resource::Pio(requested) => { + if let Some((existing, _)) = self + .pio_routes + .iter() + .find(|(existing, _)| port_ranges_overlap(*existing, requested)) + { + return Err(DeviceError::ResourceConflict { + existing: Resource::Pio(*existing), + requested: *resource, + }); + } + } + Resource::SysReg(requested) => { + if let Some((existing, _)) = self + .sysreg_routes + .iter() + .find(|(existing, _)| sysreg_ranges_overlap(*existing, requested)) + { + return Err(DeviceError::ResourceConflict { + existing: Resource::SysReg(*existing), + requested: *resource, + }); + } + } + Resource::Irq(_) + | Resource::Msi { .. } + | Resource::Dma + | Resource::PciBar { .. } => {} + } + } + Ok(()) + } +} + +#[inline] +fn mmio_ranges_overlap(left: GuestPhysAddrRange, right: GuestPhysAddrRange) -> bool { + left.start < right.end && right.start < left.end +} + +#[inline] +fn port_ranges_overlap(left: PortRange, right: PortRange) -> bool { + left.start <= right.end && right.start <= left.end +} + +#[inline] +fn sysreg_ranges_overlap(left: SysRegAddrRange, right: SysRegAddrRange) -> bool { + left.start <= right.end && right.start <= left.end +} diff --git a/virtualization/axdevice_base/src/bus.rs b/virtualization/axdevice_base/src/bus.rs new file mode 100644 index 0000000000..4c6b65836f --- /dev/null +++ b/virtualization/axdevice_base/src/bus.rs @@ -0,0 +1,151 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Unified bus transaction types for emulated device access. + +use axvm_types::GuestPhysAddr; + +use crate::{AccessWidth, Port, SysRegAddr}; + +/// The kind of guest-visible bus or register namespace used for a device access. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BusKind { + /// Memory-mapped I/O in the guest physical address space. + Mmio, + /// Port I/O, primarily used by x86 `in`/`out` instructions. + Pio, + /// Architecture system register namespace such as MSR, CSR, or AArch64 sysreg. + SysReg, +} + +/// A bus address tagged with the namespace it belongs to. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum BusAddress { + /// A guest physical MMIO address. + Mmio(GuestPhysAddr), + /// A port I/O address. + Pio(Port), + /// A system register address. + SysReg(SysRegAddr), +} + +impl BusAddress { + /// Returns the bus kind implied by this address. + pub const fn kind(self) -> BusKind { + match self { + Self::Mmio(_) => BusKind::Mmio, + Self::Pio(_) => BusKind::Pio, + Self::SysReg(_) => BusKind::SysReg, + } + } +} + +/// The operation requested by a bus access. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BusOp { + /// Read from the addressed device register. + Read, + /// Write a value to the addressed device register. + Write { + /// The value to write. Only the low bits selected by [`BusAccess::width`] are significant. + value: usize, + }, +} + +/// A normalized device access generated from a VM exit. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BusAccess { + /// The bus namespace used by the access. + pub kind: BusKind, + /// The address in the selected bus namespace. + pub addr: BusAddress, + /// The access width. + pub width: AccessWidth, + /// The requested operation. + pub op: BusOp, +} + +impl BusAccess { + /// Creates a new bus access. + pub const fn new(kind: BusKind, addr: BusAddress, width: AccessWidth, op: BusOp) -> Self { + Self { + kind, + addr, + width, + op, + } + } + + /// Creates an MMIO read access. + pub const fn mmio_read(addr: GuestPhysAddr, width: AccessWidth) -> Self { + Self::new(BusKind::Mmio, BusAddress::Mmio(addr), width, BusOp::Read) + } + + /// Creates an MMIO write access. + pub const fn mmio_write(addr: GuestPhysAddr, width: AccessWidth, value: usize) -> Self { + Self::new( + BusKind::Mmio, + BusAddress::Mmio(addr), + width, + BusOp::Write { value }, + ) + } + + /// Creates a port I/O read access. + pub const fn pio_read(port: Port, width: AccessWidth) -> Self { + Self::new(BusKind::Pio, BusAddress::Pio(port), width, BusOp::Read) + } + + /// Creates a port I/O write access. + pub const fn pio_write(port: Port, width: AccessWidth, value: usize) -> Self { + Self::new( + BusKind::Pio, + BusAddress::Pio(port), + width, + BusOp::Write { value }, + ) + } + + /// Creates a system register read access. + pub const fn sysreg_read(addr: SysRegAddr, width: AccessWidth) -> Self { + Self::new( + BusKind::SysReg, + BusAddress::SysReg(addr), + width, + BusOp::Read, + ) + } + + /// Creates a system register write access. + pub const fn sysreg_write(addr: SysRegAddr, width: AccessWidth, value: usize) -> Self { + Self::new( + BusKind::SysReg, + BusAddress::SysReg(addr), + width, + BusOp::Write { value }, + ) + } +} + +/// The result returned by a device for a bus access. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BusResponse { + /// A read completed and returned a value. + Read { + /// The read value, zero-extended by the device implementation when needed. + value: usize, + }, + /// A write completed. + Write, +} diff --git a/virtualization/axdevice_base/src/factory_contract.rs b/virtualization/axdevice_base/src/factory_contract.rs new file mode 100644 index 0000000000..f3bf6a4284 --- /dev/null +++ b/virtualization/axdevice_base/src/factory_contract.rs @@ -0,0 +1,190 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Device factory capabilities shared by device implementations and VM containers. + +use alloc::{string::String, sync::Arc, vec::Vec}; + +use axvm_types::{EmulatedDeviceConfig, EmulatedDeviceType, InterruptTriggerMode}; + +use crate::{Device, DeviceError, DeviceResult, IrqError, IrqLine}; + +/// Errors reported while a factory validates configuration or builds devices. +#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)] +pub enum DeviceFactoryError { + /// The VM device configuration is malformed or inconsistent. + #[error("invalid device factory configuration for {operation}: {detail}")] + InvalidConfig { + /// The factory operation that rejected the configuration. + operation: &'static str, + /// Diagnostic detail describing the invalid configuration. + detail: String, + }, + /// A constructed device or device adapter reported an error. + #[error(transparent)] + Device(#[from] DeviceError), + /// Resolving a VM-local interrupt line failed. + #[error(transparent)] + Irq(#[from] IrqError), +} + +/// Result type returned by device factory operations. +pub type DeviceFactoryResult = Result; + +/// VM-owned services available while a device factory builds a device. +pub trait DeviceFactoryContext: Send + Sync { + /// Resolves a VM-local interrupt line with the requested trigger mode. + fn resolve_irq( + &self, + line: usize, + trigger: InterruptTriggerMode, + ) -> DeviceFactoryResult; +} + +/// A device capability that can be polled by the VM runtime. +pub trait PollableDeviceOps: Send + Sync { + /// Advances the device using the current monotonic time in nanoseconds. + fn poll(&self, now_ns: u64) -> DeviceResult; +} + +/// One strongly typed capability contributed by a device factory. +#[non_exhaustive] +pub enum DeviceRegistration { + /// A device implementing the unified [`Device`] trait. + Device(Arc), + /// A capability that requires periodic polling. + Pollable(Arc), +} + +/// Device capability lists consumed by a VM device container. +pub struct DeviceBundleParts { + /// Devices that participate in bus routing and resource registration. + pub devices: Vec>, + /// Device capabilities that require periodic polling. + pub pollable_devices: Vec>, +} + +/// A set of device capabilities built from one VM device configuration. +#[derive(Default)] +pub struct DeviceBundle { + devices: Vec>, + pollable: Vec>, +} + +impl DeviceBundle { + /// Creates an empty bundle. + pub const fn new() -> Self { + Self { + devices: Vec::new(), + pollable: Vec::new(), + } + } + + /// Creates a bundle containing one registration. + pub fn from_registration(registration: DeviceRegistration) -> Self { + let mut bundle = Self::new(); + bundle.push(registration); + bundle + } + + /// Adds one capability to this bundle. + pub fn push(&mut self, registration: DeviceRegistration) { + match registration { + DeviceRegistration::Device(device) => self.devices.push(device), + DeviceRegistration::Pollable(device) => self.pollable.push(device), + } + } + + /// Adds one capability and returns the bundle for builder-style use. + pub fn with_registration(mut self, registration: DeviceRegistration) -> Self { + self.push(registration); + self + } + + /// Returns whether this bundle contains no capabilities. + pub fn is_empty(&self) -> bool { + self.devices.is_empty() && self.pollable.is_empty() + } + + /// Splits the bundle into the capabilities consumed by the VM device container. + pub fn into_parts(self) -> DeviceBundleParts { + DeviceBundleParts { + devices: self.devices, + pollable_devices: self.pollable, + } + } +} + +impl From for DeviceBundle { + fn from(registration: DeviceRegistration) -> Self { + Self::from_registration(registration) + } +} + +/// Builds all capabilities contributed by one emulated device type. +pub trait DeviceFactory: Send + Sync { + /// Returns the VM configuration type handled by this factory. + fn device_type(&self) -> EmulatedDeviceType; + + /// Builds device instances from one VM device configuration. + fn build( + &self, + config: &EmulatedDeviceConfig, + context: &dyn DeviceFactoryContext, + ) -> DeviceFactoryResult; +} + +/// Static factory registration entry collected from the final image. +/// +/// This is an internal Rust-image layout contract, not a cross-language ABI. +#[repr(C)] +#[derive(Clone, Copy)] +pub struct DeviceFactoryRegister { + name: &'static str, + factory: &'static dyn DeviceFactory, +} + +impl DeviceFactoryRegister { + /// Creates a static factory registration entry. + pub const fn new(name: &'static str, factory: &'static dyn DeviceFactory) -> Self { + Self { name, factory } + } + + /// Returns the human-readable factory name used for diagnostics. + pub const fn name(&self) -> &'static str { + self.name + } + + /// Returns the registered factory. + pub const fn factory(&self) -> &'static dyn DeviceFactory { + self.factory + } +} + +/// Registers a device factory in the `.axdevice.factory` linker section. +/// +/// The concrete device crate must be linked into the final image. The final +/// linker script must retain `.axdevice.factory*` and export the section's +/// start and end symbols. +#[macro_export] +macro_rules! register_device_factory { + ($name:expr, $factory:expr $(,)?) => { + const _: () = { + #[unsafe(link_section = ".axdevice.factory")] + #[used] + static FACTORY_REGISTER: $crate::DeviceFactoryRegister = + $crate::DeviceFactoryRegister::new($name, &$factory); + }; + }; +} diff --git a/virtualization/axdevice_base/src/lib.rs b/virtualization/axdevice_base/src/lib.rs index eb5d4e852b..408e7f9d78 100644 --- a/virtualization/axdevice_base/src/lib.rs +++ b/virtualization/axdevice_base/src/lib.rs @@ -596,7 +596,12 @@ pub trait BusRouter { // --------------------------------------------------------------------------- mod adapter; +mod factory_contract; mod irq; pub use adapter::{MmioDeviceAdapter, PortDeviceAdapter, SysRegDeviceAdapter}; +pub use factory_contract::{ + DeviceBundle, DeviceBundleParts, DeviceFactory, DeviceFactoryContext, DeviceFactoryError, + DeviceFactoryRegister, DeviceFactoryResult, DeviceRegistration, PollableDeviceOps, +}; pub use irq::{IrqError, IrqLine, IrqResult, IrqSink}; diff --git a/virtualization/axdevice_base/src/resource.rs b/virtualization/axdevice_base/src/resource.rs new file mode 100644 index 0000000000..39e4a8420b --- /dev/null +++ b/virtualization/axdevice_base/src/resource.rs @@ -0,0 +1,100 @@ +// Copyright 2025 The Axvisor Team +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Resource and capability declarations for emulated devices. + +use axvm_types::GuestPhysAddrRange; + +use crate::{IrqLine, PortRange, SysRegAddrRange}; + +/// The kind of PCI BAR exposed by a device. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum PciBarKind { + /// A 32-bit memory BAR. + Mmio32 { + /// Whether the BAR is prefetchable. + prefetchable: bool, + }, + /// A 64-bit memory BAR. + Mmio64 { + /// Whether the BAR is prefetchable. + prefetchable: bool, + }, + /// A port I/O BAR. + Pio, +} + +/// A resource occupied or requested by an emulated device. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Resource { + /// A guest physical MMIO range. + Mmio(GuestPhysAddrRange), + /// A port I/O range. + Pio(PortRange), + /// A system register range. + SysReg(SysRegAddrRange), + /// A legacy interrupt line. + Irq(IrqLine), + /// A message-signaled interrupt vector allocation request. + Msi { + /// Number of MSI/MSI-X vectors requested or supported. + vectors: u16, + }, + /// The device can initiate DMA to guest memory. + Dma, + /// A PCI BAR exposed by the device. + PciBar { + /// BAR index in PCI configuration space. + index: u8, + /// BAR type and attributes. + kind: PciBarKind, + }, +} + +/// Capability flags exposed by an emulated device. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] +pub struct DeviceCapabilities { + /// The device supports MSI. + pub msi: bool, + /// The device supports MSI-X. + pub msix: bool, + /// The device may initiate DMA. + pub dma: bool, + /// The device exposes one or more PCI BARs. + pub pci_bar: bool, + /// The device has a meaningful reset operation. + pub reset: bool, + /// The device has a meaningful suspend operation. + pub suspend: bool, + /// The device has a meaningful resume operation. + pub resume: bool, +} + +impl DeviceCapabilities { + /// No optional capabilities. + pub const NONE: Self = Self { + msi: false, + msix: false, + dma: false, + pci_bar: false, + reset: false, + suspend: false, + resume: false, + }; + + /// Returns an empty capability set. + pub const fn none() -> Self { + Self::NONE + } +} diff --git a/virtualization/axvm/src/arch/riscv64/irq.rs b/virtualization/axvm/src/arch/riscv64/irq.rs index fc4b540753..362c22f995 100644 --- a/virtualization/axvm/src/arch/riscv64/irq.rs +++ b/virtualization/axvm/src/arch/riscv64/irq.rs @@ -17,8 +17,8 @@ use alloc::sync::Arc; use axdevice::{ - DeviceBuildContext, DeviceBundle, DeviceFactory, DeviceFactoryRegistry, DeviceManagerError, - DeviceManagerResult, DeviceRegistration, MmioDeviceAdapter, + DeviceBundle, DeviceFactory, DeviceFactoryContext, DeviceFactoryError, DeviceFactoryRegistry, + DeviceFactoryResult, DeviceRegistration, MmioDeviceAdapter, }; use axdevice_base::{IrqError, IrqLineId, IrqResult, IrqSink}; use axvm_types::{EmulatedDeviceConfig, EmulatedDeviceType, VMInterruptMode}; @@ -72,13 +72,13 @@ impl DeviceFactory for RiscvPlicFactory { fn build( &self, config: &EmulatedDeviceConfig, - _context: &DeviceBuildContext<'_>, - ) -> DeviceManagerResult { + _context: &dyn DeviceFactoryContext, + ) -> DeviceFactoryResult { if config.base_gpa != self.base_gpa || config.length != self.length || config.cfg_list.as_slice() != [self.contexts_num] { - return Err(DeviceManagerError::InvalidConfig { + return Err(DeviceFactoryError::InvalidConfig { operation: "build virtual PLIC", detail: alloc::format!( "factory configuration does not match device '{}'", diff --git a/virtualization/axvm/src/irq/mod.rs b/virtualization/axvm/src/irq/mod.rs index 5ae6c5af25..e388c42dda 100644 --- a/virtualization/axvm/src/irq/mod.rs +++ b/virtualization/axvm/src/irq/mod.rs @@ -16,7 +16,7 @@ use alloc::sync::Arc; -use axdevice::{DeviceManagerResult, IrqResolver}; +use axdevice::IrqResolver; use axdevice_base::{InterruptTriggerMode, IrqError, IrqLine, IrqLineId, IrqResult, IrqSink}; use axvm_types::VMInterruptMode; @@ -145,11 +145,7 @@ impl Default for InterruptFabric { } impl IrqResolver for InterruptFabric { - fn resolve_irq( - &self, - line: usize, - trigger: InterruptTriggerMode, - ) -> DeviceManagerResult { + fn resolve_irq(&self, line: usize, trigger: InterruptTriggerMode) -> IrqResult { Ok(IrqLine::new( IrqLineId(line), trigger, diff --git a/virtualization/axvm/src/vm/prepare.rs b/virtualization/axvm/src/vm/prepare.rs index c2fa477eb5..74e5927ac0 100644 --- a/virtualization/axvm/src/vm/prepare.rs +++ b/virtualization/axvm/src/vm/prepare.rs @@ -6,7 +6,7 @@ pub(crate) mod vcpus; use alloc::{format, sync::Arc}; -use axdevice::{DeviceFactoryRegistry, register_builtin_factories}; +use axdevice::{DeviceFactoryRegistry, register_builtin_factories, register_linker_factories}; use self::{devices::PreparedDevices, vcpus::PreparedVcpus}; use super::{AxVM, AxVMResources}; @@ -56,6 +56,7 @@ impl AxVM { pub(crate) fn default_device_factories() -> AxVmResult { let mut factories = DeviceFactoryRegistry::new(); register_builtin_factories(&mut factories)?; + register_linker_factories(&mut factories)?; Ok(factories) } diff --git a/virtualization/test_crates/virtualization-tests/tests/axdevice.rs b/virtualization/test_crates/virtualization-tests/tests/axdevice.rs index 2587b7090b..875faff5bd 100644 --- a/virtualization/test_crates/virtualization-tests/tests/axdevice.rs +++ b/virtualization/test_crates/virtualization-tests/tests/axdevice.rs @@ -16,14 +16,14 @@ use std::sync::{Arc, Mutex}; use axdevice::{ AxVmDeviceConfig, AxVmDevices, DeviceBuildContext, DeviceBundle, DeviceFactory, - DeviceFactoryRegistry, DeviceManagerError, DeviceManagerResult, DeviceRegistration, - IrqResolver, MmioDeviceAdapter, PollableDeviceOps, PortDeviceAdapter, SysRegDeviceAdapter, - register_builtin_factories, + DeviceFactoryContext, DeviceFactoryError, DeviceFactoryRegistry, DeviceFactoryResult, + DeviceManagerError, DeviceRegistration, IrqResolver, MmioDeviceAdapter, PollableDeviceOps, + PortDeviceAdapter, SysRegDeviceAdapter, register_builtin_factories, }; use axdevice_base::{ AccessWidth, BaseDeviceOps, Device, DeviceError, DeviceRegistry as _, DeviceResult, - InterruptTriggerMode, InvalidResourceReason, IrqError, IrqLine, IrqLineId, Port, PortRange, - RegistryError, Resource, SysRegAddr, SysRegAddrRange, + InterruptTriggerMode, InvalidResourceReason, IrqError, IrqLine, IrqLineId, IrqResult, Port, + PortRange, RegistryError, Resource, SysRegAddr, SysRegAddrRange, }; use axvm_types::{EmulatedDeviceConfig, EmulatedDeviceType, GuestPhysAddr, GuestPhysAddrRange}; @@ -212,7 +212,7 @@ impl BaseDeviceOps for MockMmioPollableDevice { } impl PollableDeviceOps for MockMmioPollableDevice { - fn poll(&self, now_ns: u64) -> DeviceManagerResult { + fn poll(&self, now_ns: u64) -> DeviceResult { self.polled_at.lock().unwrap().push(now_ns); Ok(()) } @@ -282,17 +282,12 @@ fn device_config( struct RejectingIrqResolver; impl IrqResolver for RejectingIrqResolver { - fn resolve_irq( - &self, - line: usize, - _trigger: InterruptTriggerMode, - ) -> DeviceManagerResult { + fn resolve_irq(&self, line: usize, _trigger: InterruptTriggerMode) -> IrqResult { Err(IrqError::Unsupported { line: IrqLineId(line), operation: "resolve test IRQ", detail: "test resolver rejects every line".into(), - } - .into()) + }) } } @@ -306,16 +301,16 @@ impl DeviceFactory for MockMmioFactory { fn build( &self, config: &EmulatedDeviceConfig, - _context: &DeviceBuildContext<'_>, - ) -> DeviceManagerResult { + _context: &dyn DeviceFactoryContext, + ) -> DeviceFactoryResult { let Some(end) = config.base_gpa.checked_add(config.length) else { - return Err(DeviceManagerError::InvalidConfig { + return Err(DeviceFactoryError::InvalidConfig { operation: "build mock MMIO device", detail: "device address range overflows".into(), }); }; if config.length == 0 { - return Err(DeviceManagerError::InvalidConfig { + return Err(DeviceFactoryError::InvalidConfig { operation: "build mock MMIO device", detail: "device range is empty".into(), }); diff --git a/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs b/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs index 2965a5fd27..b9009e4e61 100644 --- a/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs +++ b/virtualization/test_crates/virtualization-tests/tests/axvm_irq.rs @@ -17,8 +17,8 @@ use std::sync::{Arc, Mutex, Weak}; use ax_plat::{console::ConsoleIf, time::TimeIf}; use axdevice::{ AxVmDeviceConfig, AxVmDevices, DeviceBuildContext, DeviceBundle, DeviceFactory, - DeviceFactoryRegistry, DeviceManagerError, DeviceManagerResult, DeviceRegistration, - IrqResolver, MmioDeviceAdapter, + DeviceFactoryContext, DeviceFactoryError, DeviceFactoryRegistry, DeviceFactoryResult, + DeviceManagerError, DeviceRegistration, IrqResolver, MmioDeviceAdapter, }; use axdevice_base::{ AccessWidth, BaseDeviceOps, DeviceResult, InterruptTriggerMode, IrqError, IrqLine, IrqLineId, @@ -153,10 +153,10 @@ impl DeviceFactory for IrqMmioFactory { fn build( &self, config: &EmulatedDeviceConfig, - context: &DeviceBuildContext<'_>, - ) -> DeviceManagerResult { + context: &dyn DeviceFactoryContext, + ) -> DeviceFactoryResult { let Some(end) = config.base_gpa.checked_add(config.length) else { - return Err(DeviceManagerError::InvalidConfig { + return Err(DeviceFactoryError::InvalidConfig { operation: "build IRQ MMIO test device", detail: "device address range overflows".into(), });