Skip to content

virtio-gpu: KMS + virgl 3D driver for QEMU/UTM/libvirt guests - #499

Open
borovikovd wants to merge 12 commits into
freebsd:6.12-ltsfrom
borovikovd:virtio-gpu
Open

virtio-gpu: KMS + virgl 3D driver for QEMU/UTM/libvirt guests#499
borovikovd wants to merge 12 commits into
freebsd:6.12-ltsfrom
borovikovd:virtio-gpu

Conversation

@borovikovd

@borovikovd borovikovd commented Aug 26, 2026

Copy link
Copy Markdown

FreeBSD guests with a virtio-gpu-pci display device have no DRM/KMS path, which rules out wlroots/sway and Wayland generally. This ports Linux's drivers/gpu/drm/virtio (v6.12, matching this tree): KMS, dumb buffers, fbdev console — plus classic virgl 3D when the host device offers it (GPU-accelerated GLES via Mesa virgl; sway then runs its default GLES2 renderer with no env overrides). No blob resources / venus.

Working on FreeBSD 15.1 aarch64 under UTM/QEMU-hvf, and confirmed on amd64 under QEMU-KVM (Xorg/XFCE) in review testing: /dev/dri/card0, vt(4) console via vt_drmfb, sway + foot + grim (WLR_RENDERER=pixman on non-GL hosts, default GLES2 renderer on virgl hosts), persistent across reboots. Setup, architecture and platform quirks in drivers/gpu/drm/virtio/README.FreeBSD.md.

Commits (review fixes are appended, not squashed):

  1. build the shmem GEM stack into drm.ko and import drm_simple_kms_helper.c
  2. verbatim Linux v6.12 import, so future re-syncs stay clean
  3. FreeBSD hunks on the imported sources: 2 files, +36 lines, 0 deletions, all under #ifdef __FreeBSD__
  4. newbus glue + a module-local Linux-virtio shim over virtqueue(9) — base LinuxKPI has no virtio compat; the shim could move into LinuxKPI later to serve other virtio ports
  5. driver README
  6. review fixes: committed-build-artifact cleanup, README notes for amd64/QEMU-KVM
  7. classic virgl: an 8-line feature-negotiation change — the imported driver's 3D machinery (EXECBUFFER, capsets, contexts) was already compiled in. Verified under UTM GPU acceleration (virglrenderer/ANGLE/Metal): kmscube ~600 fps, glmark2-es2 full suite score 689 under sway's default GLES2 renderer, sustained soak on a WITNESS+INVARIANTS kernel with no lock-order reports and no leaks; clean 2D fallback on hosts without virgl
  8. sync imported sources with Linux v6.12.85 (the tree's exact DRM version; picks up the per-plane-state fence fix)
  9. wire drivers/gpu/drm/virtio into scripts/drmgeneratepatch, so future stable syncs cover the driver like amd/i915/radeon

Licensing: all imported files keep their upstream licenses, nothing is relicensed. That includes GPL-2.0-only drm_gem_shmem_helper.c and virtgpu_vram.c — following existing practice, since drm.ko already builds GPL-2.0 drm_writeback.c and drm_panel_backlight_quirks.c, dmabuf.ko builds GPL-2.0-only sync_file.c, and the kmod ports declare BSD2CLAUSE MIT GPLv2. The FreeBSD-specific glue and shim are new code under MIT.

Prior art: #119, #62, #356. reviews.freebsd.org/D55012 is an independent virtio-gpu effort for the base system — complementary (this provides the DRM/KMS path), happy to coordinate.


Disclaimer: this code was developed with heavy AI assistance. Knowing the failure modes of AI-generated code, it went through adversarial review and testing rather than just "it works":

  • independent review passes (bugs, over-engineering, comment noise); every finding adversarially verified against the actual kernel sources before acting on it
  • clang static analyzer clean on the FreeBSD-authored TUs
  • full test cycle on a WITNESS + INVARIANTS kernel: no lock-order reports, no assertion failures
  • per-change test ladder on a dedicated test VM: cold boot, kld_list autoload, modeset test, sway session, completion-counter deltas, dmesg scan
  • 6K (77 MB) and 8K (127 MB) dumb-buffer create/destroy stress through the indirect-descriptor path
  • cross-compiles on every supported arch (amd64, i386, riscv64, powerpc64, powerpc64le); daily sway use on two VMs (one at 3456x2234)

Root causes for the platform workarounds (notify-window fault, interrupt arming, indirect descriptors) are documented in the commit messages and README.

Build drm_gem_shmem_helper.c, drm_fbdev_shmem.c and
drm_format_helper.c into drm.ko, for KMS drivers whose objects live
in anonymous memory rather than device VRAM (virtio-gpu; bochs/qxl
would use the same stack), and import drm_simple_kms_helper.c
(GPL-2.0-or-later) for the virtio module to build.

drm_gem_shmem_helper.{c,h} is the Linux 6.12 file under its
GPL-2.0-only license, following existing practice - drm.ko already
builds GPL-2.0 drm_writeback.c and drm_panel_backlight_quirks.c, and
dmabuf.ko builds GPL-2.0-only sync_file.c.  The FreeBSD diff is 47
lines under #ifdef: gfp masks, VM-object locking around the fault
handler's pfn insert (as ttm_bo_vm does), and elision of the
device-level DMA mapping, which virtio-gpu does not use (the device
addresses guest memory physically).

drm_gem.c grows FreeBSD drm_gem_get_pages()/drm_gem_put_pages(): the
Linux 6.x originals walk folio batches and the unevictable LRU, which
LinuxKPI does not model, so these are the pre-folio page-at-a-time
shape over shmem_read_mapping_page().  drm_fbdev_shmem.c compiles
with its deferred-I/O paths under #ifdef __linux__ (LinuxKPI has no
fb_deferred_io; the helper works without it).  LinuxKPI additions:
dma_fence_match_context() and the trace_dma_fence_emit() stub, both
used by virtio-gpu's fence code.
drivers/gpu/drm/virtio/ and include/uapi/linux/virtio_gpu.h from
Linux v6.12, byte-identical, including files the FreeBSD build will
not compile (Kconfig, the Linux Makefile, virtgpu_vram.c).  No FreeBSD
content in this commit, so future re-syncs against newer Linux are a
clean re-import; all local changes come as separate commits on top.
The complete diff against Linux v6.12: two files, +36 lines, zero
deletions, every line inside #ifdef __FreeBSD__ (or the __linux__ side
of an #elif that preserves the original).  Compiled with __linux__
these files are byte-equivalent to upstream.

virtgpu_drv.c: a hw.virtio_gpu sysctl node for LinuxKPI module
parameters, and a virtio_gpu_freebsd_driver() accessor so the newbus
glue can reach probe/config_changed without the Linux virtio bus
(virtio_gpu_driver is static, and module_virtio_driver() expands to
nothing useful here).

virtgpu_object.c: MODULE_PARM_DESC for virglhack (LinuxKPI's
module_param_named() requires the description symbol), and a
page-aligned allocation for the ents array - vmalloc_to_sgt() in
virtgpu_vq.c requires it, and LinuxKPI's kvmalloc is malloc(9), which
only guarantees page alignment for page-multiple sizes.
Base LinuxKPI has no virtio compat, and both kernels define struct
virtqueue, so the port is two TUs that never share virtio types:

virtgpu_freebsd.c - a native newbus virtio child (BUS_PROBE_VENDOR,
outranking vtgpu(4)) that drives the virtio 1.x status ladder for
kldload-time attach, allocates the control/cursor queues, and hands
off to the upstream driver's probe.  Two workarounds, both documented
in README.FreeBSD.md: the notify window is mapped with pmap_mapdev()
because stores through vtpci's notify sub-map fault on QEMU/hvf
aarch64, and each virtqueue gets an explicit virtqueue_enable_intr()
because FreeBSD virtqueues are born with interrupts disabled while
Linux vrings start enabled and the upstream driver relies on that.
The driver is purely interrupt-driven (shared INTx on this platform).
VIRTIO_RING_F_INDIRECT_DESC is negotiated, as Linux always does:
without it, ATTACH_BACKING of a large framebuffer (one segment per
ents page) can demand more ring slots than exist and the upstream
ring-space precheck hangs.  Detach and kldunload are refused while
the DRM device is registered (drm-kmod devfs teardown panics
otherwise); diagnostics via dev.virtio_gpu_drm.0.{reaps,queue_state}.

linux_virtio.c - the Linux virtio API over virtqueue(9): sg-chain
flattening that never merges across the readable/writable boundary
(upstream places inline responses right after the command),
virtqueue_add_sgs/get_buf/kick, errno translation, and the interrupt
trampoline.  freebsd/ holds the shim headers and the cross-TU
contract (struct fbsd_vq opacity).

virtio/Makefile builds virtio_gpu_drm.ko (all supported
architectures); the 2D KMS configuration only, no virgl.  All
imported files build under their upstream licenses, including
GPL-2.0-only virtgpu_vram.c, whose RESOURCE_BLOB/HOST_VISIBLE
features are rejected at negotiation.
drivers/gpu/drm/virtio/README.FreeBSD.md - what the port is (2D KMS,
no virgl), how to build and load it, the wlroots/sway recipe
(WLR_RENDERER=pixman), UTM notes including HiDPI, platform quirks with
their root causes, and diagnostics.
Generated headers, interface-directory symlinks (machine/i386/x86),
export_syms and .kld intermediates slipped into the tree; all of them
are build products that bsd.kmod.mk (re)creates and .gitignore already
covers.  The committed machine symlink pointed at arm64 includes and
silently broke amd64 builds from a fresh checkout - found in amd64
review testing on PR freebsd#499.
Warn against loading via loader.conf _load entries (DRM core is not
initialized at loader time; probe fails with -19 - use kld_list), and
document -vga virtio for plain QEMU, where a bare -device
virtio-gpu-pci leaves QEMU's default std-VGA in place and X picks the
wrong card as primary.  Both found in amd64 testing on PR freebsd#499.
Request and propagate VIRTIO_GPU_F_VIRGL.  The imported driver is
already fully wired for classic virgl (EXECBUFFER, capsets, 3D
transfers, per-file contexts, render node); its sync_file and
dma_fence_unwrap dependencies resolve against the bundled dmabuf
module, so unlocking 3D is feature negotiation only.  Blob resources
and host-visible memory stay off (virtio_get_shm_region() reports
absence), and on hosts without virgl the bit does not negotiate and
behavior is unchanged - the same binary serves both.

With a virtio-gpu-gl-pci host (UTM GPU acceleration, virglrenderer
over ANGLE/Metal on Apple silicon): Mesa 26 virgl probes clean
(eglinfo: "virgl (ANGLE (Apple, Apple M4 Pro, OpenGL 4.1 Metal))"),
kmscube renders 280-630 fps, and sway runs on its default GLES2
renderer - no WLR_RENDERER=pixman needed.  Verified on stock and
WITNESS+INVARIANTS kernels: full 2D ladder still green with 3D
active (kmstest, 6K/8K dumb-buffer stress, grim), no witness or
assertion output.  The GL device exposes a 256-slot control ring;
the existing MAX(ring, VIRTGPU_MAX_INDIRECT) sglist sizing covers
it.  amd64 cross-compile clean.
Document the classic-virgl 3D path: host device requirements
(UTM GPU-accelerated display / virtio-gpu-gl-pci), Mesa virgl on the
guest, sway on the default GLES2 renderer, and the clean 2D fallback
on hosts without virgl.  Scope line updated; WLR_RENDERER=pixman is
now documented as needed only on non-GL host devices.
The initial import was Linux v6.12.0; the tree's DRM core is 6.12.85.
The only virtio delta in the stable series up to that tag is the
per-plane-state fence fix ("drm/virtio: New fence for every plane
update"): the pageflip fence moves from the reusable framebuffer
object to duplicated plane state, fixing fence lifetime when the same
FB is flipped repeatedly.  virtgpu_plane.c and virtgpu_drv.h are
byte-identical to v6.12.85; the two files carrying FreeBSD hunks
(virtgpu_drv.c, virtgpu_object.c) are unchanged upstream between
v6.12.0 and v6.12.85.
Add drivers/gpu/drm/virtio/ and include/uapi/linux/virtio_gpu.h to the
format-patch path list so future Linux stable syncs cover the driver
the same way as amd/i915/radeon.  The FreeBSD-only files (freebsd/,
virtgpu_freebsd.c, linux_virtio.c, README.FreeBSD.md) do not exist in
Linux, so generated patches never touch them, and the FreeBSD hunks in
virtgpu_drv.c/virtgpu_object.c are additive #ifdef blocks that
git am --3way merges around.
Verified: with the three modules in /boot/modules,
kld_list="virtio_gpu_drm" is enough - dmabuf.ko and drm.ko resolve
via MODULE_DEPEND.  This is the config a drm-kmod package install
would use; absolute paths remain for out-of-module-path setups.
@ut316ab

ut316ab commented Aug 29, 2026

Copy link
Copy Markdown

Tested this on amd64 under QEMU/KVM (the PR's existing testing was aarch64/UTM/QEMU-hvf on Apple Silicon) — wanted to close that gap.

Up front: this confirms Xorg/X11 specifically. I could not get any Wayland compositor working on this driver — not a Sway-specific issue, every Wayland path I tried hit the same wall (details below). Summary follows; full writeup and a reproduction script available if useful.

Bug found and fixed: virtio/machine symlink

virtio/machine is a committed symlink pointing at:

/usr/src/sys/arm64/include

It needs to point at /usr/src/sys/amd64/include. As shipped, every amd64 build silently compiles against ARM64 kernel headers, producing confusing "undeclared function" errors (pmap_invalidate_cache, cpu_feature, CPUID_PAT, etc.) that look like missing includes in base's LinuxKPI but are actually just the wrong architecture entirely. i386/x86 symlinks in the same directory were already correct — just machine was stale, presumably left over from an aarch64 dev environment.

diff --git a/virtio/machine b/virtio/machine
index d6a2c08..f5ca013 120000
--- a/virtio/machine
+++ b/virtio/machine
@@ -1 +1 @@
-/usr/src/sys/arm64/include
\ No newline at end of file
+/usr/src/sys/amd64/include
\ No newline at end of file

With that fixed, it builds and works cleanly on amd64: driver attaches, /dev/dri/card0 registers, console framebuffer takes over correctly.

Confirmed working: Xorg/X11 (XFCE4) on amd64/QEMU-KVM

Full desktop session over X11 (modesetting driver), cold boot → automatic login (dbus + sddm via kld_list) → working XFCE4, keyboard and mouse both functional. One QEMU launch detail that mattered: -vga virtio (not bare -device virtio-gpu-pci) — otherwise QEMU's implicit default std-VGA device stacks alongside it and Xorg picks the wrong one as primary.

Not working: Wayland (any compositor, not just Sway)

I tried Sway specifically and could not get it rendering. Traced the failure down to DRM core, not compositor-specific code, so I'd expect any Wayland compositor to hit the same thing: this build negotiates -resource_blob -host_visible -virgl (2D-only, no accelerated buffer path). Mesa's EGL/GBM falls back to kms_swrast, which allocates via CREATE_DUMB through the render node — and drm_ioctl_permit() in drm_ioctl.c refuses any non-DRM_RENDER_ALLOW ioctl from a render client. Confirmed CREATE_DUMB has flags 0 in real upstream Linux v6.12 too, so this is expected DRM behavior, not a porting bug — X11 avoids it entirely by allocating through the primary/master node instead. A real fix would mean implementing resource_create_blob/VIRTGPU_BLOB — genuine feature work, out of scope for a quick patch.

Secondary finding: loader.conf autoload race

Loading via /boot/loader.conf (true boot-time) fails outright — DRM core is not initialized, probe returns -19. drm.ko's own init hasn't completed yet at that point in boot. kld_list in rc.conf (loads later, via normal /etc/rc.d) avoids this; worth being aware of either way.

Happy to share the full reproduction package (QEMU launch script + guest-side build script with everything above scripted, including the symlink fix and desktop config) if that'd help independent verification.

@borovikovd

borovikovd commented Aug 29, 2026

Copy link
Copy Markdown
Author

Good catch @ut316ab on the symlink - it was actually worse: 26 build artifacts (generated headers, machine/i386/x86 symlinks, .kld files) got committed by a stray git add -A on my side. Fixed and verified with a fresh clone on aarch64 + amd64 cross build.

Thanks for the X11 confirmation. Added the -vga virtio note to the README - UTM sets up the device itself so I never hit the std-VGA stacking.

On Wayland: your trace is right, but that wall is exactly why the README says to run sway with WLR_RENDERER=pixman (and LIBSEAT_BACKEND=seatd). Pixman skips Mesa/EGL/GBM entirely and allocates dumb buffers on the master fd, so the render-node CREATE_DUMB restriction never comes into play. That's how sway runs on my two aarch64 VMs daily. Worth a retry on your setup - I'd expect it to just work. Agreed that a real accelerated path means VIRTGPU_BLOB and is out of scope here.

loader.conf: correct, DRM core isn't up at loader time. README now warns to use kld_list (same commit).

Yes to the reproduction package, the QEMU launch script especially.

UPDATE: the accelerated path turned out to be 8 lines away - classic virgl is now in the PR (last two commits; the imported driver's 3D code was already compiled in). With a GL host device sway runs its default GLES2 renderer, no pixman needed. Verified on aarch64/UTM: kmscube ~600 fps, glmark2-es2 score 689, WITNESS soak clean. On your setup try -device virtio-gpu-gl-pci. The render-node wall now only applies to non-GL devices.

@borovikovd borovikovd changed the title virtio-gpu: 2D KMS driver for QEMU/UTM/libvirt guests virtio-gpu: KMS + virgl 3D driver for QEMU/UTM/libvirt guests Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants