From: "Elias Huhsovitz" <e.huhsovitz@proxmox.com>
To: "Dominik Csapak" <d.csapak@proxmox.com>, <pve-devel@lists.proxmox.com>
Subject: Re: [PATCH pve-qemu-server-rs 1/9] add pve-qemu-server-pci crate for guest PCI address generation
Date: Mon, 28 Sep 2026 14:53:48 +0200 [thread overview]
Message-ID: <DLQYQH22CBUB.2URWVLBX6LGM5@proxmox.com> (raw)
In-Reply-To: <20260922105550.2084078-2-d.csapak@proxmox.com>
I tested this locally and it seems to work fine so far.
from a high level view:
* I really like the idea of the layout tree. I currently cannot think
of any better way to do this.
* I also like that this is handled in rust and not perl. handling this
in perl might be easier short term, but overall i don't see much
benefit.
* Will this repo be used only for the qemu-server? If we find some other
uses, perhaps a better name would be something like `pve-pci-rs` or
`pve-guest-pci-rs`. IMO this repo should provide clean PCI topology and
functions and shouldnt be limited to only the qemu-server.
My question extends to the crate name. So maybe `pve-pci-rs` for the repo
name and `pve-guest-pci` for the crate. (IMO we should have some
kind of poll for this).
See other comments inline.
On Tue Sep 22, 2026 at 12:55 PM CEST, Dominik Csapak wrote:
> Since the hardware layout, including PCI addresses, are part of the
> guest's ABI, the assignment has to stay stable for the whole lifetime
> of a VM. The Perl implementation in PVE::QemuServer::PCI encodes this as
> two flat hashes of hardcoded addresses, which is hard to extend without
> accidentally shifting an existing entry.
>
> Describe the assignment as a layout instead: a tree of buses, slots and
> functions that says what may sit where, which addresses are reserved for
> something outside of our control, and which are still free. Looking up a
> device then means walking that tree, and adding a new device means
> filling in a free slot rather than picking a number by hand.
>
> The layouts added here reproduce the current Perl address maps exactly,
> including the quirks for legacy IGD passthrough, virtio-scsi-single and
> the Windows 7 PCIe workaround. The tests cover every id the Perl maps
> contain, so both implementations can be used interchangeably.
>
> Also adds the cargo workspace and the build tooling the other Proxmox
> Rust repositories use to build Debian packages.
>
> Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
[snip]
> + mkfs.erofs extensions/proxmox-workspace.raw build/sysext/workspace
> diff --git a/build.sh b/build.sh
> new file mode 100755
> index 0000000..12444c1
> --- /dev/null
> +++ b/build.sh
> @@ -0,0 +1,40 @@
> +#!/bin/sh
> +
> +set -e
nit: IMO should be set -eu
> +
> +export CARGO=/usr/bin/cargo
> +export RUSTC=/usr/bin/rustc
> +
> +CRATE=$1
> +BUILDCMD=${BUILDCMD:-"dpkg-buildpackage -b -uc -us"}
> +BUILDDIR="${BUILDDIR:-"build"}"
> +TEST_CMD="${TEST_CMD:-"$CARGO test --all-features --all-targets --release"}"
> +
> +mkdir -p "${BUILDDIR}"
> +echo system >"${BUILDDIR}"/rust-toolchain
> +rm -rf ""${BUILDDIR}"/${CRATE}"
nit: redundant quotes, should be
rm -rf "${BUILDDIR}/${CRATE}"
> +
> +CONTROL="$PWD/${CRATE}/debian/control"
> +
> +if [ -e "$CONTROL" ]; then
> + # check but only warn, debcargo fails anyway if crates are missing
> + dpkg-checkbuilddeps $PWD/${CRATE}/debian/control || true
> + [ "x$NOCONTROL" = 'x' ] && rm -f "$PWD/${CRATE}/debian/control"
> +fi
> +
> +debcargo package \
> + --config "$PWD/${CRATE}/debian/debcargo.toml" \
> + --changelog-ready \
> + --no-overlay-write-back \
> + --directory "$PWD/"${BUILDDIR}"/${CRATE}" \
> + "${CRATE}" \
> + "$(dpkg-parsechangelog -l "${CRATE}/debian/changelog" -SVersion | sed -e 's/-.*//')"
> +
> +cd ""${BUILDDIR}"/${CRATE}"
nit: redundant quotes, should be
cd "${BUILDDIR}/${CRATE}"
> +rm -f debian/source/format.debcargo.hint
> +${BUILDCMD}
> +
> +# needs all crates build-dependencies, which can be more than what debcargo assembles.
> +[ "x$NOTEST" = "x" ] && ${TEST_CMD}
nit: redundant parathesis around {TEST_CMD}. Can be simplified to
$TEST_CMD.
nit: why not just use [ -z ] here?
> +
> +[ "x$NOCONTROL" = "x" ] && cp debian/control "$CONTROL"
nit: also [ -z ] possible here.
[snip]
> diff --git a/pve-qemu-server-pci/src/constants.rs b/pve-qemu-server-pci/src/constants.rs
> new file mode 100644
> index 0000000..a96c519
> --- /dev/null
> +++ b/pve-qemu-server-pci/src/constants.rs
> @@ -0,0 +1,15 @@
> +//! Upper bounds for the per-guest device counts the layouts reserve slots for.
> +//!
> +//! These mirror the limits enforced by the qemu-server configuration schema.
> +
> +pub const MAX_NET_DEVICES: u8 = 32;
> +pub const MAX_SCSI_DEVICES: u8 = 31;
> +pub const MAX_VIRTIO_BLK_DEVICES: u8 = 16;
> +pub const MAX_VIRTIOFS_DEVICES: u8 = 10;
> +pub const MAX_HOSTPCI_DEVICES: u8 = 16;
To my knowledge, all these constants are already defined in
proxmox/pve-api-types
Perhaps we can import this somehow, I would like to avoid multiple
sources of truth.
> +
> +/// Number of slots on a PCI bus. Slot 0x00 is always taken by the bridge
> +/// itself, so only [`USABLE_BRIDGE_SLOTS`] of them can hold a device.
> +pub(crate) const BRIDGE_SLOT_NUM: usize = 32;
nit: naming: Maybe something like SLOTS_PER_BUS is more accurate. But current
naming here is fine.
> +pub(crate) const USABLE_BRIDGE_SLOTS: u8 = BRIDGE_SLOT_NUM as u8 - 1;
nit: naming: Same thing here, could be USEABLE_SLOTS_PER_BUS.
> +pub(crate) const FUNCTIONS_NUM: usize = 8;
nit: naming: could be FUNCTIONS_PER_SLOT.
> diff --git a/pve-qemu-server-pci/src/error.rs b/pve-qemu-server-pci/src/error.rs
> new file mode 100644
> index 0000000..6cd5abd
> --- /dev/null
> +++ b/pve-qemu-server-pci/src/error.rs
> @@ -0,0 +1,23 @@
> +use std::{error::Error, fmt::Display};
> +
> +/// Errors that can occur while looking up a PCI address.
> +#[derive(Clone, Copy, Debug, PartialEq, Eq)]
> +#[non_exhaustive]
> +pub enum PciConfigError {
nit: naming: The current list of error are more about the layout lookup,
so we could call this something like `LayoutError`.
> + /// The given configuration id is not a device this crate knows about.
> + InvalidConfigId,
> + /// The device is not part of the layout it was looked up in.
> + NoAddressFound,
> +}
> +
> +impl Display for PciConfigError {
> + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> + let text = match self {
> + PciConfigError::InvalidConfigId => "invalid configuration id",
> + PciConfigError::NoAddressFound => "no such address found",
> + };
> + f.write_str(text)
> + }
> +}
> +
> +impl Error for PciConfigError {}
> diff --git a/pve-qemu-server-pci/src/layout/legacy.rs b/pve-qemu-server-pci/src/layout/legacy.rs
> new file mode 100644
> index 0000000..40b3265
> --- /dev/null
[snip]
> +/// Looks up the PCI address of a device, see [`crate::print_pci_addr`].
> +pub(crate) fn print_pci_addr(
> + id: &str,
> + arch: ClusterResourceHostArch,
> +) -> Result<String, PciConfigError> {
> + let new_id = map_legacy_pci_id(id);
> + // aarch64 has no PCI host bridge, only a PCIe one
> + let pcie = arch == ClusterResourceHostArch::Aarch64;
> + let layout = get_legacy_pci_layout(pcie, is_virtio_scsi_single_id(new_id));
> + let addr = layout.find_device(&new_id.parse()?)?;
> + Ok(addr.to_qemu_addr())
> +}
> +
> +/// Looks up the PCIe address of a device, see [`crate::print_pcie_addr`].
> +pub(crate) fn print_pcie_addr(id: &str) -> Result<String, PciConfigError> {
> + let (new_id, win7) = map_legacy_pcie_id(id);
> + let layout = if win7 {
> + LEGACY_PCIE_LAYOUT_WIN7
> + } else {
> + LEGACY_PCIE_LAYOUT
> + };
> + let addr = layout.find_device(&new_id.parse()?)?;
> + Ok(addr.to_qemu_addr())
> +}
> +
> +/// Renders a PCIe root port device, see [`crate::print_pcie_root_port`].
> +pub(crate) fn print_pcie_root_port(index: u8) -> Result<String, PciConfigError> {
> + // root ports are numbered from one in the layout, hostpci devices from zero
> + let index = index.checked_add(1).ok_or(PciConfigError::NoAddressFound)?;
> + let res = LEGACY_PCIE_LAYOUT
> + .print_root_port(index)?
> + .replace("pve.root-port", "ich9-pcie-port-");
I feel like the Bus should just provide the correct getter here, instead
of calling replace.
> +
> + Ok(res)
> +}
> +
I don't like the naming of these functions:
- print_pcie_addr
- print_pcie_root_port
The functions return values, and should be named as such. So either i
would just drop the prefix to:
- pcie_addr
- pcie_root_port
Or describe that they are "getting" the respective value:
- get_pcie_addr
- get_pcie_root_port
Since we are also calling `to_qemu_addr`, we could include the
formatting in the name:
- format_pcie_addr
- format_pcie_root_port
(I know that they mirror the legacy naming scheme from the perl code,
but I belive we shouldnt continue with this style here).
[snip]
> diff --git a/pve-qemu-server-pci/src/types/bus.rs b/pve-qemu-server-pci/src/types/bus.rs
> new file mode 100644
> index 0000000..37ad57d
[snip]
> +pub enum Bus {
> + /// A PCI bus. Index 0 is the root bus of a non-PCIe machine.
> + Pci(u8),
> + /// A PCIe bus. Index 0 is the root bus of a PCIe machine.
> + Pcie(u8),
> + /// The bus behind a PCIe root port, which can hold a single device.
> + RootPort(u8),
> + /// The PCI bus behind the q35 DMI-to-PCI bridge.
> + Pcidmi,
> + /// A bridge for the guest's built-in devices.
> + Sys(u8),
> + /// One of the bridges PVE adds per device kind, see [`PveBridge`].
> + Pve { kind: PveBridge, index: u8 },
> +}
> +
> +type BusConstructor = fn(u8) -> Bus;
> +
> +impl Bus {
> + /// Renders the bus as the id QEMU refers to it by.
> + ///
> + /// This is the inverse of the [`std::str::FromStr`] implementation.
> + pub fn to_id(self) -> String {
> + match self {
> + Bus::Pci(i) => format!("pci.{i}"),
> + Bus::Pcie(i) => format!("pcie.{i}"),
> + Bus::RootPort(i) => format!("pve.root-port{i}"),
> + Bus::Sys(i) => format!("pve.sys{i}"),
> + Bus::Pcidmi => "pcidmi".to_string(),
> + Bus::Pve { kind, index } => {
> + format!("pve.{kind}{index}")
> + }
> + }
> + }
IMO Should have a function like this for the `to_qemu_addr` call:
pub fn to_qemu_q35_id(self) -> String {
match self {
Bus::RootPort(i) => format!("ich9-pcie-port-{i}"),
other => other.to_id(),
}
}
This way we don't have to manually re-format the returned id after
calling to_id().
> +
> + /// The QEMU device model needed to create this bus below `parent`, or
> + /// `None` if the bus is not created by a bridge device.
> + pub fn bridge_model(self, parent: Bus) -> Option<&'static str> {
> + match self {
> + Bus::Pcie(_) | Bus::RootPort(_) => None,
> + Bus::Pcidmi => Some("i82801b11-bridge"),
> + Bus::Sys(_) | Bus::Pci(_) | Bus::Pve { .. } => Some(match parent {
> + Bus::Pcie(_) | Bus::RootPort(_) => "pcie-pci-bridge",
> + _ => "pci-bridge",
> + }),
> + }
> + }
> +}
> +
[snip]
> diff --git a/pve-qemu-server-pci/src/types/device.rs b/pve-qemu-server-pci/src/types/device.rs
> new file mode 100644
> index 0000000..f63bf39
> +
[snip]
> +type DeviceConstructor = fn(u8) -> Device;
> +
> +/// Parses the configuration id of a device, for example `net0` or `hostpci3`.
> +impl FromStr for Device {
> + type Err = PciConfigError;
> +
> + fn from_str(s: &str) -> Result<Self, Self::Err> {
> + // NOTE: order matters, the longer prefixes have to come first so that
> + // 'virtio' does not shadow 'virtioscsi' and 'hostpci' not 'hostpcie'.
> + const PREFIXES: &[(&str, DeviceConstructor)] = &[
> + ("net", Device::Net),
> + ("scsihw", Device::ScsiController),
> + ("virtioscsi", Device::ScsiController),
> + ("virtiofs", Device::VirtioFs),
> + ("virtio", Device::VirtioBlk),
> + ("hostpcie", Device::Hostpcie),
AFAIK, the "hostpcie" is never used in the perl code. We always default
to "hostpci".
This might be an issue with the new v2 layout (See Patch 3 for
reference).
Will this be changed in the phase 2 patches, or am misreading
something here?
> + ("hostpci", Device::Hostpci),
> + ];
> +
> + for (prefix, c) in PREFIXES {
> + if let Some(index) = s.strip_prefix(prefix) {
> + let index = index.parse().map_err(|_| PciConfigError::InvalidConfigId)?;
> + return Ok(c(index));
> + }
> + }
> +
> + let class = match s {
> + "viommu" => Device::Viommu,
> + "vga" => Device::Vga(0),
> + "vga1" => Device::Vga(1),
> + "vga2" => Device::Vga(2),
> + "vga3" => Device::Vga(3),
> + "xhci" => Device::XhciController(0),
> + "ahci0" => Device::Ahci,
> + "balloon0" => Device::Balloon,
> + "watchdog" => Device::Watchdog,
> + "qga0" => Device::GuestAgent,
> + "spice" => Device::SpiceSerial,
> + "rng0" => Device::Rng,
> + "audio0" => Device::Audio,
> + "ivshmem" => Device::Ivshmem,
> + // legacy rules
> + "ehci" | "piix3" => Device::Piix3Controller,
> + "legacy-igd" => Device::Vga(0),
> + "pci.2-igd" => Device::Bridge(Bus::Pci(99)),
> + other => {
> + if let Ok(bridge) = other.parse() {
> + return Ok(Device::Bridge(bridge));
> + }
> +
> + return Err(PciConfigError::InvalidConfigId);
> + }
> + };
> +
> + Ok(class)
> + }
> +}
> diff --git a/pve-qemu-server-pci/src/types/layout.rs b/pve-qemu-server-pci/src/types/layout.rs
> new file mode 100644
> index 0000000..18977af
> --- /dev/null
> +++ b/pve-qemu-server-pci/src/types/layout.rs
> @@ -0,0 +1,345 @@
> +use std::collections::VecDeque;
> +
> +use crate::constants::{BRIDGE_SLOT_NUM, FUNCTIONS_NUM, USABLE_BRIDGE_SLOTS};
> +use crate::{Bus, Device, PciAddress, PciConfigError, PveBridge};
> +
> +/// What sits at one function number of a slot.
> +#[derive(Clone, Copy, Debug, PartialEq, Eq)]
> +pub enum Function<'a> {
> + /// Occupied by something outside of this crate's control, for example by
> + /// the machine type's own configuration file.
> + Reserved,
> + /// Free for future use. Kept explicit so that the surrounding entries keep
> + /// their address when something is added later.
> + Unused,
> + Device(Device),
> + /// A bridge with a statically known set of slots behind it.
> + FixedBridge(Bus, &'a [Slot<'a>; BRIDGE_SLOT_NUM]),
> + /// A PCIe root port with the single device attached to it.
> + RootPort(u8, Device),
> +}
> +
> +/// What sits at one slot of a bus.
> +#[derive(Clone, Copy, Debug, PartialEq, Eq)]
> +pub enum Slot<'a> {
nit naming: I am kind of torn about the naming here. The code has
Slot::Multi which reads like a multi-function slot, instead of a
multi-function device. I thought about re-naming it to something like
`DeviceSlot`, but this seems too verbose.
But I am unable to come up with a better name.
> + /// Occupied by something outside of this crate's control.
> + Reserved,
> + /// Free for future use, see [`Function::Unused`].
> + Unused,
> + /// A single device on function 0.
> + Single(Function<'a>),
> + /// A multifunction slot holding up to [`FUNCTIONS_NUM`] entries.
> + Multi(&'a [Function<'a>; FUNCTIONS_NUM]),
> + /// As many bridges of `kind` as are needed to hold `max` devices. The
> + /// bridges share this slot by using one function number each.
> + DynamicBridge { kind: PveBridge, max: u8 },
nit: name: IMO name it DynamicBridges (plural) since there can be
multiple.
> +}
> +
> +/// The complete slot assignment of a guest, starting at the root bus.
> +///
> +/// A layout is a static description: it lists every address the crate can
> +/// hand out, regardless of which devices a concrete guest configures.
> +#[derive(Clone, Copy, Debug, PartialEq)]
> +pub struct DeviceLayout<'a> {
IMO: Could just be called `Layout` since we are inside of the PCI crate.
Or we have other layouts in the future (.e.g, CPU, NUMA, USB, machine,
etc) then perhaps something like `PCILayout` | `PciLayout` might be
better.
> + /// Whether the root bus is PCIe. Only the root bus is affected, nested
> + /// buses stay plain PCI.
> + pub pcie: bool,
> + pub root: [Slot<'a>; BRIDGE_SLOT_NUM],
> +}
> +
> +impl DeviceLayout<'_> {
> + /// Renders the QEMU `-device` arguments for the `index`th PCIe root port
> + /// of this layout.
> + pub fn print_root_port(&self, index: u8) -> Result<String, PciConfigError> {
> + let pci_addr = self
> + .find_root_port(index)
> + .ok_or(PciConfigError::NoAddressFound)?;
> +
> + let id = Bus::RootPort(index).to_id();
print_root_port is always used in a q35 machine context, e.g. in
legacy.rs::print_pcie_root_port. So what about just calling the correct
id here? e.g.,
let id = Bus::RootPort(index).to_qemu_q35_id();
See the to_qemu_q35_id() function in the Bus implementation.
> +
> + let bus = format!("bus={}", pci_addr.bus.to_id());
> + let addr = format!("addr={:02x}.{}", pci_addr.device, pci_addr.function);
> +
> + Ok(format!(
> + "pcie-root-port,id={id},{addr},x-speed=16,x-width=32,multifunction=on,{bus},port={index},chassis={index}"
Why is `multifunction=on` hardcoded here? Is there any specific reason
or is this just to match the legacy perl code?
> + ))
> + }
> +
> + /// Looks up the address of a device in this layout.
> + ///
> + /// The first match wins, so the order of the layout is significant: a
> + /// device may legitimately appear in two places, for example a SCSI
> + /// controller that moves to its own bridge with `virtio-scsi-single`.
> + pub fn find_device(&self, device: &Device) -> Result<PciAddress, PciConfigError> {
> + self.iter()
> + .find_map(|used| (used.device == *device).then_some(used.addr))
> + .ok_or(PciConfigError::NoAddressFound)
> + }
> +
> + fn find_root_port(&self, i: u8) -> Option<PciAddress> {
> + self.iter()
> + .find_map(|used| (used.device == Device::RootPort(i)).then_some(used.addr))
> + }
> +
> + /// Iterates over every address this layout hands out.
> + ///
> + /// Bridges are yielded before the devices behind them, so the result can
> + /// be turned into a command line in order.
> + pub fn iter(&self) -> UsedSlotIterator<'_> {
> + UsedSlotIterator::new(self)
> + }
> +}
> +
> +/// A slot of a layout that holds a device, together with its address.
> +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
> +pub struct UsedSlot {
nit: name: `UsedSlot` seems like it is trying to describe a state to me.
What about something like `DevicePlacement` | `SlotAssignment`.
> + pub addr: PciAddress,
> + pub device: Device,
> + /// Whether QEMU has to be told that other functions of this slot are in
> + /// use. Only ever set on function 0.
> + pub multifunction: bool,
> +}
> +
> +/// Iterator over the used slots of a [`DeviceLayout`], see
> +/// [`DeviceLayout::iter`].
> +#[derive(Debug)]
> +pub struct UsedSlotIterator<'a> {
> + resolved_slots: VecDeque<UsedSlot>,
> + bridge_slot_stack: Vec<(Slot<'a>, PciAddress)>,
> +}
> +
> +impl<'a> UsedSlotIterator<'a> {
> + pub(crate) fn new(layout: &DeviceLayout<'a>) -> Self {
> + let bus_type = if layout.pcie {
> + Bus::Pcie(0)
> + } else {
> + Bus::Pci(0)
> + };
> + let mut stack: Vec<(Slot<'a>, PciAddress)> = Vec::new();
> + for (device, slot) in layout.root.iter().enumerate().rev() {
> + let address = PciAddress::new(bus_type, device as u8, 0);
> + stack.push((*slot, address));
> + }
> + Self {
> + bridge_slot_stack: stack,
> + resolved_slots: VecDeque::new(),
> + }
> + }
> +
> + /// Resolves one function of a slot into the queue of used slots.
> + ///
> + /// A bridge is queued before the devices behind it: fixed bridges push
> + /// their slots onto the stack, which is only drained once the queue is
> + /// empty again.
> + fn handle_function(&mut self, addr: PciAddress, function: Function<'a>, multifunction: bool) {
> + match function {
> + Function::Reserved | Function::Unused => {}
> + Function::Device(device) => {
> + self.resolved_slots.push_back(UsedSlot {
> + addr,
> + device,
> + multifunction,
> + });
> + }
> + Function::RootPort(i, device) => {
> + self.resolved_slots.push_back(UsedSlot {
> + addr,
> + device: Device::RootPort(i),
> + multifunction,
> + });
> + self.resolved_slots.push_back(UsedSlot {
> + addr: PciAddress::new(Bus::RootPort(i), 0, 0),
> + device,
> + multifunction: false,
> + });
> + }
> + Function::FixedBridge(bus_type, slots) => {
> + self.resolved_slots.push_back(UsedSlot {
> + addr,
> + device: Device::Bridge(bus_type),
> + multifunction,
> + });
> + for (device, slot) in slots.iter().enumerate().rev() {
> + let addr = PciAddress::new(bus_type, device as u8, 0);
> + self.bridge_slot_stack.push((*slot, addr));
> + }
> + }
> + }
> + }
> +}
> +
nit: Iterator Indentation is a bit too deep IMO. I recommend factoring
out the logic for each Slot type.
> +impl Iterator for UsedSlotIterator<'_> {
> + type Item = UsedSlot;
> +
> + fn next(&mut self) -> Option<Self::Item> {
> + loop {
> + if let Some(slot) = self.resolved_slots.pop_front() {
> + return Some(slot);
> + }
> + if let Some((slot, mut addr)) = self.bridge_slot_stack.pop() {
> + match slot {
> + Slot::Reserved | Slot::Unused => {}
> + Slot::Single(function) => self.handle_function(addr, function, false),
> + Slot::Multi(functions) => {
> + for (function_nr, function) in functions.iter().enumerate() {
> + addr.function = function_nr as u8;
> + self.handle_function(addr, *function, function_nr == 0);
> + }
> + }
> + Slot::DynamicBridge { kind, max } => {
> + // the bridges share one slot, so there cannot be more
> + // of them than the slot has function numbers
> + let num_bridges = max.div_ceil(USABLE_BRIDGE_SLOTS);
> + debug_assert!(usize::from(num_bridges) <= FUNCTIONS_NUM);
> +
> + for bridge_index in 0..num_bridges {
> + addr.function = bridge_index;
> + let bus = Bus::Pve {
> + kind,
> + index: bridge_index,
> + };
> + self.resolved_slots.push_back(UsedSlot {
> + addr,
> + device: Device::Bridge(bus),
> + multifunction: bridge_index == 0 && num_bridges > 1,
> + });
> + for slot_nr in 1..=USABLE_BRIDGE_SLOTS {
> + // u16 so that a bigger 'max' cannot overflow
> + let index = u16::from(bridge_index)
> + * u16::from(USABLE_BRIDGE_SLOTS)
> + + u16::from(slot_nr)
> + - 1;
> + if index >= u16::from(max) {
> + break;
> + }
> + self.resolved_slots.push_back(UsedSlot {
> + addr: PciAddress::new(bus, slot_nr, 0),
> + device: kind.device(index as u8),
> + multifunction: false,
> + });
> + }
> + }
> + }
> + }
> + }
> +
> + if self.resolved_slots.is_empty() && self.bridge_slot_stack.is_empty() {
> + return None;
> + }
> + }
> + }
> +}
> +
[snip]
> diff --git a/pve-qemu-server-pci/src/types/pci_address.rs b/pve-qemu-server-pci/src/types/pci_address.rs
> new file mode 100644
> index 0000000..5be601e
> --- /dev/null
> +++ b/pve-qemu-server-pci/src/types/pci_address.rs
> @@ -0,0 +1,36 @@
> +use crate::Bus;
> +
> +/// The complete address of a device: the bus it sits on plus the slot and
> +/// function number within that bus.
> +#[derive(Hash, Debug, Clone, Copy, PartialEq, Eq)]
> +pub struct PciAddress {
> + pub bus: Bus,
> + pub device: u8,
> + pub function: u8,
> +}
> +
> +impl PciAddress {
> + pub const fn new(bus: Bus, device: u8, function: u8) -> Self {
> + Self {
> + bus,
> + device,
> + function,
> + }
> + }
> +
> + /// Renders the address as the `,bus=...,addr=...` suffix of a QEMU
> + /// `-device` argument, including the leading comma.
> + ///
> + /// The function number is only emitted when it is not zero, matching what
> + /// the Perl implementation produces.
> + pub fn to_qemu_addr(&self) -> String {
> + let bus = self.bus.to_id().replace("pve.root-port", "ich9-pcie-port-");
Same thing here as in `print_pcie_root_port`. I feel like the call
should just be something like
let bus = self.bus.to_qemu_q35_id()
See the bus implementation as a reference
> + let function = if self.function > 0 {
> + format!(".{}", self.function)
> + } else {
> + String::new()
> + };
> +
> + format!(",bus={bus},addr=0x{:x}{function}", self.device)
> + }
> +}
[snip]
next prev parent reply other threads:[~2026-09-28 12:54 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-22 10:55 [RFC proxmox-perl-rs/qemu-server/qemu-server-rs 0/9] pci-handling rewrite (part 1) Dominik Csapak
2026-09-22 10:55 ` [PATCH pve-qemu-server-rs 1/9] add pve-qemu-server-pci crate for guest PCI address generation Dominik Csapak
2026-09-28 12:53 ` Elias Huhsovitz [this message]
2026-09-28 14:00 ` Dominik Csapak
2026-09-22 10:55 ` [PATCH pve-qemu-server-rs 2/9] pci: add machine abstraction and PCI bridge generation Dominik Csapak
2026-09-28 12:55 ` Elias Huhsovitz
2026-09-28 14:04 ` Dominik Csapak
2026-09-22 10:55 ` [PATCH pve-qemu-server-rs 3/9] pci: layout: add v2 PCI and PCIe layouts Dominik Csapak
2026-09-28 12:55 ` Elias Huhsovitz
2026-09-28 14:05 ` Dominik Csapak
2026-09-22 10:55 ` [PATCH pve-qemu-server-rs 4/9] fixup! add pve-qemu-server-pci crate for guest PCI address generation Dominik Csapak
2026-09-22 10:55 ` [PATCH proxmox-perl-rs 5/9] pve: add bindings for `pve-qemu-server-pci` crate Dominik Csapak
2026-09-28 12:56 ` Elias Huhsovitz
2026-09-28 14:06 ` Dominik Csapak
2026-09-22 10:55 ` [PATCH proxmox-perl-rs 6/9] pve: pci bindings: add bindings for the `Machine` struct Dominik Csapak
2026-09-22 10:55 ` [PATCH qemu-server 7/9] pci: use PVE::RS::PCI bindings Dominik Csapak
2026-09-22 10:55 ` [PATCH qemu-server 8/9] helpers: factor out the version parts parsing Dominik Csapak
2026-09-22 10:55 ` [PATCH qemu-server 9/9] pci: bridges: use the rust `Machine` struct to pass parameters Dominik Csapak
2026-09-22 11:06 ` [RFC proxmox-perl-rs/qemu-server/qemu-server-rs 0/9] pci-handling rewrite (part 1) 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=DLQYQH22CBUB.2URWVLBX6LGM5@proxmox.com \
--to=e.huhsovitz@proxmox.com \
--cc=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