← Back · sub-plan of the NextBSD graphics plan · sibling of the virtio-gpu DRM plan

NextBSD fallback graphics: KMS on machines with no supported GPU

A machine whose GPU we do not support gets a vt(4) text console and nothing else — no /dev/dri/card0, so no Wayland, no Xorg modesetting, no desktop. Linux solved this in 2021 with SimpleDRM: a DRM driver that binds the framebuffer the firmware already set up, works on any machine regardless of GPU, and steps aside automatically when a real driver arrives. This plan scopes the NextBSD equivalent.

2026-08-16. Scoped separately from the virtio-gpu work because it is a different problem with different users: virtio-gpu is about making VMs fast, this is about making unsupported hardware usable at all. Neither blocks the other. Citations to Linux v6.12/v6.16, freebsd/drm-kmod 6.12-lts and freebsd/freebsd-src releng/15.1.

Scope This driver is deliberately not a substitute for a real one. It produces /dev/dri/card0 with no render node — software rendering via llvmpipe, at whatever single resolution the firmware chose. It is the difference between “no desktop” and “a working desktop”, not between slow and fast. Every machine that has a real driver should use the real driver, and this one must get out of the way when that happens.

Contents

  1. The problem it solves
  2. What Linux does — and the surprising part
  3. Firmware paths: this is not EFI-only
  4. The load-trigger question — the NextBSD-specific problem
  5. Handoff and eviction
  6. What we would build, and what we already have
  7. Write or port: the licensing fork
  8. Decisions to make

1. The problem it solves

Today, NextBSD's graphics kexts bind by PCI device id. IntelGraphics carries 364 ids, AMDGraphics 308, RadeonGraphics 550. If a machine's GPU is not in one of those tables — new silicon, an NVIDIA card outside the legacy blob's range, anything unusual — nothing matches, nothing loads, and there is no DRM device at all.

The user-visible result is not “slower graphics”. It is a text console. No compositor will start, because Wayland compositors require a KMS device and Xorg's modesetting driver requires /dev/dri/card0. The machine is unusable as a desktop until someone writes or fixes a driver for that specific GPU.

A fallback driver changes that outcome to “a working desktop on software rendering”, on any machine, immediately — including machines nobody has ever tested.

2. What Linux does — and the surprising part

Linux's simpledrm landed in 5.14 (2021) at drivers/gpu/drm/tiny/simpledrm.c and moved to drivers/gpu/drm/sysfb/ in April 2025, gaining efidrm, vesadrm and ofdrm siblings. drm-kmod's own maintainer describes the value precisely, in issue #269:

“SimpleDRM creates a drm device (/dev/dri/card0) but no drm render device. So you still have software accel (and so need swrast/llvmpipe) but can talk with KMS to the driver meaning wayland software will work.”

The surprising part Linux does not gate the load on “is anything else driving the display?” It loads the fallback unconditionally and early, then lets the real driver evict it when it probes.

Boot-time firmware code (drivers/firmware/sysfb.c) registers a platform device — simple-framebuffer, efi-framebuffer or vesa-framebuffer — from the framebuffer the firmware left behind. simpledrm binds it as an ordinary platform driver. Later, when a real GPU driver probes, it calls drm_aperture_remove_conflicting_framebuffers(), which unregisters that device and unbinds simpledrm.

This is worth weighing carefully, because the intuitive design — “load it only if nothing else claimed the display” — has a flaw Linux's approach avoids: there is no window during which the machine has no KMS device. The fallback is there from early boot, and the handover is a replacement rather than a gap.

3. Firmware paths: this is not EFI-only

FreeBSD's vt(4) already has four framebuffer backends, and they are structurally identical — the same preload_search_info() call against a different metadata tag left by the loader:

/* sys/dev/vt/hw/efifb/efifb.c:86 */
efifb = (struct efi_fb *)preload_search_info(preload_kmdp,
    MODINFO_METADATA | MODINFOMD_EFI_FB);

/* sys/dev/vt/hw/vbefb/vbefb.c:86 */
vbefb = (struct vbe_fb *)preload_search_info(preload_kmdp,
    MODINFO_METADATA | MODINFOMD_VBE_FB);

They map one-to-one onto Linux's family, which means legacy BIOS machines are supportable by the same driver — a different front-end selecting the metadata source, not a separate driver:

FirmwareFreeBSD vt(4) backendMetadata tagLinux DRM driverMachines
UEFI GOPefifbMODINFOMD_EFI_FBefidrmmodern amd64, all arm64
BIOS VBEvbefbMODINFOMD_VBE_FBvesadrmlegacy amd64
Open FirmwareofwfbofdrmPowerPC
Device treesimplefbsimpledrmARM SBCs

The name should therefore be firmware-neutral. EFIGraphics was the working title and is wrong; FirmwareGraphics or SimpleGraphics both describe it. This is decision F4, and it interacts with the still-open IOGraphicsExtras naming question.

4. The load-trigger question — the NextBSD-specific problem

This is the part Linux does not have to solve, and the reason this document exists separately.

NextBSD kexts autoload through the in-kernel IOKit matcher, on IOPCIPrimaryMatch — a table of PCI vendor:device words matched against IOPCIDevice nodes. A firmware framebuffer is not a PCI node. There is no device to match; the driver's entire input is a struct the loader left in memory. So the existing autoload path cannot express “load this one”.

Four candidate triggers, with the trade-off that matters for each:

T1 — Always load early, evict on real bind Linux's model

Load at boot (via loader.conf, or a kext the matcher always loads), take card0 immediately, and rely on the real driver's drm_aperture_remove_conflicting_framebuffers() to evict it.

For: no window without a KMS device; the eviction machinery already exists in drm-kmod and is already called by our bochs and vboxvideo ports. Proven design.

Against: on the overwhelmingly common case — a supported GPU — we load a driver, publish a device node, then tear it down seconds later. The card0 minor may shift when the real driver takes over, which compositors handle on Linux but is a behaviour to verify rather than assume.

T2 — Match any display controller at the lowest probe score most IOKit-idiomatic; needs verification

IOKit resolves competing drivers by IOProbeScore — highest wins. If the fallback carried a personality matching any PCI display controller by class code (IOPCIClassMatch, class 0x03) at a probe score below every real driver's, the matcher would pick the real driver whenever one exists and the fallback only otherwise. No policy code anywhere; the arbitration is the framework's job.

For: this is exactly what IOProbeScore is for, it needs no new kextd logic, and it degrades correctly by construction.

Against: the matcher does not support class matching today — verified. sys/sys/iocatalogue.h documents exactly one match form:

/* each encoded 0x<device><vendor> (device in the high 16 bits, vendor
   in the low 16) - the IOPCIPrimaryMatch form, e.g. 0x24f38086 */

So this costs a matcher change before it costs a driver. But the change is small, because every ingredient is already present. The scan loop in iokit_catalogue.c already calls pci_get_class(child) and already compares it against PCIC_DISPLAY; struct iocat_add already carries int32_t probe_score; /* IOProbeScore; higher wins */. Adding a second match form (a class/subclass word alongside the vendor:device array) is a contained extension to code that already reads the field.

The genuine limitation is different: it only covers displays that appear as PCI nodes. A machine whose framebuffer comes from a non-PCI display — the normal case on ARM SBCs — would still have nothing to match, so T2 cannot be the only trigger if those are ever a target.

T3 — kextd loads it if nothing claimed the display explicit, but needs a new signal

After the matcher settles, kextd asks “did any IOPCIDevice with a display class bind a driver?” and loads the fallback if not.

For: explicit, debuggable, no wasted load on supported machines.

For, more than first assumed: the hard half of this already exists. The matcher's scan loop already answers “does this display device have a DRM driver bound?” — it walks PCI children, identifies display-class devices, looks through a vgapci claim to see whether a drmn child is actually attached (iocat_drmn_bound()), and requests the GPU kext when it is not. That logic was written so a vgapci-shadowed GPU still gets its driver loaded (#64). “No display device has DRM bound” is the same query with a different answer, not new machinery.

Against: it still needs a “matching has settled” moment, which does not exist. Matching is asynchronous — proving autoload for bochs required polling rather than a single check, because the first look raced the load. Deciding “nothing claimed it” too early gives a false negative on a slow-probing driver, and the cost of that error is a machine that boots to a text console for no reason.

T4 — loginwindow triggers it too late

Userland decides at session start.

Against: leaves the console without KMS for the whole boot, cannot help anything running before the login window, and puts a kernel-driver policy decision in a GUI process. Recorded for completeness; not recommended.

5. Handoff and eviction

Whichever trigger is chosen, the eviction path is the same one already working in our tree. On amd64 the bochs port produces exactly this transition:

drmn0: <drmn> on vgapci0
[drm] Found bochs VGA, ID 0xb0c5.
VT: Replacing driver "efifb" with new "drmfb".

The fallback sits one layer up in the same sequence:

firmware GOP / VBE
  → efifb | vbefb            vt(4) console, from power-on
  → FirmwareGraphics.kext    card0, software rendering — ANY machine
  → IntelGraphics | AMDGraphics | RadeonGraphics | VirtIOGraphics …
                             card0, accelerated — when the GPU is supported

Ordering is load-bearing A real driver must evict the fallback before it touches the hardware, not after. On some configurations the firmware framebuffer is a resource allocated on the device itself — on arm64, ArmVirtQemu's VirtioGpuDxe allocates the GOP framebuffer as a virtio-gpu resource — so a device reset invalidates the very memory the fallback is scanning out of. Sloppy ordering would survive on amd64, where efifb points at a PCI BAR that outlives a reset, and would fault on arm64. This belongs in the boot test as an asserted behaviour rather than an assumed one.

6. What we would build, and what we already have

The driver is small: read the framebuffer descriptor, publish one fixed mode, one CRTC/encoder/connector on drm_simple_kms_helper, and damage-blit client buffers into the fixed aperture. The reason it is small is that the helper layer already exists — it was built for bochs and vboxvideo and happens to be exactly what this needs.

Checking Linux's simpledrm plane-update path against what IOGraphicsExtras.kext exports today:

NeededProvided byStatus
drm_fb_blitdrm_format_helper.c✅ built (added for vboxvideo)
drm_format_conv_state_reservedrm_format_helper.c✅ built (added for vboxvideo)
shadow plane statedrm_gem_atomic_helper.c✅ built (added for vboxvideo)
drm_gem_fb_create_with_dirtyour drm_gem_framebuffer_helper.c✅ shipping
drm_simple_display_pipedrm_simple_kms_helper.c✅ shipping
drm_atomic_helper_damage_iter_*drm_damage_helper.c✅ drm-kmod ships it
TTM system-domain GEMttm/, drm_gem_ttm_helper.c✅ drm-kmod ships it
drm_gem_fb_begin_cpu_access / end_cpu_accessour drm_gem_framebuffer_helper.cexcluded when vendored

That last row is a gap of our own making. When drm_gem_framebuffer_helper.c was vendored, five functions were excluded under #ifndef __FreeBSD__ — three because drm-kmod's stub already provides them, and the dma-buf CPU-access pair because nothing needed them at the time. simpledrm's damage-blit path calls exactly that pair. Re-enabling them is small and known, but it is a real prerequisite rather than a detail to discover at link time.

Buffer backing — decide before writing

Second strike against the VRAM helper Beyond the Wayland double-buffering problem below, drm_gem_vram_helper places the BO in device memory — precisely the path blocked by nextbsd-kernel#71. TTM system-domain GEM, the recommendation below, stays in system memory and takes the branch verified working on i915 hardware. This is now a correctness reason, not only a capacity one.

The scanout target is fixed iomem; a GEM allocator is only needed for client-side buffers, which are then blitted in. TTM system-domain GEM is the right choice. drm_gem_vram_helper is already built and would appear to work, but a VRAM pool over a firmware aperture is exactly one screen's worth of memory — sufficient for Xorg modesetting, and fatal for any Wayland compositor that wants to double-buffer. Choosing it would produce a driver that passes a smoke test and fails the actual use case.

7. Write or port: the licensing fork

Everything vendored so far is GPL-2.0-or-later, and graphics/README.md states that as an invariant. Linux's simpledrm.c, the whole sysfb/ family, and drm_gem_shmem_helper.c (which they all select) are GPL-2.0-only.

Port Linux's simpledrm/efidrmWrite the FreeBSD-native equivalent
LicenceGPL-2.0-only, twice over (driver + shmem helper)No new obligation — invariant holds
Buffer backingInherits drm_gem_shmem_helper (782 lines, not in drm-kmod)TTM, already shipping
Framebuffer discoveryNeeds linux/sysfb.h, linux/screen_info.h — neither in LinuxKPIFour-line preload_search_info() call
Upstream driftTracks a moving Linux targetOurs to maintain

The native route wins on every axis here, which is unusual in this project — for bochs and vboxvideo, vendoring was clearly right because the drivers encode real hardware knowledge. A firmware framebuffer encodes none: the “driver” is a mode, a pointer and a blit. Most of what Linux's version contains is discovery plumbing that FreeBSD replaces with one call.

8. Decisions to make

F1 — Which load trigger? blocking

T1 (always load, evict) is proven and has no KMS-less window. T2 (class match at low probe score) is the most IOKit-idiomatic and needs no policy code, if the matcher supports IOPCIClassMatch — unverified, and cheap to check. T3 needs a “matching finished” signal that does not exist. T4 is too late to be useful.

F2 — Write native, or port Linux's? blocking

Native keeps the GPL-2.0-or-later invariant and avoids the shmem helper entirely. Porting tracks upstream but crosses the licence line twice and drags in a 782-line helper drm-kmod does not carry. See §7.

F3 — Which firmware paths at launch?

UEFI (efifb) covers all arm64 and modern amd64. BIOS (vbefb) is a small addition on the same driver. Device tree and Open Firmware are only relevant if NextBSD ever targets SBCs or PowerPC.

F4 — Name

FirmwareGraphics, SimpleGraphics, or something else. Not EFIGraphics — the driver is not EFI-specific.

F5 — Extend the matcher with a class-match form? answered: not supported today; small to add

Resolved by reading src-overlay/sys/dev/iokit/iokit_catalogue.c and sys/sys/iocatalogue.h: the matcher supports the IOPCIPrimaryMatch vendor:device form only. Class matching would be a new form.

What remains is a genuine choice rather than an unknown, because the extension is cheap — the scan already reads pci_get_class() and tests PCIC_DISPLAY, and probe_score is already in the ABI. Either:

F6 — Does the fallback need to cover non-PCI displays? scoping

T2 can only match displays that appear as PCI nodes. A firmware framebuffer on a machine with no PCI display device — ARM SBCs, some virtual platforms — has nothing to match against, and would need T1 or T3 regardless. Deciding whether those are ever a NextBSD target settles whether T2 can stand alone or must be paired.

Sub-plan of the NextBSD graphics plan; sibling of the virtio-gpu DRM plan, which is the priority work. This document exists because the two solve different problems — virtio-gpu makes VMs fast, this makes unsupported hardware usable — and neither blocks the other. Every claim above was checked against a primary source; the one item that is not verified is the matcher's IOPCIClassMatch support (§F5), and it is flagged as such rather than assumed.