public inbox for pve-devel@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal