From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: Dominik Csapak <d.csapak@proxmox.com>
Cc: pve-devel@lists.proxmox.com
Subject: Re: [PATCH pve-qemu-server-rs v2 1/9] add pve-qemu-server-pci crate for guest PCI address generation
Date: Thu, 15 Oct 2026 14:16:44 +0200 [thread overview]
Message-ID: <ehl5twunzipka5qhmxpl3tyg4c4d537mivdbwrbtlgne6trw7w@m6clmzczvee5> (raw)
In-Reply-To: <20261015081041.743422-2-d.csapak@proxmox.com>
On Thu, Oct 15, 2026 at 10:10:23AM +0200, 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 (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.
But does it make sense to act like this *here*?
I'm thinking it may make more sense for that to happen at the `pve-rs`
side, to not let this creep in any further in the future?
> + #[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);
This can also be `Ordering::Relaxed`.
> + }
> + 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> {
A rogue lifetime appears.
Drop it and return `Layout<'static>`.
> + 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";
> + }
nit: might be nicer to read as:
match original {
"a" => "b",
"x" => "y",
o => o,
}
> +
> + 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 12:19 UTC|newest]
Thread overview: 12+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-15 8:10 [RFC proxmox-perl-rs/qemu-server/qemu-server-rs v2 0/9] pci-handling rewrite (part 1/2/3) Dominik Csapak
2026-10-15 8:10 ` [PATCH pve-qemu-server-rs v2 1/9] add pve-qemu-server-pci crate for guest PCI address generation Dominik Csapak
2026-10-15 12:16 ` Wolfgang Bumiller [this message]
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=ehl5twunzipka5qhmxpl3tyg4c4d537mivdbwrbtlgne6trw7w@m6clmzczvee5 \
--to=w.bumiller@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