NextBSD graphics: porting the NVIDIA driver as per-branch co-existing kexts

One NVIDIAGraphics<NNN>.kext per FreeBSD nvidia branch — drm/modeset/core tiers, per-branch firmware, and the hard single-branch-per-kernel wall · built from NVIDIA’s driver tarball like FreeBSD’s nvidia ports. Draft, July 2026.

0. What you asked, and the two corrections that shape everything

The request: name the newest branch by version too (NVIDIAGraphics595.kext, not a bare NVIDIAGraphics.kext) so the version is always legible; port as many older branches as possible as co-existing kexts; tier them by method — DRM/KMS, else nvidia-modeset, else nvidia.ko only. That taxonomy is exactly right and is adopted verbatim. A three-agent adversarial review surfaced two things the naive version of this plan got wrong, and both are baked into what follows:

1. The six branches and the three method tiers

As of mid-2026 FreeBSD ships one current branch plus five frozen legacy branches. There is no 570 legacy port — 570 was a transient mainline version superseded by 580; do not create a 570 kext. Version numbers below are examples; pin the exact branch version at build from each port’s distinfo.

KextBranch / portMethodModules bundled
(bold = load target = CFBundleExecutable)
/dev/dri?FirmwareGPU familiesBinds IOGraphics?
NVIDIAGraphics595.kextmain / 595
x11/nvidia-kmod (+ graphics/nvidia-drm-kmod)
drm-kmsnvidia-drm → nvidia-modeset → nvidiayesGSP blobs (see §4)Turing, Ampere, Ada, Hopper, Blackwellyes (drmn)
NVIDIAGraphics580.kext580
x11/nvidia-kmod-580
drm-kmsnvidia-drm → nvidia-modeset → nvidiayesGSP blobs (see §4)Maxwell, Pascal, Volta + Turing…Blackwellyes (drmn)
NVIDIAGraphics470.kext470
x11/nvidia-kmod-470
modeset-onlynvidia-modeset → nvidianononeKepler (GeForce 600/700)no
NVIDIAGraphics390.kext390
x11/nvidia-kmod-390
modeset-onlynvidia-modeset → nvidianononeFermi (GeForce 400/500)no
NVIDIAGraphics340.kext340
x11/nvidia-kmod-340
core-onlynvidianononeTesla (GeForce 8/9/200/300)no
NVIDIAGraphics304.kext304
x11/nvidia-kmod-304
core-onlynvidianononeCurie (GeForce 6/7)no

Why 470/390 are “modeset-only” despite shipping an nvidia-drm.ko upstream: the gap is FreeBSD-specific. graphics/nvidia-drm-kmod builds the DRM bridge only for the main and 580 branches; there is no FreeBSD nvidia-drm for 470/390/340/304, so those give X11 UMS + nvidia-modeset console KMS but never /dev/dri. Verify at build against that Makefile before finalizing the 470/390 tier — it is the single fact the whole no-/dev/dri, no-IOGraphics classification hangs on.

2. Naming: every kext carries its branch number

Adopted rule, per your request: the kext basename always carries the branch number; there is no unnumbered “latest” alias. So NVIDIAGraphics595.kext, not NVIDIAGraphics.kext. When 600 lands it is a new numbered kext with no rename churn, and a machine’s loaded kext tells you the driver series at a glance.

3. The packaging blocker: many .kos, one load target

NextBSD’s ko2kext.sh model gives a kext one Contents/MacOS/<executable> and keys loaded-state off that single CFBundleExecutable. But an NVIDIA drm-kms kext bundles three modules and the dependency runs nvidia_drm → nvidia_modeset → nvidia (each depends on the one below). Loading the leaf nvidia pulls in nothing above it — you get /dev/nvidia and no KMS. You must load the top of the chain, which drags its dependencies down. Therefore:

TierBundled .kosCFBundleExecutable (load target)Result
drm-kms (595, 580)nvidia, nvidia-modeset, nvidia-drmnvidia-drmfull KMS, /dev/dri/cardN
modeset-only (470, 390)nvidia, nvidia-modesetnvidia-modesetconsole KMS + X11 UMS, no /dev/dri
core-only (340, 304)nvidianvidia/dev/nvidia only

ko2kext.sh needs a multi-.ko packaging step it does not have today: stage all of a branch’s modules into the bundle, set CFBundleExecutable to the tier’s top module, and ensure the intra-bundle dependency chain resolves within the bundle (via OSBundleLibraries / full-path preload) rather than by bare module name across the whole Extensions tree — see the linker.hints hazard in §5. This also corrects a premise the first-pass plan leaned on: CFBundleExecutable is not identical across all six (it is nvidia-drm / nvidia-modeset / nvidia by tier). Note CFBundleExecutable is the .ko filename, which you may rename per branch without touching the binary; only the compiled-in DECLARE_MODULE name is immutable — and that immutable name is what causes the load-time wall.

OSBundleLibraries, and why NVIDIA omits TTM/DMABuf

The 595/580 kexts link only the drmn/IOGraphics core (ko2kext.sh -l org.nextbsd.kext.iographics); nvidia-drm’s hard bind is MODULE_DEPEND(nvidia_drm, drmn, 2,2,2). Unlike the i915/amdgpu kexts they do not list TTM/DMABuf — NVIDIA uses its own memory manager, not the Linux TTM path. That asymmetry vs the sibling kexts is correct, not an oversight; state it in the Info.plist rationale so nobody “fixes” it.

4. Firmware: raw GSP blobs in Resources/firmware/, only on the Turing+ kexts

Match the mechanism NextBSD’s shipping drm kexts already use (drm612 plan §3): firmware is staged as raw blobscp -RL into <kext>/Contents/Resources/firmware/ — and the kernel finds them via the firmware_path kernel patch. No separate firmware kext, and not the firmware(9)-.ko model (which would add yet another module to the load chain and compound §3). Only two branches carry firmware:

BranchGSP firmwareDefaultNotes
595gsp_tu10x.bin (Turing) + gsp_ga10x.bin (Ampere and all newer: Ada/Hopper/Blackwell)GSP off by default; required only for Blackwell / RTX-50Bundle both blobs. Enable per-GPU via hw.nvidia.registry.EnableGpuFirmware=1.
580same two blobsGSP off by defaultThe Maxwell/Pascal/Volta GPUs this branch uniquely covers are pre-GSP and never touch it.
470/390/340/304noneEmpty Resources/. (470 ships a legacy gsp.bin that only GSP-enables a few datacenter parts; treat as pre-GSP for packaging.)

The two GSP blobs total on the order of tens of MB, so the 595 and 580 bundles are materially larger than the four empty-Resources/ legacy kexts — a packaging/size note, not a blocker. There is no third GSP variant to chase (gsp_ga10x.bin already covers Ada/Hopper/Blackwell); just re-check each tarball’s firmware/ dir at build.

5. Co-existence: on disk yes, co-resident no

On disk — fine, that is the point

All six bundles stage side-by-side in the kext directory. Each carries its branch’s .kos, its own de-overlapped IOPCIPrimaryMatch table, its firmware (595/580 only), and a per-branch-unique CFBundleIdentifier. They do nothing until the matcher autoloads one.

Co-resident — impossible, permanently

A kext only wraps the unmodified .ko; the kernel sees the module by its compiled-in DECLARE_MODULE name, not the bundle directory. Every branch’s core is literally module nvidia (the modeset/bridge are uniformly nvidia_modeset/nvidia_drm). FreeBSD’s module_register() does module_lookupbyname() and returns EEXIST if that name is already resident — version is irrelevant; name identity alone drives it. So the first branch loads; a second branch’s kldload hits EEXIST at module_register before any driver attaches, and kern_linker unwinds and unloads the entire linker file.

Renaming the internal module to dodge this means patching NVIDIA’s binary (we don’t — we build and ship it unmodified, like the FreeBSD port), and severing the MODULE_DEPEND chain and the /dev/nvidia* userland contract. Off the table. At most one NVIDIA branch is resident per running kernel, and this is identical on stock FreeBSD and Linux (you cannot install two nvidia-driver branches concurrently there either).

Single-GPU — the supported target

All six kexts on disk; the matcher maps the card to exactly one branch; that one loads; the other five stay unloaded so the name collision never fires. For 595/580 the bridge binds drmn and creates /dev/dri/cardN; the older branches are /dev/nvidia-only. A single loaded branch already drives multiple same-generation GPUs via /dev/nvidia0..N. Cross-vendor is normal (an Intel/AMD card on card0, NVIDIA on card1, sharing drmn).

Multi-GPU — the honest limitation

6. The load-bearing open question: how does the matcher pick a branch?

Everything above assumes NextBSD’s userspace matcher autoloads one branch by GPU. There are two candidate mechanisms, and which one NextBSD actually uses is unconfirmed and must be settled before committing:

  1. PCI-id personalities (the graphics-plan model). gen-nvidia-personalities parses each branch’s supportedchips.html into an exact, non-contiguous 0x10de id list, then computes a global de-overlap (newest branch that lists a chip wins) so the six match tables are pairwise-disjoint. NVIDIA support ranges are cumulative and overlap heavily (a Turing card is in both 580’s and main’s raw lists), so verbatim lists would match one card to two or three kexts. A descending IOProbeScore (595 > 580 > 470 >…) is belt-and-suspenders for any residual overlap.
  2. Marketing-name-string selection (what the closely-related lineage actually shipped). Per freebsd-gpu-detection-comparison.html §7/§9, the IOPCIMatch+IOProbeScore personality approach for NVIDIA was ripped out in Phase 1d because hand-curated PCI-id tables “don’t scale”; the shipping selector matches the GPU’s marketing-name string against per-branch patterns, newest-first (the NomadBSD model) — and it ships one nvidia-drm-kmod with no version selection at all.

7. Base & DRM bump: which branches are coupled

Separate the two halves of the “6.6→6.12” story, because they touch different branches:

BranchDRM-6.12 coupled?Base-15.1 (kernel KBI) coupled?Build risk on 15.x
595, 580yes (bridge lockstep)yeslow
470, 390no bridgeyesmoderate
340, 304no bridgeyeshigh — predate open-modules & modern LinuxKPI; may not compile against a 15.x kernel at all. 304 is the likely drop.

Practical rule: the recipe always rebuilds nvidia-drm against whatever drm core IOGraphics is currently built from.

7a. One shared drm.ko core — Intel/AMD/Radeon and NVIDIA move together

There is exactly one drm.ko (drmn) core on the system, and the entire drm-kmod tree is a single versioned unit. The shipping CI proves it: the drm-kmod job builds drm + i915kms + amdgpu + radeonkms from one freebsd/drm-kmod checkout, and IntelGraphics/AMDGraphics/RadeonGraphics all -l org.nextbsd.kext.iographics (the single IOGraphics.kext = drm.ko). nvidia-drm binds that same core. So the core version is all-or-nothing:

7b. What the drm-612 bump actually costs (and why NVIDIA is spared it)

The execution plan’s one real code surprise is the Intel device-id generator (#337) — the exact “Intel IDs don’t line up” issue. On 6.6, gen-i915-personalities.sh greps INTEL_VGA_DEVICE(0x…) literals out of i915_pciids.h. 6.12 restructured that header: the IDs moved into MACRO__(0x…) entries inside per-platform INTEL_<PLAT>_IDS(…) macros, so the old regex matches zero lines → empty IntelGraphics.kext → the generator’s [ -n ] guard exit 1s. The fix is not a regex swap — a naive header scrape over-claims xe-only platforms (INTEL_LNL_IDS Lunar Lake / INTEL_BMG_IDS Battlemage, no i915 driver in 6.12-lts) and under-claims transitive includes (INTEL_MTL_IDS pulls in INTEL_ARL_IDS). The correct extraction is preprocessor-driven off the driver’s own pciidlist[] in i915_pci.c (→ 364 bound IDs; LNL/BMG absent, ARL present). amdgpu (308 rows) and radeon (699 rows) are unaffected. Full implementation-ready diff in that plan’s §3.7.

8. The build repo and the build DAG

NVIDIA is built the same way FreeBSD’s nvidia ports build it: fetch NVIDIA’s driver tarball at build, compile the kernel glue against the local kernel, link the nv-kernel blob, and wrap with ko2kext.sh. The compiled result is redistributable — FreeBSD ships nvidia-driver/nvidia-kmod as binary packages under LICENSE_PERMS = dist-mirror no-dist-sell pkg-mirror no-pkg-sell auto-accept — so NextBSD can ship the resulting kexts in nextbsd-pkg the same way, carrying NVIDIA’s license file (doc/license.txt) and not selling them. Whether the build/wrap lives in a sibling repo (nextbsd-nvidia) or a job in nextbsd-kernel-modules is a build-architecture choice. This mirrors FreeBSD’s split of x11/nvidia-driverx11/nvidia-kmod + graphics/nvidia-drm-kmod.

  1. Stand up the NVIDIA build (sibling repo nextbsd-nvidia or a job in nextbsd-kernel-modules); fetch NVIDIA’s tarball at build (no NVIDIA source vendored in-repo).
  2. Provide the NextBSD kernel obj (/usr/src/sys artifacts + linker.hints) and the exact IOGraphics.kext/drmn commit the 595/580 bridges must KBI-match.
  3. Per branch, in parallel: fetch the unmodified tarball; recompile the shim + link nv-kernel.onvidia.ko (all six), nvidia-modeset.ko (595/580/470/390), and — only 595/580 — nvidia-drm.ko against the step-2 drmn.
  4. gen-nvidia-personalities: parse all six supportedchips.html and compute the global de-overlap in one pass (re-run whenever any branch’s chip list changes — a point release can move a boundary GPU). Gated on §6 first.
  5. Per branch: stage firmware raw into Resources/firmware/ (595/580 only).
  6. Per branch: ko2kext.sh with the multi-.ko step (§3) — CFBundleExecutable = tier top module; unique CFBundleIdentifier; -l org.nextbsd.kext.iographics and -f firmware for 595/580 only.
  7. Assemble all six; validate the six match tables are pairwise-disjoint (no id in zero or two lists) before shipping.
  8. Test single-GPU autoload end-to-end per card; confirm a second branch’s load reports a clean EEXIST “branch already resident.”
  9. License: carry NVIDIA’s doc/license.txt in the kext/package and don’t sell it — the same terms FreeBSD ships nvidia-driver under (LICENSE_PERMS pkg-mirror no-pkg-sell auto-accept).

9. Per-branch recipe checklist

10. Risks & open questions