diff --git a/sys/arch/x86/pv/files.pv b/sys/arch/x86/pv/files.pv
index 8d167632687..0c2997a747f 100644
--- a/sys/arch/x86/pv/files.pv
+++ b/sys/arch/x86/pv/files.pv
@@ -2,4 +2,7 @@ define	pvbus {}
 
 device	pv {}
 attach	pv at pvbus
-file	arch/x86/pv/pvbus.c			pvbus	needs-flag
+file	arch/x86/pv/pvbus.c				pvbus	needs-flag
+
+attach	virtio at pv with mmio_cmdline:			virtio_mmio
+file	dev/virtio/arch/x86/virtio_mmio_cmdline.c	mmio_cmdline
diff --git a/sys/dev/virtio/arch/x86/virtio_mmio_cmdline.c b/sys/dev/virtio/arch/x86/virtio_mmio_cmdline.c
new file mode 100644
index 00000000000..90d4782dc97
--- /dev/null
+++ b/sys/dev/virtio/arch/x86/virtio_mmio_cmdline.c
@@ -0,0 +1,329 @@
+/* $NetBSD$ */
+
+/*-
+ * Copyright (c) 2025 The NetBSD Foundation, Inc.
+ * All rights reserved.
+ *
+ * This code is derived from software contributed to The NetBSD Foundation
+ * by Emile 'iMil' Heitor.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
+ * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*-
+ * Copyright (c) 2022 Colin Percival
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <sys/param.h>
+#include <sys/bus.h>
+#include <sys/device.h>
+#include <sys/kernel.h>
+#include <sys/module.h>
+#include <sys/systm.h>
+
+#define VIRTIO_PRIVATE
+#include <dev/virtio/virtio_mmiovar.h>
+#include <arch/x86/pv/pvvar.h>
+#include <xen/hypervisor.h>
+
+#include <machine/i82093var.h>
+#include "ioapic.h"
+
+#define VMMIOSTR "virtio_mmio.device="
+
+struct mmio_args {
+	uint64_t	sz;
+	uint64_t	baseaddr;
+	uint64_t	irq;
+	uint64_t	id;
+};
+
+struct virtio_mmio_cmdline_softc {
+	struct virtio_mmio_softc	sc_msc;
+	struct mmio_args		margs;
+};
+
+static int	virtio_mmio_cmdline_match(device_t, cfdata_t, void *);
+static void	virtio_mmio_cmdline_attach(device_t, device_t, void *);
+static int	virtio_mmio_cmdline_do_attach(device_t,
+		    struct pv_attach_args *, struct mmio_args *);
+static int	virtio_mmio_cmdline_detach(device_t, int);
+static int	virtio_mmio_cmdline_rescan(device_t, const char *, const int *);
+static int	virtio_mmio_cmdline_alloc_interrupts(struct virtio_mmio_softc *);
+static void	virtio_mmio_cmdline_free_interrupts(struct virtio_mmio_softc *);
+
+CFATTACH_DECL3_NEW(mmio_cmdline,
+    sizeof(struct virtio_mmio_cmdline_softc),
+    virtio_mmio_cmdline_match, virtio_mmio_cmdline_attach,
+    virtio_mmio_cmdline_detach, NULL,
+    virtio_mmio_cmdline_rescan, NULL, DVF_DETACH_SHUTDOWN);
+
+static int
+virtio_mmio_cmdline_match(device_t parent, cfdata_t match, void *aux)
+{
+	if (strstr(xen_start_info.cmd_line, VMMIOSTR) == NULL)
+		return 0;
+
+	return 1;
+}
+
+static void
+parsearg(struct mmio_args *margs, const char *arg)
+{
+	char *p;
+
+	/* <size> */
+	margs->sz = strtoull(arg, (char **)&p, 0);
+	if ((margs->sz == 0) || (margs->sz == ULLONG_MAX))
+		goto bad;
+	switch (*p) {
+	case 'E': case 'e':
+		margs->sz <<= 10;
+		/* FALLTHROUGH */
+	case 'P': case 'p':
+		margs->sz <<= 10;
+		/* FALLTHROUGH */
+	case 'T': case 't':
+		margs->sz <<= 10;
+		/* FALLTHROUGH */
+	case 'G': case 'g':
+		margs->sz <<= 10;
+		/* FALLTHROUGH */
+	case 'M': case 'm':
+		margs->sz <<= 10;
+		/* FALLTHROUGH */
+	case 'K': case 'k':
+		margs->sz <<= 10;
+		p++;
+		break;
+	}
+
+	/* @<baseaddr> */
+	if (*p++ != '@')
+		goto bad;
+	margs->baseaddr = strtoull(p, (char **)&p, 0);
+	if ((margs->baseaddr == 0) || (margs->baseaddr == ULLONG_MAX))
+		goto bad;
+
+	/* :<irq> */
+	if (*p++ != ':')
+		goto bad;
+	margs->irq = strtoull(p, (char **)&p, 0);
+	if ((margs->irq == 0) || (margs->irq == ULLONG_MAX))
+		goto bad;
+
+	/* Optionally, :<id> */
+	if (*p) {
+		if (*p++ != ':')
+			goto bad;
+		margs->id = strtoull(p, (char **)&p, 0);
+		if ((margs->id == 0) || (margs->id == ULLONG_MAX))
+			goto bad;
+	} else {
+		margs->id = 0;
+	}
+
+	/* Should have reached the end of the string. */
+	if (*p)
+		goto bad;
+
+	return;
+
+bad:
+	aprint_error("Error parsing virtio_mmio parameter: %s\n", arg);
+}
+
+static void
+virtio_mmio_cmdline_attach(device_t parent, device_t self, void *aux)
+{
+	struct virtio_mmio_cmdline_softc *sc = device_private(self);
+	struct pv_attach_args *pvaa = aux;
+	struct mmio_args *margs = &sc->margs;
+	int keylen = strlen(VMMIOSTR);
+	char *next;
+	static char cmdline[LINE_MAX], *parg = NULL;
+
+	aprint_normal("\n");
+	aprint_naive("\n");
+
+	if (parg == NULL) { /* first pass */
+		strlcpy(cmdline, xen_start_info.cmd_line, sizeof(cmdline));
+		aprint_verbose("kernel parameters: %s\n", cmdline);
+		parg = strstr(cmdline, VMMIOSTR);
+	}
+
+	if (parg != NULL) {
+		parg += keylen;
+		if (!*parg)
+			return;
+
+		next = parg;
+		while (*next && *next != ' ') /* find end of argument */
+			next++;
+		if (*next) { /* space */
+			*next++ = '\0'; /* end the argument string */
+			next = strstr(next, VMMIOSTR);
+		}
+
+		aprint_normal("viommio: %s\n", parg);
+		parsearg(margs, parg);
+
+		if (virtio_mmio_cmdline_do_attach(self, pvaa, margs))
+			return;
+
+		if (next) {
+			parg = next;
+			config_found(parent, pvaa, NULL, CFARGS_NONE);
+		}
+	}
+}
+
+static int
+virtio_mmio_cmdline_do_attach(device_t self,
+		struct pv_attach_args *pvaa,
+		struct mmio_args *margs)
+{
+	struct virtio_mmio_cmdline_softc *sc = device_private(self);
+	struct virtio_mmio_softc *const msc = &sc->sc_msc;
+	struct virtio_softc *const vsc = &msc->sc_sc;
+	int error;
+
+	msc->sc_iot = pvaa->pvaa_memt;
+	vsc->sc_dmat = pvaa->pvaa_dmat;
+	msc->sc_iosize = margs->sz;
+	vsc->sc_dev = self;
+
+	error = bus_space_map(msc->sc_iot, margs->baseaddr, margs->sz, 0,
+	    &msc->sc_ioh);
+	if (error) {
+		aprint_error_dev(self, "couldn't map %#" PRIx64 ": %d",
+		    margs->baseaddr, error);
+		return error;
+	}
+
+	msc->sc_alloc_interrupts = virtio_mmio_cmdline_alloc_interrupts;
+	msc->sc_free_interrupts = virtio_mmio_cmdline_free_interrupts;
+
+	virtio_mmio_common_attach(msc);
+	virtio_mmio_cmdline_rescan(self, "virtio", NULL);
+
+	return 0;
+}
+
+static int
+virtio_mmio_cmdline_detach(device_t self, int flags)
+{
+	struct virtio_mmio_cmdline_softc * const sc = device_private(self);
+	struct virtio_mmio_softc * const msc = &sc->sc_msc;
+
+	return virtio_mmio_common_detach(msc, flags);
+}
+
+static int
+virtio_mmio_cmdline_rescan(device_t self, const char *ifattr, const int *locs)
+{
+	struct virtio_mmio_cmdline_softc *const sc = device_private(self);
+	struct virtio_mmio_softc *const msc = &sc->sc_msc;
+	struct virtio_softc *const vsc = &msc->sc_sc;
+	struct virtio_attach_args va;
+
+	if (vsc->sc_child)
+		return 0;
+
+	memset(&va, 0, sizeof(va));
+	va.sc_childdevid = vsc->sc_childdevid;
+
+	config_found(self, &va, NULL, CFARGS_NONE);
+
+	if (virtio_attach_failed(vsc))
+		return 0;
+
+	return 0;
+}
+
+
+static int
+virtio_mmio_cmdline_alloc_interrupts(struct virtio_mmio_softc *msc)
+{
+	struct virtio_mmio_cmdline_softc *const sc =
+		(struct virtio_mmio_cmdline_softc *)msc;
+	struct virtio_softc *const vsc = &msc->sc_sc;
+	struct ioapic_softc *ioapic;
+	struct pic *pic;
+	int irq = sc->margs.irq;
+	int pin = irq;
+	bool mpsafe;
+
+	ioapic = ioapic_find_bybase(irq);
+
+	if (ioapic != NULL) {
+		KASSERT(ioapic->sc_pic.pic_type == PIC_IOAPIC);
+		pic = &ioapic->sc_pic;
+		pin = irq - pic->pic_vecbase;
+		irq = -1;
+	} else
+		pic = &i8259_pic;
+
+	mpsafe = (0 != (vsc->sc_flags & VIRTIO_F_INTR_MPSAFE));
+
+	msc->sc_ih = intr_establish_xname(irq, pic, pin, IST_LEVEL, vsc->sc_ipl,
+	    virtio_mmio_intr, msc, mpsafe, device_xname(vsc->sc_dev));
+	if (msc->sc_ih == NULL) {
+		aprint_error_dev(vsc->sc_dev,
+		    "failed to establish interrupt\n");
+		return -1;
+	}
+	aprint_normal_dev(vsc->sc_dev, "interrupting on %d\n", irq);
+
+	return 0;
+}
+
+static void
+virtio_mmio_cmdline_free_interrupts(struct virtio_mmio_softc *msc)
+{
+	if (msc->sc_ih != NULL) {
+		intr_disestablish(msc->sc_ih);
+		msc->sc_ih = NULL;
+	}
+}
+