From: Dominik Csapak <d.csapak@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH pve-qemu-server-rs v2 1/9] add pve-qemu-server-pci crate for guest PCI address generation
Date: Thu, 15 Oct 2026 10:10:23 +0200 [thread overview]
Message-ID: <20261015081041.743422-2-d.csapak@proxmox.com> (raw)
In-Reply-To: <20261015081041.743422-1-d.csapak@proxmox.com>
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 (except win7's hostpciXbus0, since that will change in perl
anyway).
Lookups take a `HardwareConfig`, which only contains the parts of the
guest config that influence the layout and its quirks, so callers can
build it from whatever config representation they have.
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>
---
.cargo/config.toml | 5 +
.gitignore | 13 +
Cargo.toml | 29 +
Makefile | 153 +++++
build.sh | 40 ++
bump.sh | 44 ++
pve-qemu-server-pci/Cargo.toml | 15 +
pve-qemu-server-pci/debian/changelog | 5 +
pve-qemu-server-pci/debian/control | 36 ++
pve-qemu-server-pci/debian/copyright | 18 +
pve-qemu-server-pci/debian/debcargo.toml | 7 +
pve-qemu-server-pci/src/constants.rs | 7 +
pve-qemu-server-pci/src/error.rs | 24 +
pve-qemu-server-pci/src/hardware_config.rs | 156 +++++
pve-qemu-server-pci/src/layout/legacy.rs | 628 +++++++++++++++++++
pve-qemu-server-pci/src/layout/mod.rs | 48 ++
pve-qemu-server-pci/src/lib.rs | 33 +
pve-qemu-server-pci/src/types/bus.rs | 198 ++++++
pve-qemu-server-pci/src/types/device.rs | 173 +++++
pve-qemu-server-pci/src/types/layout.rs | 329 ++++++++++
pve-qemu-server-pci/src/types/mod.rs | 11 +
pve-qemu-server-pci/src/types/pci_address.rs | 36 ++
rustfmt.toml | 2 +
23 files changed, 2010 insertions(+)
create mode 100644 .cargo/config.toml
create mode 100644 .gitignore
create mode 100644 Cargo.toml
create mode 100644 Makefile
create mode 100755 build.sh
create mode 100755 bump.sh
create mode 100644 pve-qemu-server-pci/Cargo.toml
create mode 100644 pve-qemu-server-pci/debian/changelog
create mode 100644 pve-qemu-server-pci/debian/control
create mode 100644 pve-qemu-server-pci/debian/copyright
create mode 100644 pve-qemu-server-pci/debian/debcargo.toml
create mode 100644 pve-qemu-server-pci/src/constants.rs
create mode 100644 pve-qemu-server-pci/src/error.rs
create mode 100644 pve-qemu-server-pci/src/hardware_config.rs
create mode 100644 pve-qemu-server-pci/src/layout/legacy.rs
create mode 100644 pve-qemu-server-pci/src/layout/mod.rs
create mode 100644 pve-qemu-server-pci/src/lib.rs
create mode 100644 pve-qemu-server-pci/src/types/bus.rs
create mode 100644 pve-qemu-server-pci/src/types/device.rs
create mode 100644 pve-qemu-server-pci/src/types/layout.rs
create mode 100644 pve-qemu-server-pci/src/types/mod.rs
create mode 100644 pve-qemu-server-pci/src/types/pci_address.rs
create mode 100644 rustfmt.toml
diff --git a/.cargo/config.toml b/.cargo/config.toml
new file mode 100644
index 0000000..3b5b6e4
--- /dev/null
+++ b/.cargo/config.toml
@@ -0,0 +1,5 @@
+[source]
+[source.debian-packages]
+directory = "/usr/share/cargo/registry"
+[source.crates-io]
+replace-with = "debian-packages"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dbdfcdc
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+/target
+/*/target
+Cargo.lock
+**/*.rs.bk
+*.buildinfo
+*.changes
+*.deb
+*.dsc
+*.tar.?z
+*.tar.zst
+/build
+/*-deb
+/*-dsc
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..71bc8e5
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,29 @@
+[workspace]
+members = [
+ "pve-qemu-server-pci"
+]
+exclude = [
+ "build",
+]
+resolver = "3"
+
+[workspace.package]
+authors = ["Proxmox Support Team <support@proxmox.com>"]
+edition = "2024"
+license = "AGPL-3"
+repository = "https://git.proxmox.com/?p=pve-qemu-server-rs.git"
+homepage = "https://proxmox.com"
+exclude = [ "debian" ]
+rust-version = "1.94"
+
+[workspace.dependencies]
+# any features enabled here are enabled on all members using 'workspace = true'!
+
+# external dependencies
+anyhow = "1.0"
+log = "0.4"
+strum = { version = "0.26", features = ["derive"] }
+
+# proxmox dependencies
+pve-api-types = "8.1"
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..f89bb73
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,153 @@
+# Shortcut for common operations:
+
+CRATES != echo pve-*/Cargo.toml | sed -e 's|/Cargo.toml||g'
+
+# By default we just run checks:
+.PHONY: all
+all: check
+
+.PHONY: deb
+deb: $(foreach c,$(CRATES), $c-deb)
+ echo $(foreach c,$(CRATES), $c-deb)
+ lintian build/*.deb
+
+.PHONY: dsc
+dsc: $(foreach c,$(CRATES), $c-dsc)
+ echo $(foreach c,$(CRATES), $c-dsc)
+ lintian build/*.dsc
+
+.PHONY: autopkgtest
+autopkgtest: $(foreach c,$(CRATES), $c-autopkgtest)
+
+.PHONY: dinstall
+dinstall:
+ $(MAKE) clean
+ $(MAKE) deb
+ sudo -k dpkg -i build/librust-*.deb
+
+%-deb:
+ ./build.sh $*
+ touch $@
+
+proxmox-oci-deb:
+ TEST_CMD="fakeroot cargo test --all-features --all-targets --release" ./build.sh proxmox-oci
+ touch $@
+
+%-dsc:
+ BUILDCMD='dpkg-buildpackage -S -us -uc -d' NOTEST=1 ./build.sh $*
+ touch $@
+
+%-autopkgtest:
+ autopkgtest build/$* build/*.deb -- null
+ touch $@
+
+.PHONY: list-packages
+list-packages:
+ @for p in $(CRATES); do \
+ echo "librust-$$p-dev"; \
+ done
+
+.PHONY: check
+check:
+ cargo test
+
+# Run the api-test server, serving the api-test/www/ subdir as 'www' dir over
+# http:
+.PHONY: apitest
+apitest:
+ cargo run -p api-test -- api-test/www/
+
+# Prints a diff between the current code and the one rustfmt would produce
+.PHONY: fmt
+fmt:
+ cargo +nightly fmt -- --check
+
+# Doc without dependencies
+.PHONY: doc
+doc:
+ cargo doc --no-deps
+
+.PHONY: clean
+clean:
+ cargo clean
+ rm -rf build/
+ rm -f -- *-deb *-dsc *-autopkgtest *.build *.buildinfo *.changes
+
+.PHONY: update
+update:
+ cargo update
+
+%-upload: %-deb
+ cd build; \
+ dcmd --deb rust-$*_*.changes \
+ | grep -v '.changes$$' \
+ | tar -cf "$@.tar" -T-; \
+ cat "$@.tar" | ssh -X repoman@repo.proxmox.com upload --product devel --dist trixie
+
+%-install:
+ rm -rf build/install/$*
+ mkdir -p build/install/$*
+ BUILDDIR=build/install/$* BUILDCMD=/usr/bin/true NOCONTROL=1 ./build.sh "$*" || true
+ version="$$(dpkg-parsechangelog -l $*/debian/changelog -SVersion | sed -e 's/-.*//')"; \
+ install -m755 -Dd "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}"; \
+ rm -rf "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}"; \
+ mv "build/install/$*/$*" \
+ "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}"; \
+ mv "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}/debian/cargo-checksum.json" \
+ "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}/.cargo-checksum.json"; \
+ rm -rf "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}/debian" \
+
+.PHONY: install
+install: $(foreach c,$(CRATES), $c-install)
+
+%-install-overlay: %-install
+ version="$$(dpkg-parsechangelog -l $*/debian/changelog -SVersion | sed -e 's/-.*//')"; \
+ setfattr -n trusted.overlay.opaque -v y \
+ "$(DESTDIR)/usr/share/cargo/registry/$*-$${version}"
+ install -m755 -Dd $(DESTDIR)/usr/lib/extension-release.d
+ echo 'ID=_any' >$(DESTDIR)/usr/lib/extension-release.d/extension-release.$*
+
+.PHONY: install-overlay
+install-overlay: $(foreach c,$(CRATES), $c-install-overlay)
+
+# To make sure a sysext *replaces* a crate, rather than "merging" with it, we
+# need to be able to set the 'trusted.overlay.opaque' xattr. Since we cannot do
+# this as a user, we utilize `fakeroot` which keeps track of this for us, and
+# turn the final directory into an 'erofs' file system image.
+#
+# The reason is that if a crate gets changed like this:
+#
+# old:
+# src/foo.rs
+# new:
+# src/foo/mod.rs
+#
+# if its /usr/share/cargo/registry/$crate-$version directory was not marked as
+# "opaque", the merged file system would end up with both
+#
+# src/foo.rs
+# src/foo/mod.rs
+#
+# together.
+#
+# See https://docs.kernel.org/filesystems/overlayfs.html
+%-sysext:
+ fakeroot $(MAKE) $*-sysext-do
+%-sysext-do:
+ rm -f extensions/$*.raw
+ rm -rf build/sysext/$*
+ rm -rf build/install/$*
+ $(MAKE) DESTDIR=build/sysext/$* $*-install-overlay
+ mkdir -p extensions
+ mkfs.erofs extensions/$*.raw build/sysext/$*
+
+sysext:
+ fakeroot $(MAKE) sysext-do
+sysext-do:
+ rm -f extensions/proxmox-workspace.raw
+ [ -n "$(NOCLEAN)" ] || rm -rf build/sysext/workspace
+ $(MAKE) DESTDIR=build/sysext/workspace $(foreach c,$(CRATES), $c-install)
+ install -m755 -Dd build/sysext/workspace/usr/lib/extension-release.d
+ echo 'ID=_any' >build/sysext/workspace/usr/lib/extension-release.d/extension-release.proxmox-workspace
+ mkdir -p extensions
+ mkfs.erofs extensions/proxmox-workspace.raw build/sysext/workspace
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..3c2293b
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,40 @@
+#!/bin/sh
+
+set -e
+
+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}"
+
+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}"
+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
+
+[ "x$NOCONTROL" = "x" ] && cp debian/control "$CONTROL"
diff --git a/bump.sh b/bump.sh
new file mode 100755
index 0000000..08ad119
--- /dev/null
+++ b/bump.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+package=$1
+
+if [[ -z "$package" ]]; then
+ echo "USAGE:"
+ echo -e "\t bump.sh <crate> [patch|minor|major|<version>]"
+ echo ""
+ echo "Defaults to bumping patch version by 1"
+ exit 0
+fi
+
+cargo_set_version="$(command -v cargo-set-version)"
+if [[ -z "$cargo_set_version" || ! -x "$cargo_set_version" ]]; then
+ echo 'bump.sh requires "cargo set-version", provided by "cargo-edit".'
+ exit 1
+fi
+
+if [[ ! -e "$package/Cargo.toml" ]]; then
+ echo "Invalid crate '$package'"
+ exit 1
+fi
+
+version=$2
+if [[ -z "$version" ]]; then
+ version="patch"
+fi
+
+case "$version" in
+ patch|minor|major)
+ bump="--bump"
+ ;;
+ *)
+ bump=
+ ;;
+esac
+
+cargo_toml="$package/Cargo.toml"
+changelog="$package/debian/changelog"
+
+cargo set-version -p "$package" $bump "$version"
+version="$(cargo metadata --format-version=1 | jq ".packages[] | select(.name == \"$package\").version" | sed -e 's/\"//g')"
+DEBFULLNAME="Proxmox Support Team" DEBEMAIL="support@proxmox.com" dch --no-conf --changelog "$changelog" --newversion "$version-1" --distribution stable
+git commit --edit -sm "bump $package to $version-1" Cargo.toml "$cargo_toml" "$changelog"
diff --git a/pve-qemu-server-pci/Cargo.toml b/pve-qemu-server-pci/Cargo.toml
new file mode 100644
index 0000000..d3f4401
--- /dev/null
+++ b/pve-qemu-server-pci/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "pve-qemu-server-pci"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+homepage.workspace = true
+exclude.workspace = true
+rust-version.workspace = true
+
+[dependencies]
+strum.workspace = true
+
+pve-api-types.workspace = true
diff --git a/pve-qemu-server-pci/debian/changelog b/pve-qemu-server-pci/debian/changelog
new file mode 100644
index 0000000..54847b9
--- /dev/null
+++ b/pve-qemu-server-pci/debian/changelog
@@ -0,0 +1,5 @@
+rust-pve-qemu-server-pci (0.1.0) trixie; urgency=medium
+
+ * initial release
+
+ -- Proxmox Support Team <support@proxmox.com> Tue, 01 Sep 2026 14:02:25 +0200
diff --git a/pve-qemu-server-pci/debian/control b/pve-qemu-server-pci/debian/control
new file mode 100644
index 0000000..9edd0b9
--- /dev/null
+++ b/pve-qemu-server-pci/debian/control
@@ -0,0 +1,36 @@
+Source: rust-pve-qemu-server-pci
+Section: rust
+Priority: optional
+Build-Depends: debhelper-compat (= 13),
+ dh-sequence-cargo
+Build-Depends-Arch: cargo:native <!nocheck>,
+ rustc:native (>= 1.94) <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-pve-api-types-8+default-dev (>= 8.1-~~) <!nocheck>,
+ librust-strum-0.26+default-dev <!nocheck>,
+ librust-strum-0.26+derive-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.7.2
+Vcs-Git: git://git.proxmox.com/git/pve-qemu-server-rs.git
+Vcs-Browser: https://git.proxmox.com/?p=pve-qemu-server-rs.git
+Homepage: https://proxmox.com
+X-Cargo-Crate: pve-qemu-server-pci
+
+Package: librust-pve-qemu-server-pci-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-pve-api-types-8+default-dev (>= 8.1-~~),
+ librust-strum-0.26+default-dev,
+ librust-strum-0.26+derive-dev
+Provides:
+ librust-pve-qemu-server-pci+default-dev (= ${binary:Version}),
+ librust-pve-qemu-server-pci-0-dev (= ${binary:Version}),
+ librust-pve-qemu-server-pci-0+default-dev (= ${binary:Version}),
+ librust-pve-qemu-server-pci-0.1-dev (= ${binary:Version}),
+ librust-pve-qemu-server-pci-0.1+default-dev (= ${binary:Version}),
+ librust-pve-qemu-server-pci-0.1.0-dev (= ${binary:Version}),
+ librust-pve-qemu-server-pci-0.1.0+default-dev (= ${binary:Version})
+Description: Rust crate "pve-qemu-server-pci" - Rust source code
+ Source code for Debianized Rust crate "pve-qemu-server-pci"
diff --git a/pve-qemu-server-pci/debian/copyright b/pve-qemu-server-pci/debian/copyright
new file mode 100644
index 0000000..01138fa
--- /dev/null
+++ b/pve-qemu-server-pci/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2026 Proxmox Server Solutions GmbH <support@proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/pve-qemu-server-pci/debian/debcargo.toml b/pve-qemu-server-pci/debian/debcargo.toml
new file mode 100644
index 0000000..1d52b16
--- /dev/null
+++ b/pve-qemu-server-pci/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+vcs_git = "git://git.proxmox.com/git/pve-qemu-server-rs.git"
+vcs_browser = "https://git.proxmox.com/?p=pve-qemu-server-rs.git"
diff --git a/pve-qemu-server-pci/src/constants.rs b/pve-qemu-server-pci/src/constants.rs
new file mode 100644
index 0000000..ea6db21
--- /dev/null
+++ b/pve-qemu-server-pci/src/constants.rs
@@ -0,0 +1,7 @@
+//! Limits the layouts are built around.
+
+/// Number of slots on a PCI bus. Slot 0x00 is always taken by the bridge
+/// itself, so only [`USABLE_SLOTS_PER_BUS`] of them can hold a device.
+pub(crate) const SLOTS_PER_BUS: usize = 32;
+pub(crate) const USABLE_SLOTS_PER_BUS: u8 = SLOTS_PER_BUS as u8 - 1;
+pub(crate) const FUNCTIONS_PER_DEVICE: usize = 8;
diff --git a/pve-qemu-server-pci/src/error.rs b/pve-qemu-server-pci/src/error.rs
new file mode 100644
index 0000000..5c4f7de
--- /dev/null
+++ b/pve-qemu-server-pci/src/error.rs
@@ -0,0 +1,24 @@
+use std::{error::Error, fmt::Display};
+
+use crate::Device;
+
+/// Errors that can occur while looking up a PCI address.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum 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(Device),
+}
+
+impl Display for LayoutError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ LayoutError::InvalidConfigId => write!(f, "invalid configuration id"),
+ LayoutError::NoAddressFound(device) => write!(f, "address '{device}' not found"),
+ }
+ }
+}
+
+impl Error for LayoutError {}
diff --git a/pve-qemu-server-pci/src/hardware_config.rs b/pve-qemu-server-pci/src/hardware_config.rs
new file mode 100644
index 0000000..a348353
--- /dev/null
+++ b/pve-qemu-server-pci/src/hardware_config.rs
@@ -0,0 +1,156 @@
+use std::sync::Arc;
+use std::sync::atomic::AtomicU16;
+
+use pve_api_types::{ClusterResourceHostArch as Arch, QemuConfigOstype, QemuConfigScsihw};
+
+#[derive(Default, Clone, Debug, PartialEq)]
+pub enum Mainboard {
+ #[default]
+ I440fx,
+ Q35,
+ Virt,
+}
+
+impl std::fmt::Display for Mainboard {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let text = match self {
+ Mainboard::I440fx => "i440fx",
+ Mainboard::Q35 => "q35",
+ Mainboard::Virt => "virt",
+ };
+ f.write_str(text)
+ }
+}
+
+impl Mainboard {
+ pub fn has_pcie(&self) -> bool {
+ match self {
+ Mainboard::I440fx => false,
+ Mainboard::Q35 | Mainboard::Virt => true,
+ }
+ }
+}
+
+/// The parts of a guest configuration that influence which PCI layout is used
+/// and which quirks have to be applied to it.
+///
+/// This is deliberately not the full guest config: it only carries what the
+/// address generation needs, so that callers can build it from whatever
+/// configuration representation they have.
+#[derive(Default, Clone, Debug)]
+pub struct HardwareConfig {
+ pub arch: Arch,
+ pub mainboard: Mainboard,
+ pub scsihw: Option<QemuConfigScsihw>,
+ /// Highest `scsihw` controller index in use. Controllers from index 2 on
+ /// live on `pci.4`, so this decides whether that bridge is needed.
+ pub max_scsihw: u8,
+ pub legacy_igd: bool,
+ pub ostype: Option<QemuConfigOstype>,
+ /// QEMU machine version as `(major, minor, pve)`, where the PVE specific
+ /// revision is optional.
+ pub machine_version: (u16, u16, Option<u16>),
+ /// The recorded required `+pveX` version
+ pub required_pve_version: Arc<AtomicU16>,
+}
+
+impl HardwareConfig {
+ /// Builds a [`HardwareConfig`] from the raw configuration values.
+ ///
+ /// Unparsable `arch`, `scsihw` and `ostype` values are not an error here:
+ /// `arch` falls back to x86_64 and the other two to `None`, which is the
+ /// same defaulting the Perl implementation does.
+ #[allow(clippy::too_many_arguments)]
+ pub fn new(
+ arch: &str,
+ machine_type: String,
+ scsihw: Option<String>,
+ max_scsihw: u8,
+ legacy_igd: bool,
+ ostype: Option<String>,
+ major: u16,
+ minor: u16,
+ pve: Option<u16>,
+ ) -> Self {
+ let arch = match arch.parse().unwrap_or(Arch::X8664) {
+ Arch::UnknownEnumValue(_) => Arch::X8664,
+ other => other,
+ };
+ let scsihw = scsihw.and_then(|hw| hw.parse().ok());
+ let ostype = ostype.and_then(|ostype| ostype.parse().ok());
+
+ let mainboard = match arch {
+ Arch::Aarch64 => Mainboard::Virt,
+ Arch::X8664 if machine_type.contains("q35") => Mainboard::Q35,
+ Arch::X8664 | Arch::UnknownEnumValue(_) => Mainboard::I440fx,
+ };
+ Self {
+ arch,
+ mainboard,
+ scsihw,
+ max_scsihw,
+ legacy_igd,
+ ostype,
+ machine_version: (major, minor, pve),
+ required_pve_version: Arc::new(AtomicU16::new(0)),
+ }
+ }
+
+ /// Whether the guest's root bus is PCIe. True for q35 machines and for
+ /// aarch64, which only has a PCIe host bridge.
+ pub fn has_pcie(&self) -> bool {
+ self.mainboard.has_pcie()
+ }
+
+ /// Returns if the mainboard is a Q35 variant.
+ pub fn is_q35(&self) -> bool {
+ self.mainboard == Mainboard::Q35
+ }
+
+ /// Whether each SCSI disk gets its own controller, which needs the extra
+ /// `pci.3` bridge.
+ pub fn virtio_scsi_single(&self) -> bool {
+ self.scsihw == Some(QemuConfigScsihw::VirtioScsiSingle)
+ }
+
+ /// Whether the machine version is at least `major.minor.pve`.
+ ///
+ /// A machine without a PVE revision counts as '+pve0'.
+ pub fn version_at_least(&self, major: u16, minor: u16, pve: Option<u16>) -> bool {
+ match (self.machine_version.0, self.machine_version.1).cmp(&(major, minor)) {
+ std::cmp::Ordering::Less => false,
+ std::cmp::Ordering::Greater => true,
+ std::cmp::Ordering::Equal => {
+ let ours = self.machine_version.2.unwrap_or(0);
+ let pve = pve.unwrap_or(0);
+ let ok = ours >= pve;
+ if ok && pve > 0 {
+ self.required_pve_version
+ .fetch_max(pve, std::sync::atomic::Ordering::SeqCst);
+ }
+ ok
+ }
+ }
+ }
+
+ /// 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`.
+ pub fn windows_version(&self) -> u8 {
+ match self.ostype.unwrap_or(QemuConfigOstype::Other) {
+ QemuConfigOstype::Wxp | QemuConfigOstype::W2k | QemuConfigOstype::W2k3 => 5,
+ QemuConfigOstype::W2k8 | QemuConfigOstype::Wvista => 6,
+ QemuConfigOstype::Win7 => 7,
+ QemuConfigOstype::Win8 => 8,
+ QemuConfigOstype::Win10 => 10,
+ QemuConfigOstype::Win11 => 11,
+ _ => 0,
+ }
+ }
+
+ /// Returns the recorded required `+pveX` version.
+ pub fn required_pve_version(&self) -> u16 {
+ self.required_pve_version
+ .load(std::sync::atomic::Ordering::Relaxed)
+ }
+}
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..e6693ca
--- /dev/null
+++ b/pve-qemu-server-pci/src/layout/legacy.rs
@@ -0,0 +1,628 @@
+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};
+
+const PCI_1: [Slot; SLOTS_PER_BUS] = bridge_slots(&[
+ single_device!(Net(6)),
+ single_device!(Net(7)),
+ single_device!(Net(8)),
+ single_device!(Net(9)),
+ single_device!(Net(10)),
+ single_device!(Net(11)),
+ single_device!(Net(12)),
+ single_device!(Net(13)),
+ single_device!(Net(14)),
+ single_device!(Net(15)),
+ single_device!(Net(16)),
+ single_device!(Net(17)),
+ single_device!(Net(18)),
+ single_device!(Net(19)),
+ single_device!(Net(20)),
+ single_device!(Net(21)),
+ single_device!(Net(22)),
+ single_device!(Net(23)),
+ single_device!(Net(24)),
+ single_device!(Net(25)),
+ single_device!(Net(26)),
+ single_device!(Net(27)),
+ single_device!(Net(28)),
+ single_device!(Net(29)),
+ single_device!(Net(30)),
+ single_device!(Net(31)),
+ single_device!(XhciController(0)),
+ Slot::Single(Function::FixedBridge(Bus::Pci(4), &PCI_4)),
+ single_device!(Rng),
+ single_device!(Bridge(Bus::Pci(99))), // same as pci.2 but for igd
+]);
+
+const PCI_2: [Slot; SLOTS_PER_BUS] = bridge_slots(&[
+ single_device!(VirtioBlk(6)),
+ single_device!(VirtioBlk(7)),
+ single_device!(VirtioBlk(8)),
+ single_device!(VirtioBlk(9)),
+ single_device!(VirtioBlk(10)),
+ single_device!(VirtioBlk(11)),
+ single_device!(VirtioBlk(12)),
+ single_device!(VirtioBlk(13)),
+ single_device!(VirtioBlk(14)),
+ single_device!(VirtioBlk(15)),
+ single_device!(Ivshmem),
+ single_device!(Audio),
+ single_device!(Hostpci(4)),
+ single_device!(Hostpci(5)),
+ single_device!(Hostpci(6)),
+ single_device!(Hostpci(7)),
+ single_device!(Hostpci(8)),
+ single_device!(Hostpci(9)),
+ single_device!(Hostpci(10)),
+ single_device!(Hostpci(11)),
+ single_device!(Hostpci(12)),
+ single_device!(Hostpci(13)),
+ single_device!(Hostpci(14)),
+ single_device!(Hostpci(15)),
+]);
+
+const PCI_3: [Slot; SLOTS_PER_BUS] = bridge_slots(&[
+ single_device!(ScsiController(0)),
+ single_device!(ScsiController(1)),
+ single_device!(ScsiController(2)),
+ single_device!(ScsiController(3)),
+ single_device!(ScsiController(4)),
+ single_device!(ScsiController(5)),
+ single_device!(ScsiController(6)),
+ single_device!(ScsiController(7)),
+ single_device!(ScsiController(8)),
+ single_device!(ScsiController(9)),
+ single_device!(ScsiController(10)),
+ single_device!(ScsiController(11)),
+ single_device!(ScsiController(12)),
+ single_device!(ScsiController(13)),
+ single_device!(ScsiController(14)),
+ single_device!(ScsiController(15)),
+ single_device!(ScsiController(16)),
+ single_device!(ScsiController(17)),
+ single_device!(ScsiController(18)),
+ single_device!(ScsiController(19)),
+ single_device!(ScsiController(20)),
+ single_device!(ScsiController(21)),
+ single_device!(ScsiController(22)),
+ single_device!(ScsiController(23)),
+ single_device!(ScsiController(24)),
+ single_device!(ScsiController(25)),
+ single_device!(ScsiController(26)),
+ single_device!(ScsiController(27)),
+ single_device!(ScsiController(28)),
+ single_device!(ScsiController(29)),
+ single_device!(ScsiController(30)),
+]);
+
+const PCI_4: [Slot; SLOTS_PER_BUS] = bridge_slots(&[
+ single_device!(ScsiController(2)),
+ single_device!(ScsiController(3)),
+ single_device!(ScsiController(4)),
+]);
+
+/// Builds the PCI layout of a guest.
+///
+/// With `virtio_scsi_single` the SCSI controllers move off the root bus onto
+/// their own `pci.3` bridge, which is why this cannot be a constant.
+pub(crate) fn get_legacy_pci_layout<'a>(pcie: bool, virtio_scsi_single: bool) -> Layout<'a> {
+ Layout {
+ pcie,
+ root: [
+ Slot::Reserved,
+ single_device!(Piix3Controller), // actually on 0x01.2 but the function number is added by QemuServer/USB.pm
+ single_device!(Vga(0)),
+ single_device!(Balloon),
+ single_device!(Watchdog),
+ if virtio_scsi_single {
+ Slot::Single(Function::FixedBridge(Bus::Pci(3), &PCI_3))
+ } else {
+ single_device!(ScsiController(0))
+ },
+ single_device!(ScsiController(1)),
+ single_device!(Ahci),
+ single_device!(GuestAgent),
+ single_device!(SpiceSerial),
+ single_device!(VirtioBlk(0)),
+ single_device!(VirtioBlk(1)),
+ single_device!(VirtioBlk(2)),
+ single_device!(VirtioBlk(3)),
+ single_device!(VirtioBlk(4)),
+ single_device!(VirtioBlk(5)),
+ single_device!(Hostpci(0)),
+ single_device!(Hostpci(1)),
+ single_device!(Net(0)),
+ single_device!(Net(1)),
+ single_device!(Net(2)),
+ single_device!(Net(3)),
+ single_device!(Net(4)),
+ single_device!(Net(5)),
+ single_device!(Vga(1)),
+ single_device!(Vga(2)),
+ single_device!(Vga(3)),
+ single_device!(Hostpci(2)),
+ single_device!(Hostpci(3)),
+ Slot::Reserved, // usb-host (pve-usb.cfg)
+ Slot::Single(Function::FixedBridge(Bus::Pci(1), &PCI_1)),
+ Slot::Single(Function::FixedBridge(Bus::Pci(2), &PCI_2)),
+ ],
+ }
+}
+
+/// The PCIe layout of a q35 guest.
+///
+/// Only contains the devices on the PCIe bus itself, not the ones nested on
+/// the PCI bridges below it.
+#[expect(deprecated)]
+pub(crate) const LEGACY_PCIE_LAYOUT: Layout = Layout {
+ pcie: true,
+ root: [
+ Slot::Reserved,
+ single_device!(Vga(0)),
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Multi(&[
+ Function::RootPort(Bus::LegacyRootPort(5), Device::Hostpcie(4)),
+ Function::RootPort(Bus::LegacyRootPort(6), Device::Hostpcie(5)),
+ Function::RootPort(Bus::LegacyRootPort(7), Device::Hostpcie(6)),
+ Function::RootPort(Bus::LegacyRootPort(8), Device::Hostpcie(7)),
+ Function::RootPort(Bus::LegacyRootPort(9), Device::Hostpcie(8)),
+ Function::RootPort(Bus::LegacyRootPort(10), Device::Hostpcie(9)),
+ Function::RootPort(Bus::LegacyRootPort(11), Device::Hostpcie(10)),
+ Function::RootPort(Bus::LegacyRootPort(12), Device::Hostpcie(11)),
+ ]),
+ Slot::Multi(&[
+ Function::RootPort(Bus::LegacyRootPort(13), Device::Hostpcie(12)),
+ Function::RootPort(Bus::LegacyRootPort(14), Device::Hostpcie(13)),
+ Function::RootPort(Bus::LegacyRootPort(15), Device::Hostpcie(14)),
+ Function::RootPort(Bus::LegacyRootPort(16), Device::Hostpcie(15)),
+ Function::Unused,
+ Function::Unused,
+ Function::Unused,
+ Function::Unused,
+ ]),
+ Slot::Unused,
+ Slot::Unused,
+ single_device!(Ivshmem),
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Multi(&[
+ Function::Device(Device::Ich9Uhci(4)),
+ Function::Device(Device::Ich9Uhci(5)),
+ Function::Device(Device::Ich9Uhci(6)),
+ Function::Unused,
+ Function::Unused,
+ Function::Unused,
+ Function::Unused,
+ Function::Device(Device::Ich9Ehci(2)),
+ ]),
+ single_device!(Ich9Audio),
+ Slot::Multi(&[
+ Function::RootPort(Bus::LegacyRootPort(1), Device::Hostpcie(0)),
+ Function::RootPort(Bus::LegacyRootPort(2), Device::Hostpcie(1)),
+ Function::RootPort(Bus::LegacyRootPort(3), Device::Hostpcie(2)),
+ Function::RootPort(Bus::LegacyRootPort(4), Device::Hostpcie(3)),
+ Function::Unused,
+ Function::Unused,
+ Function::Unused,
+ Function::Unused,
+ ]),
+ Slot::Multi(&[
+ Function::Device(Device::Ich9Uhci(1)),
+ Function::Device(Device::Ich9Uhci(2)),
+ Function::Device(Device::Ich9Uhci(3)),
+ Function::Unused,
+ Function::Unused,
+ Function::Unused,
+ Function::Unused,
+ Function::Device(Device::Ich9Ehci(1)),
+ ]),
+ Slot::Single(Function::FixedBridge(
+ Bus::Pcidmi,
+ &bridge_slots(&[
+ single_device!(Bridge(Bus::Pci(0))),
+ single_device!(Bridge(Bus::Pci(1))),
+ single_device!(Bridge(Bus::Pci(2))),
+ single_device!(Bridge(Bus::Pci(3))),
+ ]),
+ )),
+ Slot::Reserved, // ICH9 LPC, SATA and SMBus controllers
+ ],
+};
+
+/// The PCIe layout for Windows 7 guests, whose driver cannot cope with
+/// passed through devices behind root ports.
+///
+/// Only contains the `hostpci` slots, everything else is handled by
+/// [`LEGACY_PCIE_LAYOUT`].
+pub(crate) const LEGACY_PCIE_LAYOUT_WIN7: Layout = Layout {
+ pcie: true,
+ root: [
+ Slot::Reserved,
+ Slot::Reserved,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ single_device!(Hostpcie(4)),
+ single_device!(Hostpcie(5)),
+ single_device!(Hostpcie(6)),
+ single_device!(Hostpcie(7)),
+ single_device!(Hostpcie(8)),
+ single_device!(Hostpcie(9)),
+ single_device!(Hostpcie(10)),
+ single_device!(Hostpcie(0)),
+ single_device!(Hostpcie(1)),
+ single_device!(Hostpcie(2)),
+ single_device!(Hostpcie(3)),
+ Slot::Reserved,
+ single_device!(Hostpcie(11)),
+ single_device!(Hostpcie(12)),
+ single_device!(Hostpcie(13)),
+ single_device!(Hostpcie(14)),
+ single_device!(Hostpcie(15)),
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Reserved,
+ Slot::Unused,
+ Slot::Unused,
+ Slot::Unused,
+ ],
+};
+
+/// Whether a (mapped) id asks for the `virtio-scsi-single` variant of the
+/// layout, where every SCSI disk gets its own controller on `pci.3`.
+fn is_virtio_scsi_single_id(id: &str) -> bool {
+ id.starts_with("virtioscsi")
+}
+
+/// Maps (conflicting) legacy PCI IDs to ones we can represent here.
+fn map_legacy_pci_id(original: &str) -> &str {
+ // we can't request scsihwX and virtioscsiX at the same time,
+ // and that's the only thing living on 'pci.3'
+ if original == "pci.3" {
+ return "scsihw0";
+ }
+
+ // the legacy-igd device must be the same as the primary adapter
+ // and requires vga=none
+ if original == "legacy-igd" {
+ return "vga";
+ }
+
+ // when using legacy-igd, pci.2 is on a different address due to
+ // driver constraints
+ if original == "pci.2-igd" {
+ return "pci.99";
+ }
+
+ original
+}
+
+/// 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> {
+ let new_id = map_legacy_pci_id(id);
+ // the root bus is never a bridge we have to add ourselves, so it does not
+ // matter whether it is PCI or PCIe here
+ let layout = get_legacy_pci_layout(false, is_virtio_scsi_single_id(new_id));
+ let device = new_id.parse().ok()?;
+ let addr = layout.find_device(&device).ok()?;
+ Some(addr.bus)
+}
+
+/// Returns if the caller wants a PCIE address instead of a PCI one based on the id.
+fn is_pcie_id(id: &str) -> bool {
+ id.starts_with("hostpcie") || id == "vga" || id == "ivshmem"
+}
+
+pub(crate) fn address(hw: &HardwareConfig, id: &str) -> Result<PciAddress, LayoutError> {
+ let (layout, new_id) = match hw.mainboard {
+ Mainboard::Q35 if is_pcie_id(id) => {
+ let layout = if hw.windows_version() == 7 && id.starts_with("hostpcie") {
+ LEGACY_PCIE_LAYOUT_WIN7
+ } else {
+ LEGACY_PCIE_LAYOUT
+ };
+ (layout, id)
+ }
+ Mainboard::Q35 | Mainboard::Virt | Mainboard::I440fx => {
+ let new_id = map_legacy_pci_id(id);
+ let layout = get_legacy_pci_layout(
+ hw.mainboard == Mainboard::Virt,
+ is_virtio_scsi_single_id(id),
+ );
+ (layout, new_id)
+ }
+ };
+
+ layout.find_device(&new_id.parse()?)
+}
+
+#[cfg(test)]
+pub(crate) mod test {
+ use std::collections::HashSet;
+
+ use super::{LEGACY_PCIE_LAYOUT, get_legacy_pci_layout, pci_bridge_for_device};
+ use crate::hardware_config::Mainboard;
+ use crate::layout::legacy::is_pcie_id;
+ use crate::{Bus, Device, HardwareConfig, Layout, PciAddress};
+
+ pub const LEGACY_ADDRS: [(&str, u8, u8); 121] = [
+ ("ahci0", 0, 7),
+ ("audio0", 2, 12),
+ ("balloon0", 0, 3),
+ ("ehci", 0, 1),
+ ("hostpci0", 0, 16),
+ ("hostpci1", 0, 17),
+ ("hostpci2", 0, 27),
+ ("hostpci3", 0, 28),
+ ("hostpci4", 2, 13),
+ ("hostpci5", 2, 14),
+ ("hostpci6", 2, 15),
+ ("hostpci7", 2, 16),
+ ("hostpci8", 2, 17),
+ ("hostpci9", 2, 18),
+ ("hostpci10", 2, 19),
+ ("hostpci11", 2, 20),
+ ("hostpci12", 2, 21),
+ ("hostpci13", 2, 22),
+ ("hostpci14", 2, 23),
+ ("hostpci15", 2, 24),
+ ("ivshmem", 2, 11),
+ ("legacy-igd", 0, 2),
+ ("net0", 0, 18),
+ ("net1", 0, 19),
+ ("net2", 0, 20),
+ ("net3", 0, 21),
+ ("net4", 0, 22),
+ ("net5", 0, 23),
+ ("net6", 1, 1),
+ ("net7", 1, 2),
+ ("net8", 1, 3),
+ ("net9", 1, 4),
+ ("net10", 1, 5),
+ ("net11", 1, 6),
+ ("net12", 1, 7),
+ ("net13", 1, 8),
+ ("net14", 1, 9),
+ ("net15", 1, 10),
+ ("net16", 1, 11),
+ ("net17", 1, 12),
+ ("net18", 1, 13),
+ ("net19", 1, 14),
+ ("net20", 1, 15),
+ ("net21", 1, 16),
+ ("net22", 1, 17),
+ ("net23", 1, 18),
+ ("net24", 1, 19),
+ ("net25", 1, 20),
+ ("net26", 1, 21),
+ ("net27", 1, 22),
+ ("net28", 1, 23),
+ ("net29", 1, 24),
+ ("net30", 1, 25),
+ ("net31", 1, 26),
+ ("pci.1", 0, 30),
+ ("pci.2", 0, 31),
+ ("pci.2-igd", 1, 30),
+ ("pci.3", 0, 5),
+ ("pci.4", 1, 28),
+ ("piix3", 0, 1),
+ ("qga0", 0, 8),
+ ("rng0", 1, 29),
+ ("scsihw0", 0, 5),
+ ("scsihw1", 0, 6),
+ ("scsihw2", 4, 1),
+ ("scsihw3", 4, 2),
+ ("scsihw4", 4, 3),
+ ("spice", 0, 9),
+ ("vga", 0, 2),
+ ("vga1", 0, 24),
+ ("vga2", 0, 25),
+ ("vga3", 0, 26),
+ ("virtio0", 0, 10),
+ ("virtio1", 0, 11),
+ ("virtio2", 0, 12),
+ ("virtio3", 0, 13),
+ ("virtio4", 0, 14),
+ ("virtio5", 0, 15),
+ ("virtio6", 2, 1),
+ ("virtio7", 2, 2),
+ ("virtio8", 2, 3),
+ ("virtio9", 2, 4),
+ ("virtio10", 2, 5),
+ ("virtio11", 2, 6),
+ ("virtio12", 2, 7),
+ ("virtio13", 2, 8),
+ ("virtio14", 2, 9),
+ ("virtio15", 2, 10),
+ ("virtioscsi0", 3, 1),
+ ("virtioscsi1", 3, 2),
+ ("virtioscsi2", 3, 3),
+ ("virtioscsi3", 3, 4),
+ ("virtioscsi4", 3, 5),
+ ("virtioscsi5", 3, 6),
+ ("virtioscsi6", 3, 7),
+ ("virtioscsi7", 3, 8),
+ ("virtioscsi8", 3, 9),
+ ("virtioscsi9", 3, 10),
+ ("virtioscsi10", 3, 11),
+ ("virtioscsi11", 3, 12),
+ ("virtioscsi12", 3, 13),
+ ("virtioscsi13", 3, 14),
+ ("virtioscsi14", 3, 15),
+ ("virtioscsi15", 3, 16),
+ ("virtioscsi16", 3, 17),
+ ("virtioscsi17", 3, 18),
+ ("virtioscsi18", 3, 19),
+ ("virtioscsi19", 3, 20),
+ ("virtioscsi20", 3, 21),
+ ("virtioscsi21", 3, 22),
+ ("virtioscsi22", 3, 23),
+ ("virtioscsi23", 3, 24),
+ ("virtioscsi24", 3, 25),
+ ("virtioscsi25", 3, 26),
+ ("virtioscsi26", 3, 27),
+ ("virtioscsi27", 3, 28),
+ ("virtioscsi28", 3, 29),
+ ("virtioscsi29", 3, 30),
+ ("virtioscsi30", 3, 31),
+ ("watchdog", 0, 4),
+ ("xhci", 1, 27),
+ ];
+
+ fn pci_addr(pcie: bool, bus: u8, device: u8) -> PciAddress {
+ PciAddress {
+ bus: if pcie { Bus::Pcie(bus) } else { Bus::Pci(bus) },
+ device,
+ function: 0,
+ }
+ }
+
+ #[test]
+ /// Test string id to addr, big overlap with config2command tests in qemu-server
+ fn test_pci_addr_maps() {
+ // copied and sorted from src/PVE/QemuServer/PCI.pm
+
+ let i440fx = HardwareConfig::default();
+ let q35 = HardwareConfig {
+ mainboard: Mainboard::Q35,
+ ..Default::default()
+ };
+ let arm = HardwareConfig {
+ arch: pve_api_types::ClusterResourceHostArch::Aarch64,
+ mainboard: Mainboard::Virt,
+ ..Default::default()
+ };
+ for (input, bus, device) in LEGACY_ADDRS {
+ let addr =
+ crate::address(&i440fx, input).unwrap_or_else(|_| panic!("{input} not found"));
+ let expected = pci_addr(false, bus, device);
+ assert_eq!(addr, expected, "tested {input} on i440fx");
+
+ if !is_pcie_id(input) {
+ let addr =
+ crate::address(&q35, input).unwrap_or_else(|_| panic!("{input} not found"));
+ let expected = pci_addr(false, bus, device);
+ assert_eq!(addr, expected, "tested {input} on q35");
+ }
+
+ // arm is on pcie
+ let addr = crate::address(&arm, input).unwrap_or_else(|_| panic!("{input} not found"));
+ let expected = pci_addr(bus == 0, bus, device);
+ assert_eq!(addr, expected, "tested {input} on virt");
+ }
+ }
+
+ #[test]
+ /// Test string id to addr, big overlap with config2command tests in qemu-server
+ fn test_pcie_addr_maps() {
+ // copied and sorted from src/PVE/QemuServer/PCI.pm
+ let tests = [
+ ("hostpcie0", true, 1),
+ ("hostpcie1", true, 2),
+ ("hostpcie2", true, 3),
+ ("hostpcie3", true, 4),
+ ("hostpcie4", true, 5),
+ ("hostpcie5", true, 6),
+ ("hostpcie6", true, 7),
+ ("hostpcie7", true, 8),
+ ("hostpcie8", true, 9),
+ ("hostpcie9", true, 10),
+ ("hostpcie10", true, 11),
+ ("hostpcie11", true, 12),
+ ("hostpcie12", true, 13),
+ ("hostpcie13", true, 14),
+ ("hostpcie14", true, 15),
+ ("hostpcie15", true, 16),
+ ("ivshmem", false, 20),
+ ("vga", false, 1),
+ ];
+
+ let pcie_machine = HardwareConfig {
+ mainboard: Mainboard::Q35,
+ ..Default::default()
+ };
+ for (input, rp, num) in tests {
+ let addr = crate::address(&pcie_machine, input)
+ .unwrap_or_else(|_| panic!("device not found: {input}"));
+ let (bus, device) = if rp {
+ #[expect(deprecated)]
+ (Bus::LegacyRootPort(num), 0)
+ } else {
+ (Bus::Pcie(0), num)
+ };
+ let expected = PciAddress::new(bus, device, 0);
+ assert_eq!(addr, expected, "tested {input}");
+ }
+ }
+
+ /// 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.
+ fn assert_buses_before_devices(layout: &Layout) {
+ let mut buses = HashSet::from([if layout.pcie {
+ Bus::Pcie(0)
+ } else {
+ Bus::Pci(0)
+ }]);
+
+ for used in layout.iter() {
+ assert!(
+ buses.contains(&used.addr.bus),
+ "{:?} is on {:?}, which was not created yet",
+ used.device,
+ used.addr.bus
+ );
+ match used.device {
+ Device::Bridge(bus) => buses.insert(bus),
+ Device::RootPort(bus) => buses.insert(bus),
+ _ => continue,
+ };
+ }
+ }
+
+ #[test]
+ fn test_bus_order() {
+ assert_buses_before_devices(&LEGACY_PCIE_LAYOUT);
+ assert_buses_before_devices(&get_legacy_pci_layout(false, false));
+ assert_buses_before_devices(&get_legacy_pci_layout(false, true));
+ assert_buses_before_devices(&get_legacy_pci_layout(true, true));
+ }
+
+ #[test]
+ /// The bus of a device has to agree with the address it is given. Note
+ /// that the root bus is reported as bus zero, not as no bus at all.
+ fn test_pci_bridge_for_device() {
+ for (input, bus, _) in LEGACY_ADDRS {
+ assert_eq!(
+ pci_bridge_for_device(input),
+ Some(Bus::Pci(bus)),
+ "for {input}"
+ );
+ }
+
+ assert_eq!(pci_bridge_for_device("no-such-device"), None);
+ }
+}
diff --git a/pve-qemu-server-pci/src/layout/mod.rs b/pve-qemu-server-pci/src/layout/mod.rs
new file mode 100644
index 0000000..5439c17
--- /dev/null
+++ b/pve-qemu-server-pci/src/layout/mod.rs
@@ -0,0 +1,48 @@
+use crate::Slot;
+use crate::constants::SLOTS_PER_BUS;
+
+// local helper macro to make the lines shorter
+macro_rules! single_device {
+ ($i:ident ( $e:expr )) => {
+ $crate::Slot::Single($crate::Function::Device($crate::Device::$i($e)))
+ };
+ ($i:ident) => {
+ $crate::Slot::Single($crate::Function::Device($crate::Device::$i))
+ };
+}
+
+pub(crate) mod legacy;
+
+/// Constructs a correctly sized list of bridge slots, starting at address 0x01
+/// because 0x00 is always taken by the bridge itself.
+///
+/// Panics if more slots than fit are passed in. As this is a `const fn` used
+/// from constants only, that turns into a compile time error.
+pub(crate) const fn bridge_slots<'a>(slots: &'a [Slot<'a>]) -> [Slot<'a>; SLOTS_PER_BUS] {
+ const PLACEHOLDER: Slot = Slot::Unused;
+ let mut out = [PLACEHOLDER; SLOTS_PER_BUS];
+ if slots.len() > SLOTS_PER_BUS - 1 {
+ panic!("too many devices on bridge");
+ }
+ out[0] = Slot::Reserved;
+ let mut i = 1;
+ while i < SLOTS_PER_BUS && i - 1 < slots.len() {
+ out[i] = slots[i - 1];
+ i += 1;
+ }
+ out
+}
+
+#[cfg(test)]
+mod test {
+ use crate::Slot;
+ use crate::constants::SLOTS_PER_BUS;
+ use crate::layout::bridge_slots;
+
+ #[test]
+ fn test_bridge_slots() {
+ const BRIDGE: [Slot; SLOTS_PER_BUS] =
+ bridge_slots(&[Slot::Reserved, single_device!(Viommu)]);
+ assert_eq!(BRIDGE[1], Slot::Reserved);
+ }
+}
diff --git a/pve-qemu-server-pci/src/lib.rs b/pve-qemu-server-pci/src/lib.rs
new file mode 100644
index 0000000..43a3886
--- /dev/null
+++ b/pve-qemu-server-pci/src/lib.rs
@@ -0,0 +1,33 @@
+//! Generation of the PCI and PCIe addresses of the devices of a PVE guest.
+//!
+//! Which address a device gets is part of the guest's ABI: moving a device
+//! makes the guest operating system see it as a new one, so the assignment has
+//! to stay stable for the lifetime of a VM. This crate therefore describes the
+//! assignment as a static [`layout`] and only looks addresses up in it.
+//!
+//! The entry points below reproduce what the Perl implementation in
+//! `PVE::QemuServer::PCI` does, so that both can be used interchangeably.
+
+pub mod constants;
+
+mod hardware_config;
+pub use hardware_config::HardwareConfig;
+
+mod types;
+pub use types::*;
+
+mod error;
+pub use error::LayoutError;
+
+pub mod layout;
+
+/// Returns the number of the PCI bridge `id` sits on, or `None` if it is on
+/// the root bus or not a known device.
+pub fn pci_bridge_for_device(id: &str) -> Option<Bus> {
+ layout::legacy::pci_bridge_for_device(id)
+}
+
+/// Returns the address of the device with the configuration id `id`.
+pub fn address(hw: &HardwareConfig, id: &str) -> Result<PciAddress, LayoutError> {
+ layout::legacy::address(hw, id)
+}
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..052ef00
--- /dev/null
+++ b/pve-qemu-server-pci/src/types/bus.rs
@@ -0,0 +1,198 @@
+#[cfg(test)]
+use strum::VariantArray;
+
+use crate::{Device, LayoutError};
+
+/// The kinds of bridge PVE adds itself, as opposed to the ones a QEMU machine
+/// type already brings along.
+///
+/// Each kind gets its own set of bridges so that the slot of a device only
+/// depends on its own index, not on how many devices of other kinds exist.
+#[cfg_attr(test, derive(VariantArray))]
+#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq)]
+pub enum PveBridge {
+ Net,
+ Scsi,
+ VirtioBlk,
+ VirtioFs,
+ Hostpci,
+}
+
+impl PveBridge {
+ /// The bus formed by the `index`th bridge of this kind.
+ pub const fn new_bus(self, index: u8) -> Bus {
+ Bus::Pve { kind: self, index }
+ }
+
+ /// The device this kind of bridge carries at the given device index.
+ pub const fn device(self, index: u8) -> Device {
+ match self {
+ PveBridge::Net => Device::Net(index),
+ PveBridge::Scsi => Device::ScsiController(index),
+ PveBridge::VirtioBlk => Device::VirtioBlk(index),
+ PveBridge::VirtioFs => Device::VirtioFs(index),
+ PveBridge::Hostpci => Device::Hostpci(index),
+ }
+ }
+}
+
+impl std::fmt::Display for PveBridge {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let text = match self {
+ PveBridge::Net => "net",
+ PveBridge::Scsi => "scsi",
+ PveBridge::VirtioBlk => "virtio-blk",
+ PveBridge::VirtioFs => "virtio-fs",
+ PveBridge::Hostpci => "hostpci",
+ };
+ f.write_str(text)
+ }
+}
+
+impl std::str::FromStr for PveBridge {
+ type Err = LayoutError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let kind = match s {
+ "net" => PveBridge::Net,
+ "scsi" => PveBridge::Scsi,
+ "virtio-blk" => PveBridge::VirtioBlk,
+ "virtio-fs" => PveBridge::VirtioFs,
+ "hostpci" => PveBridge::Hostpci,
+ _ => return Err(LayoutError::InvalidConfigId),
+ };
+ Ok(kind)
+ }
+}
+
+/// A bus a device can be attached to, identified the same way QEMU's `bus=`
+/// parameter does.
+#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq)]
+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),
+ /// A legacy, deprecated variant of a root port.
+ #[deprecated]
+ LegacyRootPort(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 {
+ /// 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 {
+ #[expect(deprecated)]
+ Bus::Pcie(_) | Bus::RootPort(_) | Bus::LegacyRootPort(_) => None,
+ Bus::Pcidmi => Some("i82801b11-bridge"),
+ Bus::Sys(_) | Bus::Pci(_) | Bus::Pve { .. } => Some(match parent {
+ #[expect(deprecated)]
+ Bus::Pcie(_) | Bus::RootPort(_) | Bus::LegacyRootPort(_) => "pcie-pci-bridge",
+ _ => "pci-bridge",
+ }),
+ }
+ }
+}
+
+impl std::fmt::Display for Bus {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Bus::Pci(i) => write!(f, "pci.{i}"),
+ Bus::Pcie(i) => write!(f, "pcie.{i}"),
+ Bus::RootPort(i) => write!(f, "pve.root-port{i}"),
+ #[expect(deprecated)]
+ Bus::LegacyRootPort(i) => write!(f, "ich9-pcie-port-{i}"),
+ Bus::Sys(i) => write!(f, "pve.sys{i}"),
+ Bus::Pcidmi => write!(f, "pcidmi"),
+ Bus::Pve { kind, index } => {
+ write!(f, "pve.{kind}{index}")
+ }
+ }
+ }
+}
+
+impl std::str::FromStr for Bus {
+ type Err = LayoutError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ // NOTE: the prefixes here must be checked before the generic 'pve.'
+ // one below, which would otherwise swallow them.
+ const PREFIXES: &[(&str, BusConstructor)] = &[
+ ("pci.", Bus::Pci),
+ ("pcie.", Bus::Pcie),
+ ("pve.root-port", Bus::RootPort),
+ #[expect(deprecated)]
+ ("ich9-pcie-port-", Bus::LegacyRootPort),
+ ("pve.sys", Bus::Sys),
+ ];
+
+ if s == "pcidmi" {
+ return Ok(Bus::Pcidmi);
+ }
+
+ for (prefix, c) in PREFIXES {
+ if let Some(rest) = s.strip_prefix(prefix) {
+ let index: u8 = rest.parse().map_err(|_| LayoutError::InvalidConfigId)?;
+ return Ok(c(index));
+ }
+ }
+
+ if let Some(rest) = s.strip_prefix("pve.")
+ && let Some(idx) = rest.find(|c: char| c.is_ascii_digit())
+ {
+ let kind: PveBridge = rest[0..idx]
+ .parse()
+ .map_err(|_| LayoutError::InvalidConfigId)?;
+ let index: u8 = rest[idx..]
+ .parse()
+ .map_err(|_| LayoutError::InvalidConfigId)?;
+ return Ok(Bus::Pve { kind, index });
+ }
+
+ Err(LayoutError::InvalidConfigId)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use strum::VariantArray;
+
+ use crate::Bus;
+
+ use super::PveBridge;
+
+ #[test]
+ fn test_roundtrip() {
+ let test = |bus: Bus| {
+ let id = bus.to_string();
+ let new_bus: Bus = id
+ .parse()
+ .unwrap_or_else(|_| panic!("could not parse {id}"));
+ assert_eq!(bus, new_bus);
+ };
+
+ test(Bus::Pcidmi);
+ test(Bus::Pci(99));
+ test(Bus::Pcie(99));
+
+ for kind in PveBridge::VARIANTS.iter().copied() {
+ test(Bus::Pve { kind, index: 99 });
+ }
+
+ test(Bus::RootPort(99));
+ #[expect(deprecated)]
+ test(Bus::LegacyRootPort(99));
+ test(Bus::Sys(99));
+ }
+}
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..d3bf60b
--- /dev/null
+++ b/pve-qemu-server-pci/src/types/device.rs
@@ -0,0 +1,173 @@
+use std::{fmt::Display, str::FromStr};
+use strum::{EnumDiscriminants, VariantArray};
+
+use crate::{Bus, LayoutError};
+
+/// A device that needs a PCI address.
+///
+/// Variants carrying an index map to a configuration key of the same name plus
+/// that index, for example `Net(3)` is the guest's `net3`.
+#[derive(Hash, Debug, Clone, Copy, PartialEq, Eq, EnumDiscriminants)]
+#[strum_discriminants(derive(VariantArray))]
+pub enum Device {
+ Ahci,
+ Balloon,
+ Watchdog,
+ GuestAgent,
+ SpiceSerial,
+ Rng,
+ Audio,
+ Ivshmem,
+ Bridge(Bus),
+ RootPort(Bus),
+ Vga(u8),
+ Viommu,
+ VirtioFs(u8),
+ Net(u8),
+ ScsiController(u8),
+ VirtioBlk(u8),
+ Piix3Controller,
+ XhciController(u8),
+ Hostpci(u8),
+ Hostpcie(u8),
+ /// The EHCI USB controllers of the ICH9 chipset, numbered from one like
+ /// their QEMU models.
+ Ich9Ehci(u8),
+ /// The UHCI USB controllers of the ICH9 chipset, numbered from one like
+ /// their QEMU models.
+ Ich9Uhci(u8),
+ /// The HDA controller legacy q35 guests always have, independent of a
+ /// configured `audio0`.
+ Ich9Audio,
+}
+
+impl Display for Device {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ let text = match self {
+ Device::Ahci => "ahci",
+ Device::Balloon => "balloon",
+ Device::Watchdog => "watchdog",
+ Device::GuestAgent => "guest-agent",
+ Device::SpiceSerial => "spice-serial",
+ Device::Rng => "rng",
+ Device::Audio => "audio",
+ Device::Ivshmem => "ivshmem",
+ Device::Viommu => "viommu",
+ Device::Piix3Controller => "Piix3Controller",
+ Device::Ich9Audio => "ich9-audio",
+ _ => "",
+ };
+ if !text.is_empty() {
+ return f.write_str(text);
+ }
+ let text = match self {
+ Device::Bridge(bus) => bus.to_string(),
+ Device::RootPort(bus) => bus.to_string(),
+ Device::Vga(i) => format!("vga{i}"),
+ Device::VirtioFs(i) => format!("virtiofs{i}"),
+ Device::Net(i) => format!("net{i}"),
+ Device::ScsiController(i) => format!("scsihw{i}"),
+ Device::VirtioBlk(i) => format!("virtioblk{i}"),
+ Device::XhciController(i) => format!("xhci{i}"),
+ Device::Hostpci(i) => format!("hostpci{i}"),
+ Device::Hostpcie(i) => format!("hostpcie{i}"),
+ Device::Ich9Ehci(i) => format!("ich9-ehci{i}"),
+ Device::Ich9Uhci(i) => format!("ich9-uhci{i}"),
+ _ => String::new(),
+ };
+ write!(f, "{text}")
+ }
+}
+
+/// Rebuilds the single possible [`Device`] of a discriminant, which only works
+/// for the variants that carry no index.
+impl TryFrom<DeviceDiscriminants> for Device {
+ type Error = LayoutError;
+
+ fn try_from(value: DeviceDiscriminants) -> Result<Self, Self::Error> {
+ let res = match value {
+ DeviceDiscriminants::Ahci => Device::Ahci,
+ DeviceDiscriminants::Balloon => Device::Balloon,
+ DeviceDiscriminants::Watchdog => Device::Watchdog,
+ DeviceDiscriminants::GuestAgent => Device::GuestAgent,
+ DeviceDiscriminants::SpiceSerial => Device::SpiceSerial,
+ DeviceDiscriminants::Rng => Device::Rng,
+ DeviceDiscriminants::Audio => Device::Audio,
+ DeviceDiscriminants::Ivshmem => Device::Ivshmem,
+ DeviceDiscriminants::Piix3Controller => Device::Piix3Controller,
+ DeviceDiscriminants::Ich9Audio => Device::Ich9Audio,
+ DeviceDiscriminants::Bridge
+ | DeviceDiscriminants::RootPort
+ | DeviceDiscriminants::Vga
+ | DeviceDiscriminants::Viommu
+ | DeviceDiscriminants::VirtioFs
+ | DeviceDiscriminants::Net
+ | DeviceDiscriminants::ScsiController
+ | DeviceDiscriminants::VirtioBlk
+ | DeviceDiscriminants::XhciController
+ | DeviceDiscriminants::Hostpci
+ | DeviceDiscriminants::Hostpcie
+ | DeviceDiscriminants::Ich9Ehci
+ | DeviceDiscriminants::Ich9Uhci => return Err(LayoutError::InvalidConfigId),
+ };
+ Ok(res)
+ }
+}
+
+type DeviceConstructor = fn(u8) -> Device;
+
+/// Parses the configuration id of a device, for example `net0` or `hostpci3`.
+impl FromStr for Device {
+ type Err = LayoutError;
+
+ 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),
+ ("hostpci", Device::Hostpci),
+ ];
+
+ for (prefix, c) in PREFIXES {
+ if let Some(index) = s.strip_prefix(prefix) {
+ let index = index.parse().map_err(|_| LayoutError::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(LayoutError::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..889b961
--- /dev/null
+++ b/pve-qemu-server-pci/src/types/layout.rs
@@ -0,0 +1,329 @@
+use std::collections::VecDeque;
+
+use crate::constants::{FUNCTIONS_PER_DEVICE, SLOTS_PER_BUS, USABLE_SLOTS_PER_BUS};
+use crate::{Bus, Device, LayoutError, PciAddress, 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>; SLOTS_PER_BUS]),
+ /// A PCIe root port with the single device attached to it.
+ RootPort(Bus, Device),
+}
+
+/// What sits at one slot of a bus.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Slot<'a> {
+ /// 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 with one entry per function number.
+ Multi(&'a [Function<'a>; FUNCTIONS_PER_DEVICE]),
+ /// As many bridges of `kind` as are needed to hold `max` devices. The
+ /// bridges share this slot by using one function number each.
+ DynamicBridges { kind: PveBridge, max: u8 },
+}
+
+/// 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 Layout<'a> {
+ /// Whether the root bus is PCIe. Only the root bus is affected, nested
+ /// buses stay plain PCI.
+ pub pcie: bool,
+ pub root: [Slot<'a>; SLOTS_PER_BUS],
+}
+
+impl Layout<'_> {
+ /// 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, LayoutError> {
+ self.iter()
+ .find_map(|used| (used.device == *device).then_some(used.addr))
+ .ok_or(LayoutError::NoAddressFound(*device))
+ }
+
+ /// 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) -> AddressAssignments<'_> {
+ AddressAssignments::new(self)
+ }
+}
+
+/// The address a layout assigns to a device.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct AddressAssignment {
+ 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 address assignments of a [`Layout`], see
+/// [`Layout::iter`].
+#[derive(Debug)]
+pub struct AddressAssignments<'a> {
+ resolved: VecDeque<AddressAssignment>,
+ bridge_slot_stack: Vec<(Slot<'a>, PciAddress)>,
+}
+
+impl<'a> AddressAssignments<'a> {
+ pub(crate) fn new(layout: &Layout<'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: VecDeque::new(),
+ }
+ }
+
+ /// Resolves one function of a slot into the queue of address assignments.
+ ///
+ /// 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.push_back(AddressAssignment {
+ addr,
+ device,
+ multifunction,
+ });
+ }
+ Function::RootPort(bus, device) => {
+ self.resolved.push_back(AddressAssignment {
+ addr,
+ device: Device::RootPort(bus),
+ multifunction,
+ });
+ self.resolved.push_back(AddressAssignment {
+ addr: PciAddress::new(bus, 0, 0),
+ device,
+ multifunction: false,
+ });
+ }
+ Function::FixedBridge(bus_type, slots) => {
+ self.resolved.push_back(AddressAssignment {
+ 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));
+ }
+ }
+ }
+ }
+
+ /// Resolves a slot of dynamic bridges into the queue of address
+ /// assignments.
+ ///
+ /// Unlike fixed bridges, the devices behind them are queued right away,
+ /// as they are generated from the bridge kind rather than listed in the
+ /// layout.
+ fn handle_dynamic_bridges(&mut self, mut addr: PciAddress, kind: PveBridge, max: u8) {
+ // 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_SLOTS_PER_BUS);
+ debug_assert!(usize::from(num_bridges) <= FUNCTIONS_PER_DEVICE);
+
+ for bridge_index in 0..num_bridges {
+ addr.function = bridge_index;
+ let bus = kind.new_bus(bridge_index);
+ self.resolved.push_back(AddressAssignment {
+ addr,
+ device: Device::Bridge(bus),
+ multifunction: bridge_index == 0 && num_bridges > 1,
+ });
+ for slot_nr in 1..=USABLE_SLOTS_PER_BUS {
+ // u16 so that a bigger 'max' cannot overflow
+ let index = u16::from(bridge_index) * u16::from(USABLE_SLOTS_PER_BUS)
+ + u16::from(slot_nr)
+ - 1;
+ if index >= u16::from(max) {
+ break;
+ }
+ self.resolved.push_back(AddressAssignment {
+ addr: PciAddress::new(bus, slot_nr, 0),
+ device: kind.device(index as u8),
+ multifunction: false,
+ });
+ }
+ }
+ }
+}
+
+impl Iterator for AddressAssignments<'_> {
+ type Item = AddressAssignment;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ loop {
+ if let Some(assignment) = self.resolved.pop_front() {
+ return Some(assignment);
+ }
+ 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::DynamicBridges { kind, max } => {
+ self.handle_dynamic_bridges(addr, kind, max)
+ }
+ }
+ }
+
+ if self.resolved.is_empty() && self.bridge_slot_stack.is_empty() {
+ return None;
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::constants::SLOTS_PER_BUS;
+ use crate::layout::bridge_slots;
+ use crate::{AddressAssignment, Bus, Device, PciAddress};
+
+ use super::{Function, Layout, Slot};
+
+ const BRIDGE: [Slot; SLOTS_PER_BUS] =
+ bridge_slots(&[Slot::Single(Function::Device(Device::Watchdog))]);
+ const LAYOUT: Layout<'static> = Layout {
+ pcie: false,
+ root: crate::layout::bridge_slots(&[
+ Slot::Single(Function::Device(Device::Rng)),
+ Slot::Single(Function::Device(Device::Audio)),
+ Slot::Reserved,
+ Slot::Unused,
+ Slot::Multi(&[
+ Function::Reserved,
+ Function::Unused,
+ Function::Unused,
+ Function::Unused,
+ Function::Device(Device::Viommu),
+ Function::Unused,
+ Function::Unused,
+ Function::Unused,
+ ]),
+ Slot::Single(Function::FixedBridge(Bus::Pci(99), &BRIDGE)),
+ ]),
+ };
+
+ #[test]
+ fn basic_layout() {
+ let test = |device: Device, addr: PciAddress| {
+ let found = LAYOUT
+ .find_device(&device)
+ .unwrap_or_else(|_| panic!("could not find {device:?}"));
+ assert_eq!(found, addr);
+ };
+
+ test(Device::Rng, PciAddress::new(Bus::Pci(0), 1, 0));
+ test(Device::Audio, PciAddress::new(Bus::Pci(0), 2, 0));
+ test(Device::Viommu, PciAddress::new(Bus::Pci(0), 5, 4));
+ test(Device::Watchdog, PciAddress::new(Bus::Pci(99), 1, 0));
+ }
+
+ #[test]
+ fn iterator() {
+ let mut iter = LAYOUT.iter();
+ let pci0 = Bus::Pci(0);
+ assert_eq!(
+ iter.next(),
+ Some(AddressAssignment {
+ addr: PciAddress {
+ bus: pci0,
+ device: 1,
+ function: 0
+ },
+ device: Device::Rng,
+ multifunction: false,
+ })
+ );
+ assert_eq!(
+ iter.next(),
+ Some(AddressAssignment {
+ addr: PciAddress {
+ bus: pci0,
+ device: 2,
+ function: 0
+ },
+ device: Device::Audio,
+ multifunction: false,
+ })
+ );
+ assert_eq!(
+ iter.next(),
+ Some(AddressAssignment {
+ addr: PciAddress {
+ bus: pci0,
+ device: 5,
+ function: 4
+ },
+ device: Device::Viommu,
+ multifunction: false,
+ })
+ );
+ assert_eq!(
+ iter.next(),
+ Some(AddressAssignment {
+ addr: PciAddress {
+ bus: pci0,
+ device: 6,
+ function: 0
+ },
+ device: Device::Bridge(Bus::Pci(99)),
+ multifunction: false,
+ })
+ );
+ assert_eq!(
+ iter.next(),
+ Some(AddressAssignment {
+ addr: PciAddress {
+ bus: Bus::Pci(99),
+ device: 1,
+ function: 0
+ },
+ device: Device::Watchdog,
+ multifunction: false,
+ })
+ );
+ assert_eq!(iter.next(), None);
+ }
+}
diff --git a/pve-qemu-server-pci/src/types/mod.rs b/pve-qemu-server-pci/src/types/mod.rs
new file mode 100644
index 0000000..3847745
--- /dev/null
+++ b/pve-qemu-server-pci/src/types/mod.rs
@@ -0,0 +1,11 @@
+mod bus;
+pub use bus::{Bus, PveBridge};
+
+mod device;
+pub use device::Device;
+
+mod pci_address;
+pub use pci_address::PciAddress;
+
+mod layout;
+pub use layout::{AddressAssignment, AddressAssignments, Function, Layout, Slot};
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..dcef298
--- /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;
+ let function = if self.function > 0 {
+ format!(".{}", self.function)
+ } else {
+ String::new()
+ };
+
+ format!(",bus={bus},addr=0x{:x}{function}", self.device)
+ }
+}
diff --git a/rustfmt.toml b/rustfmt.toml
new file mode 100644
index 0000000..f3e454b
--- /dev/null
+++ b/rustfmt.toml
@@ -0,0 +1,2 @@
+edition = "2024"
+style_edition = "2024"
--
2.47.3
next prev parent reply other threads:[~2026-10-15 8:12 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 ` Dominik Csapak [this message]
2026-10-15 12:16 ` [PATCH pve-qemu-server-rs v2 1/9] add pve-qemu-server-pci crate for guest PCI address generation 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 ` [PATCH pve-qemu-server-rs v2 3/9] pci: legacy: generate q35 chipset devices and hostpcie root ports Dominik Csapak
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-2-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