diff --git a/src/ProxmoxSharp/Vm/QemuParamEncoder.cs b/src/ProxmoxSharp/Vm/QemuParamEncoder.cs
index c097cbe..8d173c5 100644
--- a/src/ProxmoxSharp/Vm/QemuParamEncoder.cs
+++ b/src/ProxmoxSharp/Vm/QemuParamEncoder.cs
@@ -125,12 +125,20 @@ public static string EncodeNet(QemuNet n)
return string.Join(",", opts);
}
- /// hostpci0 value: "<host>[,pcie=1][,x-vga=1][,rombar=0][,romfile=…][,mdev=…]".
+ ///
+ /// hostpci0 value: "<mapping=NAME|host>[,pcie=1][,x-vga=1][,rombar=0][,romfile=…][,mdev=…]".
+ /// Prefers the resource-mapping form ("mapping=NAME") — the only form a token can set.
+ ///
public static string EncodeHostPci(QemuHostPci h)
{
ArgumentNullException.ThrowIfNull(h);
- if (string.IsNullOrEmpty(h.Host)) throw new ArgumentException($"hostpci '{h.Id}' needs a Host PCI address.");
- var opts = new List { h.Host };
+ var head = (h.Mapping, h.Host) switch
+ {
+ ({ } m, _) when !string.IsNullOrEmpty(m) => $"mapping={m}",
+ (_, { } host) when !string.IsNullOrEmpty(host) => host,
+ _ => throw new ArgumentException($"hostpci '{h.Id}' needs a Mapping name or a raw Host PCI address."),
+ };
+ var opts = new List { head };
if (h.Pcie is true) opts.Add("pcie=1");
if (h.XVga is true) opts.Add("x-vga=1");
if (h.Rombar is false) opts.Add("rombar=0"); // only emit when explicitly disabled (default is on)
diff --git a/src/ProxmoxSharp/Vm/QemuVmSpec.cs b/src/ProxmoxSharp/Vm/QemuVmSpec.cs
index cea1708..2d43a89 100644
--- a/src/ProxmoxSharp/Vm/QemuVmSpec.cs
+++ b/src/ProxmoxSharp/Vm/QemuVmSpec.cs
@@ -91,11 +91,21 @@ public sealed record QemuNet
public bool? Firewall { get; init; }
}
-/// A PCI(e) passthrough device (hostpciN) — the gaming GPU. e.g. "hostpci0".
+///
+/// A PCI(e) passthrough device (hostpciN) — the gaming GPU. e.g. "hostpci0".
+///
+/// Provide EITHER (a Proxmox PCI resource-mapping name) or
+/// (a raw PCI address). Prefer : Proxmox only lets
+/// the real root@pam user set a raw hostpciN ("only root can set 'hostpciN'
+/// config for non-mapped devices"), whereas a mapped device is settable by any token with
+/// Mapping.Use — and it's node-portable.
+///
+///
public sealed record QemuHostPci
{
public required string Id { get; init; }
- public required string Host { get; init; } // PCI address, e.g. "0000:09:00"
+ public string? Mapping { get; init; } // PCI resource-mapping name (token-settable; preferred)
+ public string? Host { get; init; } // raw PCI address, e.g. "0000:09:00" (root@pam only)
public bool? Pcie { get; init; }
public bool? XVga { get; init; }
public bool? Rombar { get; init; }
diff --git a/tests/ProxmoxSharp.Tests/QemuParamEncoderTests.cs b/tests/ProxmoxSharp.Tests/QemuParamEncoderTests.cs
index 55ca240..3a3ec13 100644
--- a/tests/ProxmoxSharp.Tests/QemuParamEncoderTests.cs
+++ b/tests/ProxmoxSharp.Tests/QemuParamEncoderTests.cs
@@ -17,6 +17,23 @@ public void HostPci_encodes_the_passthrough_line_like_the_working_win_vm()
Assert.Equal("0000:09:00,pcie=1,x-vga=1", v);
}
+ [Fact]
+ public void HostPci_encodes_the_resource_mapping_form()
+ {
+ // The token-settable form: hostpci0: mapping=,pcie=1,x-vga=1
+ var v = QemuParamEncoder.EncodeHostPci(new QemuHostPci
+ {
+ Id = "hostpci0", Mapping = "AMD_Radeon_RX6600", Pcie = true, XVga = true,
+ });
+ Assert.Equal("mapping=AMD_Radeon_RX6600,pcie=1,x-vga=1", v);
+ }
+
+ [Fact]
+ public void HostPci_without_mapping_or_host_throws()
+ {
+ Assert.Throws(() => QemuParamEncoder.EncodeHostPci(new QemuHostPci { Id = "hostpci0" }));
+ }
+
[Fact]
public void HostPci_omits_rombar_unless_explicitly_disabled()
{