From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-qemu-server-rs v2 3/9] pci: legacy: generate q35 chipset devices and hostpcie root ports
Date: Thu, 15 Oct 2026 10:10:25 +0200 [thread overview]
Message-ID: <20261015081041.743422-4-d.csapak@proxmox.com> (raw)
In-Reply-To: <20261015081041.743422-1-d.csapak@proxmox.com>
Until now, the q35 chipset devices were added via the pve-q35-4.0.cfg
machine config file, and the root ports for hostpcie4 and above were
added by the caller for each configured device.
The latter can leave a root port without a function 0 in its slot,
e.g. when only hostpci5 is configured. Since guests only scan the
other functions of a slot when function 0 exists (Linux stops in
`pci_scan_slot()`), such a device was not visible in the guest.
To fix that and to have the layout as the single source for the pci
hierarchy, generate both in `pci_bus_devices`:
* the chipset devices with the same ids, properties and order as in
the config file, which a test checks against a copy of it. Machines
older than 4.0 used pve-q35.cfg instead, but those are not supported
anymore.
* the root ports of the configured hostpcie devices, plus the one on
function 0 of each used slot. Devices that were already visible keep
their address, and Windows 7 guests still don't get any root ports.
Since the devices of running guests must not change for live
migration, we only generate the chipset devices starting with machine
version 11.1+pve1. For older machines, `uses_q35_config_file` tells
the caller to keep using the config file.
This replaces `print_pcie_root_port`, as we now decide here which root
ports are needed.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
pve-qemu-server-pci/src/hardware_config.rs | 28 ++
pve-qemu-server-pci/src/layout/legacy.rs | 325 +++++++++++++++++-
pve-qemu-server-pci/src/lib.rs | 2 +-
pve-qemu-server-pci/src/types/layout.rs | 63 +++-
pve-qemu-server-pci/src/types/mod.rs | 2 +-
pve-qemu-server-pci/test-data/pve-q35-4.0.cfg | 163 +++++++++
6 files changed, 572 insertions(+), 11 deletions(-)
create mode 100644 pve-qemu-server-pci/test-data/pve-q35-4.0.cfg
diff --git a/pve-qemu-server-pci/src/hardware_config.rs b/pve-qemu-server-pci/src/hardware_config.rs
index a348353..677fe0a 100644
--- a/pve-qemu-server-pci/src/hardware_config.rs
+++ b/pve-qemu-server-pci/src/hardware_config.rs
@@ -1,8 +1,11 @@
+use std::collections::BTreeMap;
use std::sync::Arc;
use std::sync::atomic::AtomicU16;
use pve_api_types::{ClusterResourceHostArch as Arch, QemuConfigOstype, QemuConfigScsihw};
+use crate::Device;
+
#[derive(Default, Clone, Debug, PartialEq)]
pub enum Mainboard {
#[default]
@@ -31,6 +34,10 @@ impl Mainboard {
}
}
+/// A passed through PCIe device of the guest.
+#[derive(Default, Clone, Debug, PartialEq, Eq)]
+pub struct HostpcieDevice {}
+
/// The parts of a guest configuration that influence which PCI layout is used
/// and which quirks have to be applied to it.
///
@@ -52,6 +59,9 @@ pub struct HardwareConfig {
pub machine_version: (u16, u16, Option<u16>),
/// The recorded required `+pveX` version
pub required_pve_version: Arc<AtomicU16>,
+ /// The configured `hostpcie` devices, keyed by their index, which decide
+ /// what root ports are needed.
+ pub hostpcie_devices: BTreeMap<u8, HostpcieDevice>,
}
impl HardwareConfig {
@@ -93,6 +103,7 @@ impl HardwareConfig {
ostype,
machine_version: (major, minor, pve),
required_pve_version: Arc::new(AtomicU16::new(0)),
+ hostpcie_devices: BTreeMap::new(),
}
}
@@ -107,6 +118,14 @@ impl HardwareConfig {
self.mainboard == Mainboard::Q35
}
+ /// Whether the legacy q35 chipset devices come from qemu-server's
+ /// `pve-q35-4.0.cfg` machine config file. Only machines from 11.1+pve1 on
+ /// get them from [`crate::pci_bus_devices`], so older guests stay unchanged
+ /// for live migration.
+ pub fn uses_q35_config_file(&self) -> bool {
+ self.is_q35() && !self.version_at_least(11, 1, Some(1))
+ }
+
/// Whether each SCSI disk gets its own controller, which needs the extra
/// `pci.3` bridge.
pub fn virtio_scsi_single(&self) -> bool {
@@ -133,6 +152,15 @@ impl HardwareConfig {
}
}
+ /// The configured `hostpcie` devices.
+ pub(crate) fn hostpcie(&self) -> Vec<Device> {
+ self.hostpcie_devices
+ .keys()
+ .copied()
+ .map(Device::Hostpcie)
+ .collect()
+ }
+
/// Returns the version number for Windows OS types or 0 for other.
///
/// Windows XP era OS types are represented by `5`, and Vista era ones by `6`.
diff --git a/pve-qemu-server-pci/src/layout/legacy.rs b/pve-qemu-server-pci/src/layout/legacy.rs
index 6d5cc81..d89c0bf 100644
--- a/pve-qemu-server-pci/src/layout/legacy.rs
+++ b/pve-qemu-server-pci/src/layout/legacy.rs
@@ -1,7 +1,9 @@
use crate::constants::SLOTS_PER_BUS;
use crate::hardware_config::Mainboard;
use crate::layout::bridge_slots;
-use crate::{Bus, Device, Function, HardwareConfig, Layout, LayoutError, PciAddress, Slot};
+use crate::{
+ Bus, Device, Function, HardwareConfig, Layout, LayoutError, PciAddress, RootPort, Slot,
+};
const PCI_1: [Slot; SLOTS_PER_BUS] = bridge_slots(&[
single_device!(Net(6)),
@@ -319,6 +321,69 @@ fn map_legacy_pci_id(original: &str) -> &str {
original
}
+/// The q35 chipset devices that are not part of the QEMU machine type, in the
+/// order they were defined in pve-q35-4.0.cfg, which added them previously.
+/// UHCI controllers use the bus of their EHCI controller, so it comes first.
+#[expect(deprecated)]
+const Q35_CHIPSET_DEVICES: [Device; 18] = [
+ Device::Ich9Ehci(1),
+ Device::Ich9Uhci(1),
+ Device::Ich9Uhci(2),
+ Device::Ich9Uhci(3),
+ Device::Ich9Ehci(2),
+ Device::Ich9Uhci(4),
+ Device::Ich9Uhci(5),
+ Device::Ich9Uhci(6),
+ Device::Ich9Audio,
+ Device::RootPort(Bus::LegacyRootPort(1)),
+ Device::RootPort(Bus::LegacyRootPort(2)),
+ Device::RootPort(Bus::LegacyRootPort(3)),
+ Device::RootPort(Bus::LegacyRootPort(4)),
+ Device::Bridge(Bus::Pcidmi),
+ Device::Bridge(Bus::Pci(0)),
+ Device::Bridge(Bus::Pci(1)),
+ Device::Bridge(Bus::Pci(2)),
+ Device::Bridge(Bus::Pci(3)),
+];
+
+/// Renders a device of [`Q35_CHIPSET_DEVICES`] like the config file did.
+fn print_q35_chipset_device(device: Device) -> Result<String, LayoutError> {
+ let addr = LEGACY_PCIE_LAYOUT.find_device(&device)?;
+ let qemu_addr = addr.to_qemu_addr();
+ let ehci_id = |index: u8| match index {
+ 1 => "ehci".to_string(),
+ _ => format!("ehci-{index}"),
+ };
+
+ let res = match device {
+ Device::Ich9Ehci(index) => {
+ let id = ehci_id(index);
+ format!("ich9-usb-ehci{index},id={id},multifunction=on{qemu_addr}")
+ }
+ Device::Ich9Uhci(index) => {
+ // each EHCI controller has three UHCI companions with two ports each
+ let masterbus = ehci_id((index - 1) / 3 + 1);
+ let firstport = (index - 1) % 3 * 2;
+ format!(
+ "ich9-usb-uhci{index},id=uhci-{index},multifunction=on{qemu_addr},masterbus={masterbus}.0,firstport={firstport}"
+ )
+ }
+ Device::Ich9Audio => format!("ich9-intel-hda,id=audio0{qemu_addr}"),
+ #[expect(deprecated)]
+ Device::RootPort(id @ Bus::LegacyRootPort(index)) => {
+ RootPort { addr, id, index }.to_qemu_device()
+ }
+ Device::Bridge(Bus::Pcidmi) => format!("i82801b11-bridge,id=pcidmi{qemu_addr}"),
+ Device::Bridge(Bus::Pci(index)) => {
+ let chassis_nr = index + 1;
+ format!("pci-bridge,id=pci.{index},chassis_nr={chassis_nr}{qemu_addr}")
+ }
+ _ => return Err(LayoutError::NoAddressFound(device)),
+ };
+
+ Ok(res)
+}
+
/// Looks up the PCI bridge a device sits on, see
/// [`crate::pci_bridge_for_device`].
pub(crate) fn pci_bridge_for_device(id: &str) -> Option<Bus> {
@@ -331,17 +396,24 @@ pub(crate) fn pci_bridge_for_device(id: &str) -> Option<Bus> {
Some(addr.bus)
}
-/// Renders the bridge devices a guest needs, see
+/// Renders the chipset devices, bridges and root ports a guest needs, see
/// [`crate::pci_bus_devices`].
pub(crate) fn pci_bus_devices(hw: &HardwareConfig) -> Result<Vec<String>, LayoutError> {
let mut res = Vec::new();
+ if hw.mainboard == Mainboard::Q35 && !hw.uses_q35_config_file() {
+ for device in Q35_CHIPSET_DEVICES {
+ res.push("-device".to_string());
+ res.push(print_q35_chipset_device(device)?);
+ }
+ }
+
// some SCSI controllers can only have 7 disks each, so scsi14 and upwards
// need scsihw2 and above, which live on pci.4
let include_pci4 = hw.version_at_least(11, 1, None) || hw.max_scsihw > 1;
for i in 1..=4 {
- // q35.cfg already includes the bridges up to three
+ // the bridges up to three are part of the q35 chipset
if i < 4 && hw.mainboard == Mainboard::Q35 {
continue;
}
@@ -365,6 +437,18 @@ pub(crate) fn pci_bus_devices(hw: &HardwareConfig) -> Result<Vec<String>, Layout
res.push(format!("pci-bridge,id=pci.{i},chassis_nr={i}{addr}"));
}
+ // Windows 7 gets its PCIe devices directly on the root bus
+ if hw.mainboard == Mainboard::Q35 && hw.windows_version() != 7 {
+ for port in LEGACY_PCIE_LAYOUT.root_ports(&hw.hostpcie()) {
+ // the first four root ports are part of the q35 chipset
+ if port.index <= 4 {
+ continue;
+ }
+ res.push("-device".to_string());
+ res.push(port.to_qemu_device());
+ }
+ }
+
Ok(res)
}
@@ -398,14 +482,15 @@ pub(crate) fn address(hw: &HardwareConfig, id: &str) -> Result<PciAddress, Layou
#[cfg(test)]
pub(crate) mod test {
- use std::collections::HashSet;
+ use std::collections::{BTreeMap, HashSet};
use super::{
- LEGACY_PCIE_LAYOUT, get_legacy_pci_layout, pci_bridge_for_device, pci_bus_devices,
+ LEGACY_PCIE_LAYOUT, Q35_CHIPSET_DEVICES, get_legacy_pci_layout, pci_bridge_for_device,
+ pci_bus_devices,
};
use crate::hardware_config::Mainboard;
use crate::layout::legacy::is_pcie_id;
- use crate::{Bus, Device, HardwareConfig, Layout, PciAddress};
+ use crate::{Bus, Device, HardwareConfig, HostpcieDevice, Layout, PciAddress};
pub const LEGACY_ADDRS: [(&str, u8, u8); 121] = [
("ahci0", 0, 7),
@@ -617,6 +702,48 @@ pub(crate) mod test {
}
}
+ #[test]
+ /// test pcie port addresses
+ fn pcie_test_rp_addr() {
+ let expected = [
+ (0x1c, 0),
+ (0x1c, 1),
+ (0x1c, 2),
+ (0x1c, 3),
+ (0x10, 0),
+ (0x10, 1),
+ (0x10, 2),
+ (0x10, 3),
+ (0x10, 4),
+ (0x10, 5),
+ (0x10, 6),
+ (0x10, 7),
+ (0x11, 0),
+ (0x11, 1),
+ (0x11, 2),
+ (0x11, 3),
+ ];
+ let devices: Vec<Device> = (0..expected.len() as u8).map(Device::Hostpcie).collect();
+ let expected: Vec<String> = expected
+ .iter()
+ .enumerate()
+ .map(|(i, (addr, fnr))| {
+ let index = i + 1;
+ format!(
+ "pcie-root-port,id=ich9-pcie-port-{index},addr={addr:02x}.{fnr},x-speed=16,x-width=32,multifunction=on,bus=pcie.0,port={index},chassis={index}",
+ )
+ })
+ .collect();
+ let mut found: Vec<String> = LEGACY_PCIE_LAYOUT
+ .root_ports(&devices)
+ .iter()
+ .map(|port| port.to_qemu_device())
+ .collect();
+ // the ports are returned in layout order, where slot 0x1c comes last
+ found.rotate_right(4);
+ assert_eq!(found, expected);
+ }
+
/// A bus can only be used once the bridge or root port creating it has
/// been yielded, otherwise a command line built in iteration order would
/// reference a bus QEMU does not know yet.
@@ -665,6 +792,35 @@ pub(crate) mod test {
assert_eq!(pci_bridge_for_device("no-such-device"), None);
}
+ /// The number of q35 chipset devices `pci_bus_devices` generates for `hw`.
+ fn generated_chipset(hw: &HardwareConfig) -> usize {
+ if hw.is_q35() && !hw.uses_q35_config_file() {
+ Q35_CHIPSET_DEVICES.len()
+ } else {
+ 0
+ }
+ }
+
+ #[test]
+ fn test_q35_chipset_version() {
+ let test = |q35: bool, version: (u16, u16, Option<u16>), config_file: bool| {
+ let mut hw = hw_config(q35, None, 0, false);
+ hw.machine_version = version;
+ assert_eq!(hw.uses_q35_config_file(), config_file, "for {version:?}");
+ let res = pci_bus_devices(&hw).expect("valid machine");
+ let has_chipset = res
+ .iter()
+ .any(|device| device.starts_with("ich9-usb-ehci1,"));
+ assert_eq!(has_chipset, q35 && !config_file, "for {version:?}");
+ };
+
+ test(true, (11, 1, None), true);
+ test(true, (11, 1, Some(0)), true);
+ test(true, (11, 1, Some(1)), false);
+ test(true, (12, 0, Some(0)), false);
+ test(false, (11, 1, Some(1)), false);
+ }
+
fn hw_config(
q35: bool,
scsihw: Option<&str>,
@@ -700,12 +856,20 @@ pub(crate) mod test {
let test = |hw: HardwareConfig, expected: &[&[String; 2]]| {
let expected: Vec<String> = expected.iter().flat_map(|b| b.iter().cloned()).collect();
- assert_eq!(pci_bus_devices(&hw), Ok(expected));
+ let res = pci_bus_devices(&hw).expect("valid machine");
+ // the q35 chipset is tested separately
+ let res: Vec<String> = res
+ .chunks(2)
+ .skip(generated_chipset(&hw))
+ .flatten()
+ .cloned()
+ .collect();
+ assert_eq!(res, expected);
};
test(hw_config(false, None, 0, false), &[&pci1, &pci2]);
- // q35 already brings the bridges up to three along
+ // the bridges up to three are part of the q35 chipset
test(hw_config(true, None, 0, false), &[]);
// every SCSI disk gets its own controller on pci.3
@@ -732,4 +896,149 @@ pub(crate) mod test {
expected_igd[1] = "pci-bridge,id=pci.2,chassis_nr=2,bus=pci.1,addr=0x1e".to_string();
test(hw_config(false, None, 0, true), &[&pci1, &expected_igd]);
}
+
+ fn root_port(index: u8, addr: &str) -> [String; 2] {
+ [
+ "-device".to_string(),
+ format!(
+ "pcie-root-port,id=ich9-pcie-port-{index},addr={addr},x-speed=16,x-width=32,multifunction=on,bus=pcie.0,port={index},chassis={index}"
+ ),
+ ]
+ }
+
+ #[test]
+ fn test_pci_bus_devices_root_ports() {
+ let test = |q35: bool, ostype: Option<&str>, hostpcie: &[u8], expected: &[[String; 2]]| {
+ let mut hw = hw_config(q35, None, 0, false);
+ hw.ostype = ostype.and_then(|ostype| ostype.parse().ok());
+ hw.hostpcie_devices = hostpcie
+ .iter()
+ .map(|&index| (index, HostpcieDevice::default()))
+ .collect();
+ let expected: Vec<String> = expected.iter().flatten().cloned().collect();
+ let res = pci_bus_devices(&hw).expect("valid machine");
+ // only look at the root ports, the rest is tested separately
+ let res: Vec<String> = res
+ .chunks(2)
+ .skip(generated_chipset(&hw))
+ .filter(|device| device[1].starts_with("pcie-root-port"))
+ .flatten()
+ .cloned()
+ .collect();
+ assert_eq!(res, expected, "for {hostpcie:?}");
+ };
+
+ // the first four root ports are part of the q35 chipset
+ test(true, None, &[0, 3], &[]);
+ test(true, None, &[4], &[root_port(5, "10.0")]);
+ test(
+ true,
+ None,
+ &[0, 12, 4],
+ &[root_port(5, "10.0"), root_port(13, "11.0")],
+ );
+ // function 0 of a used slot has to exist for guests to see the others
+ test(
+ true,
+ None,
+ &[5, 7],
+ &[
+ root_port(5, "10.0"),
+ root_port(6, "10.1"),
+ root_port(8, "10.3"),
+ ],
+ );
+ test(
+ true,
+ None,
+ &[14],
+ &[root_port(13, "11.0"), root_port(15, "11.2")],
+ );
+
+ // windows 7 has its devices directly on the root bus
+ test(true, Some("win7"), &[5], &[]);
+ test(false, None, &[5], &[]);
+ }
+
+ /// Parses QEMU device options into the id, the driver and the remaining
+ /// properties, with the address normalized to slot and function.
+ fn normalize_device(
+ id: &str,
+ driver: &str,
+ props: &mut dyn Iterator<Item = (String, String)>,
+ ) -> (String, String, BTreeMap<String, String>) {
+ let mut props: BTreeMap<String, String> = props.collect();
+ if let Some(addr) = props.get_mut("addr") {
+ let addr_str = addr.trim_start_matches("0x");
+ let (slot, function) = addr_str.split_once('.').unwrap_or((addr_str, "0"));
+ let slot = u8::from_str_radix(slot, 16).expect("valid slot");
+ *addr = format!("{slot:02x}.{function}");
+ }
+ (id.to_string(), driver.to_string(), props)
+ }
+
+ /// Parses the device sections of a QEMU config file.
+ fn parse_qemu_cfg(cfg: &str) -> Vec<(String, String, BTreeMap<String, String>)> {
+ let mut devices = Vec::new();
+ let mut current: Option<(String, Vec<(String, String)>)> = None;
+ let mut finish = |current: Option<(String, Vec<(String, String)>)>| {
+ if let Some((id, mut props)) = current {
+ let pos = props.iter().position(|(k, _)| k == "driver").unwrap();
+ let (_, driver) = props.remove(pos);
+ devices.push(normalize_device(&id, &driver, &mut props.into_iter()));
+ }
+ };
+ for line in cfg.lines().map(str::trim) {
+ if line.is_empty() || line.starts_with('#') {
+ continue;
+ }
+ if let Some(id) = line.strip_prefix("[device \"") {
+ finish(current.take());
+ current = Some((id.trim_end_matches("\"]").to_string(), Vec::new()));
+ } else if let Some((key, value)) = line.split_once('=') {
+ let value = value.trim().trim_matches('"').to_string();
+ current
+ .as_mut()
+ .unwrap()
+ .1
+ .push((key.trim().to_string(), value));
+ }
+ }
+ finish(current);
+ devices
+ }
+
+ #[test]
+ /// The q35 chipset devices have to match what the machine config file of
+ /// qemu-server created before, as they are part of the guest's hardware.
+ fn test_q35_chipset_matches_cfg() {
+ let mut hw = hw_config(true, None, 0, false);
+ hw.machine_version = (11, 1, Some(1));
+ let res = pci_bus_devices(&hw).expect("valid machine");
+ let generated: Vec<_> = res
+ .chunks(2)
+ .take(Q35_CHIPSET_DEVICES.len())
+ .map(|device| {
+ assert_eq!(device[0], "-device");
+ let mut parts = device[1].split(',');
+ let driver = parts.next().unwrap();
+ let mut props = parts.map(|part| {
+ let (key, value) = part.split_once('=').unwrap();
+ (key.to_string(), value.to_string())
+ });
+ let id = props.next().unwrap();
+ assert_eq!(id.0, "id");
+ normalize_device(&id.1, driver, &mut props)
+ })
+ .collect();
+ let expected = parse_qemu_cfg(PVE_Q35_4_0_CFG);
+ assert_eq!(expected.len(), Q35_CHIPSET_DEVICES.len());
+ assert_eq!(generated, expected);
+ }
+
+ /// Copy of qemu-server's `pve-q35-4.0.cfg`.
+ const PVE_Q35_4_0_CFG: &str = include_str!(concat!(
+ env!("CARGO_MANIFEST_DIR"),
+ "/test-data/pve-q35-4.0.cfg"
+ ));
}
diff --git a/pve-qemu-server-pci/src/lib.rs b/pve-qemu-server-pci/src/lib.rs
index fc982ce..a5d82a4 100644
--- a/pve-qemu-server-pci/src/lib.rs
+++ b/pve-qemu-server-pci/src/lib.rs
@@ -11,7 +11,7 @@
pub mod constants;
mod hardware_config;
-pub use hardware_config::HardwareConfig;
+pub use hardware_config::{HardwareConfig, HostpcieDevice};
mod types;
pub use types::*;
diff --git a/pve-qemu-server-pci/src/types/layout.rs b/pve-qemu-server-pci/src/types/layout.rs
index 889b961..91b1d78 100644
--- a/pve-qemu-server-pci/src/types/layout.rs
+++ b/pve-qemu-server-pci/src/types/layout.rs
@@ -1,4 +1,4 @@
-use std::collections::VecDeque;
+use std::collections::{HashSet, VecDeque};
use crate::constants::{FUNCTIONS_PER_DEVICE, SLOTS_PER_BUS, USABLE_SLOTS_PER_BUS};
use crate::{Bus, Device, LayoutError, PciAddress, PveBridge};
@@ -48,6 +48,46 @@ pub struct Layout<'a> {
}
impl Layout<'_> {
+ /// Returns the root ports `devices` are attached to in layout order, plus
+ /// the one on function 0 of each used slot, as guests only look for further
+ /// functions if function 0 exists.
+ pub fn root_ports(&self, devices: &[Device]) -> Vec<RootPort> {
+ let attached: HashSet<PciAddress> = devices
+ .iter()
+ .filter_map(|device| self.find_device(device).ok())
+ .collect();
+ let is_used = |port: &RootPort| attached.contains(&PciAddress::new(port.id, 0, 0));
+
+ let ports: Vec<RootPort> = self
+ .iter()
+ .filter_map(|used| match used.device {
+ #[expect(deprecated)]
+ Device::RootPort(id @ (Bus::RootPort(index) | Bus::LegacyRootPort(index))) => {
+ Some(RootPort {
+ addr: used.addr,
+ id,
+ index,
+ })
+ }
+ _ => None,
+ })
+ .collect();
+ let used_slots: HashSet<(Bus, u8)> = ports
+ .iter()
+ .filter(|port| is_used(port))
+ .map(|port| (port.addr.bus, port.addr.device))
+ .collect();
+
+ ports
+ .into_iter()
+ .filter(|port| {
+ is_used(port)
+ || (port.addr.function == 0
+ && used_slots.contains(&(port.addr.bus, port.addr.device)))
+ })
+ .collect()
+ }
+
/// Looks up the address of a device in this layout.
///
/// The first match wins, so the order of the layout is significant: a
@@ -68,6 +108,27 @@ impl Layout<'_> {
}
}
+/// A PCIe root port of a layout.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct RootPort {
+ pub addr: PciAddress,
+ /// The bus the root port forms.
+ pub id: Bus,
+ /// The number of the root port, also used as port and chassis number.
+ pub index: u8,
+}
+
+impl RootPort {
+ /// Renders the QEMU `-device` argument for this root port.
+ pub fn to_qemu_device(&self) -> String {
+ let RootPort { addr, id, index } = self;
+ format!(
+ "pcie-root-port,id={id},addr={:02x}.{},x-speed=16,x-width=32,multifunction=on,bus={},port={index},chassis={index}",
+ addr.device, addr.function, addr.bus,
+ )
+ }
+}
+
/// The address a layout assigns to a device.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AddressAssignment {
diff --git a/pve-qemu-server-pci/src/types/mod.rs b/pve-qemu-server-pci/src/types/mod.rs
index 3847745..1605066 100644
--- a/pve-qemu-server-pci/src/types/mod.rs
+++ b/pve-qemu-server-pci/src/types/mod.rs
@@ -8,4 +8,4 @@ mod pci_address;
pub use pci_address::PciAddress;
mod layout;
-pub use layout::{AddressAssignment, AddressAssignments, Function, Layout, Slot};
+pub use layout::{AddressAssignment, AddressAssignments, Function, Layout, RootPort, Slot};
diff --git a/pve-qemu-server-pci/test-data/pve-q35-4.0.cfg b/pve-qemu-server-pci/test-data/pve-q35-4.0.cfg
new file mode 100644
index 0000000..c931417
--- /dev/null
+++ b/pve-qemu-server-pci/test-data/pve-q35-4.0.cfg
@@ -0,0 +1,163 @@
+[device "ehci"]
+ driver = "ich9-usb-ehci1"
+ multifunction = "on"
+ bus = "pcie.0"
+ addr = "1d.7"
+
+[device "uhci-1"]
+ driver = "ich9-usb-uhci1"
+ multifunction = "on"
+ bus = "pcie.0"
+ addr = "1d.0"
+ masterbus = "ehci.0"
+ firstport = "0"
+
+[device "uhci-2"]
+ driver = "ich9-usb-uhci2"
+ multifunction = "on"
+ bus = "pcie.0"
+ addr = "1d.1"
+ masterbus = "ehci.0"
+ firstport = "2"
+
+[device "uhci-3"]
+ driver = "ich9-usb-uhci3"
+ multifunction = "on"
+ bus = "pcie.0"
+ addr = "1d.2"
+ masterbus = "ehci.0"
+ firstport = "4"
+
+[device "ehci-2"]
+ driver = "ich9-usb-ehci2"
+ multifunction = "on"
+ bus = "pcie.0"
+ addr = "1a.7"
+
+[device "uhci-4"]
+ driver = "ich9-usb-uhci4"
+ multifunction = "on"
+ bus = "pcie.0"
+ addr = "1a.0"
+ masterbus = "ehci-2.0"
+ firstport = "0"
+
+[device "uhci-5"]
+ driver = "ich9-usb-uhci5"
+ multifunction = "on"
+ bus = "pcie.0"
+ addr = "1a.1"
+ masterbus = "ehci-2.0"
+ firstport = "2"
+
+[device "uhci-6"]
+ driver = "ich9-usb-uhci6"
+ multifunction = "on"
+ bus = "pcie.0"
+ addr = "1a.2"
+ masterbus = "ehci-2.0"
+ firstport = "4"
+
+# FIXME: Remove this audio0 device at the next possible time
+# see: https://pve.proxmox.com/pipermail/pve-devel/2019-July/038417.html
+# https://pve.proxmox.com/pipermail/pve-devel/2019-July/038428.html
+[device "audio0"]
+ driver = "ich9-intel-hda"
+ bus = "pcie.0"
+ addr = "1b.0"
+
+
+[device "ich9-pcie-port-1"]
+ driver = "pcie-root-port"
+ x-speed = "16"
+ x-width = "32"
+ multifunction = "on"
+ bus = "pcie.0"
+ addr = "1c.0"
+ port = "1"
+ chassis = "1"
+
+[device "ich9-pcie-port-2"]
+ driver = "pcie-root-port"
+ x-speed = "16"
+ x-width = "32"
+ multifunction = "on"
+ bus = "pcie.0"
+ addr = "1c.1"
+ port = "2"
+ chassis = "2"
+
+[device "ich9-pcie-port-3"]
+ driver = "pcie-root-port"
+ x-speed = "16"
+ x-width = "32"
+ multifunction = "on"
+ bus = "pcie.0"
+ addr = "1c.2"
+ port = "3"
+ chassis = "3"
+
+[device "ich9-pcie-port-4"]
+ driver = "pcie-root-port"
+ x-speed = "16"
+ x-width = "32"
+ multifunction = "on"
+ bus = "pcie.0"
+ addr = "1c.3"
+ port = "4"
+ chassis = "4"
+
+##
+# Example PCIe switch with two downstream ports
+#
+#[device "pcie-switch-upstream-port-1"]
+# driver = "x3130-upstream"
+# bus = "ich9-pcie-port-4"
+# addr = "00.0"
+#
+#[device "pcie-switch-downstream-port-1-1"]
+# driver = "xio3130-downstream"
+# multifunction = "on"
+# bus = "pcie-switch-upstream-port-1"
+# addr = "00.0"
+# port = "1"
+# chassis = "5"
+#
+#[device "pcie-switch-downstream-port-1-2"]
+# driver = "xio3130-downstream"
+# multifunction = "on"
+# bus = "pcie-switch-upstream-port-1"
+# addr = "00.1"
+# port = "1"
+# chassis = "6"
+
+
+
+[device "pcidmi"]
+ driver = "i82801b11-bridge"
+ bus = "pcie.0"
+ addr = "1e.0"
+
+[device "pci.0"]
+ driver = "pci-bridge"
+ bus = "pcidmi"
+ addr = "1.0"
+ chassis_nr = "1"
+
+[device "pci.1"]
+ driver = "pci-bridge"
+ bus = "pcidmi"
+ addr = "2.0"
+ chassis_nr = "2"
+
+[device "pci.2"]
+ driver = "pci-bridge"
+ bus = "pcidmi"
+ addr = "3.0"
+ chassis_nr = "3"
+
+[device "pci.3"]
+ driver = "pci-bridge"
+ bus = "pcidmi"
+ addr = "4.0"
+ chassis_nr = "4"
--
2.47.3
next prev parent reply other threads:[~2026-10-15 8:11 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-15 8:10 [RFC proxmox-perl-rs/qemu-server/qemu-server-rs v2 0/9] pci-handling rewrite (part 1/2/3) Dominik Csapak
2026-10-15 8:10 ` [PATCH pve-qemu-server-rs v2 1/9] add pve-qemu-server-pci crate for guest PCI address generation Dominik Csapak
2026-10-15 12:16 ` Wolfgang Bumiller
2026-10-15 8:10 ` [PATCH pve-qemu-server-rs v2 2/9] pci: legacy: generate pci bridges Dominik Csapak
2026-10-15 8:10 ` Dominik Csapak [this message]
2026-10-15 8:10 ` [PATCH pve-qemu-server-rs v2 4/9] pci: layout: add grouped PCI and PCIe layouts Dominik Csapak
2026-10-15 8:10 ` [PATCH proxmox-perl-rs v2 5/9] pve: add bindings for `pve-qemu-server-pci` crate Dominik Csapak
2026-10-15 11:47 ` Wolfgang Bumiller
2026-10-15 8:10 ` [PATCH qemu-server v2 6/9] pci: use PVE::RS::PCI bindings for addresses and bus devices Dominik Csapak
2026-10-15 8:10 ` [PATCH qemu-server v2 7/9] tree-wide: use HardwareConfig for the machine version checks Dominik Csapak
2026-10-15 8:10 ` [PATCH qemu-server v2 8/9] cfg2cmd: q35: generate chipset devices with machine version 11.1+pve1 Dominik Csapak
2026-10-15 8:10 ` [PATCH qemu-server v2 9/9] machine: add 'pci-layout' option Dominik Csapak
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261015081041.743422-4-d.csapak@proxmox.com \
--to=d.csapak@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox