all lists on lists.proxmox.com
 help / color / mirror / Atom feed
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





  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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal