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:
- A kext is not one
.ko. The drm-kms tiers involve three modules and the modeset tier two, chained byMODULE_DEPEND(nvidia_drm → nvidia_modeset → nvidia). Sinceko2kext.shwraps one.ko, each becomes its own kext chained byOSBundleLibraries(theDMABuf/IOGraphics/TTMpattern):NVIDIAGraphics595.kextwrapsnvidia-drmand depends on a modeset kext and a core kext, plusIOGraphics. Loading the top (nvidia-drm) pulls the chain; a single fat 3-module bundle would need ako2kextextension and is not how this repo works (see §3). - “Co-exist together on one system” means co-exist on disk, not co-resident. The matcher loads one branch; a second branch’s load fails
EEXISTin the kernel module namespace (§5). This is inherent to NVIDIA’s driver on FreeBSD and Linux, not a kext artifact.
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.
| Kext | Branch / port | Method | Modules bundled (bold = load target = CFBundleExecutable) | /dev/dri? | Firmware | GPU families | Binds IOGraphics? |
|---|---|---|---|---|---|---|---|
NVIDIAGraphics595.kext | main / 595 x11/nvidia-kmod (+ graphics/nvidia-drm-kmod) | drm-kms | nvidia-drm → nvidia-modeset → nvidia | yes | GSP blobs (see §4) | Turing, Ampere, Ada, Hopper, Blackwell | yes (drmn) |
NVIDIAGraphics580.kext | 580 x11/nvidia-kmod-580 | drm-kms | nvidia-drm → nvidia-modeset → nvidia | yes | GSP blobs (see §4) | Maxwell, Pascal, Volta + Turing…Blackwell | yes (drmn) |
NVIDIAGraphics470.kext | 470 x11/nvidia-kmod-470 | modeset-only | nvidia-modeset → nvidia | no | none | Kepler (GeForce 600/700) | no |
NVIDIAGraphics390.kext | 390 x11/nvidia-kmod-390 | modeset-only | nvidia-modeset → nvidia | no | none | Fermi (GeForce 400/500) | no |
NVIDIAGraphics340.kext | 340 x11/nvidia-kmod-340 | core-only | nvidia | no | none | Tesla (GeForce 8/9/200/300) | no |
NVIDIAGraphics304.kext | 304 x11/nvidia-kmod-304 | core-only | nvidia | no | none | Curie (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.
- Fits the sibling GPU kexts. The shipping drivers are
IntelGraphics/AMDGraphics/RadeonGraphics(org.nextbsd.driver.*Graphics), soNVIDIAGraphics<NNN>is more doctrine-consistent than the plan’s own bareNVIDIA/NVIDIALegacy*names. Fold this into naming decision D1 (naming-plan §3, still open) and rename the roster’sNVIDIALegacy*rows to match. - Bundle ids:
org.nextbsd.driver.NVIDIAGraphics595….NVIDIAGraphics304. A per-branch-uniqueCFBundleIdentifieris mandatory (it is the only bundle key guaranteed unique across branches within a tier). - The flat name drops one signal the old
Legacyprefix carried: which branches are KMS vs X11-only, and “Graphics” arguably over-promises KMS for core-only 340/304. Mitigation: carry the method tier and the IOGraphics dependency inInfo.plistmetadata and these docs, not in the basename, so the name stays uniform while capability stays discoverable.
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:
| Tier | Bundled .kos | CFBundleExecutable (load target) | Result |
|---|---|---|---|
| drm-kms (595, 580) | nvidia, nvidia-modeset, nvidia-drm | nvidia-drm | full KMS, /dev/dri/cardN |
| modeset-only (470, 390) | nvidia, nvidia-modeset | nvidia-modeset | console KMS + X11 UMS, no /dev/dri |
| core-only (340, 304) | nvidia | nvidia | /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 blobs — cp -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:
| Branch | GSP firmware | Default | Notes |
|---|---|---|---|
| 595 | gsp_tu10x.bin (Turing) + gsp_ga10x.bin (Ampere and all newer: Ada/Hopper/Blackwell) | GSP off by default; required only for Blackwell / RTX-50 | Bundle both blobs. Enable per-GPU via hw.nvidia.registry.EnableGpuFirmware=1. |
| 580 | same two blobs | GSP off by default | The Maxwell/Pascal/Volta GPUs this branch uniquely covers are pre-GSP and never touch it. |
| 470/390/340/304 | none | — | Empty 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:
- PCI-id personalities (the graphics-plan model).
gen-nvidia-personalitiesparses each branch’ssupportedchips.htmlinto an exact, non-contiguous0x10deid 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 descendingIOProbeScore(595 > 580 > 470 >…) is belt-and-suspenders for any residual overlap. - Marketing-name-string selection (what the closely-related lineage actually shipped). Per
freebsd-gpu-detection-comparison.html§7/§9, theIOPCIMatch+IOProbeScorepersonality 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:
- The DRM-6.12 half (drm-kmod branch flip) affects only 595 and 580, via their
nvidia-drmbridge’s hardMODULE_DEPEND(nvidia_drm, drmn, 2,2,2). The bridge must be rebuilt against the exactdrmncore commit IOGraphics ships — a stock pkg.kowill never do, and after IOGraphics moves to drm-6.12 the bridge follows in lockstep. - The base-15.1 half (kernel KBI) affects all six. Every branch’s core
nvidia.koopen-shim is recompiled against the kernel obj, so areleng/15.0 → 15.1bump re-KBIs all of them. This corrects an executive-summary overstatement from the first pass (“only the drm-kms branches are affected by the bump”) — only the drm-6.12 half is exclusive to 595/580; the base bump is universal.
| Branch | DRM-6.12 coupled? | Base-15.1 (kernel KBI) coupled? | Build risk on 15.x |
|---|---|---|---|
| 595, 580 | yes (bridge lockstep) | yes | low |
| 470, 390 | no bridge | yes | moderate |
| 340, 304 | no bridge | yes | high — 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:
nvidia-drm-612-kmodRUN_DEPENDSondrm-612-kmod’sdrm.ko;drm-612-kmodevenCONFLICTS_INSTALLwithdrm-66-kmod. You cannot run 6.6i915/amdgpu/radeonagainst a 6.12drm.ko— the KBI won’t match.- So choosing
nvidia-drm-612meansIOGraphicsis 6.12, which meansIntelGraphics/AMDGraphics/RadeonGraphicsare already 6.12 — rebuilt from the samedrm-612source. NVIDIA does not “force” the others individually; they travel together as one stack, and NVIDIA rides the shared core. - The only way to add NVIDIA without touching the others is to stay on the 6.6 core and build
nvidia-drm-66-kmod. That is the sole reason to prefer-66. Going 6.12 is fine and forward-looking; it just brings the whole stack.
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-driver → x11/nvidia-kmod + graphics/nvidia-drm-kmod.
- Stand up the NVIDIA build (sibling repo
nextbsd-nvidiaor a job innextbsd-kernel-modules); fetch NVIDIA’s tarball at build (no NVIDIA source vendored in-repo). - Provide the NextBSD kernel obj (
/usr/src/sysartifacts +linker.hints) and the exactIOGraphics.kext/drmncommit the 595/580 bridges must KBI-match. - Per branch, in parallel: fetch the unmodified tarball; recompile the shim + link
nv-kernel.o→nvidia.ko(all six),nvidia-modeset.ko(595/580/470/390), and — only 595/580 —nvidia-drm.koagainst the step-2drmn. gen-nvidia-personalities: parse all sixsupportedchips.htmland 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.- Per branch: stage firmware raw into
Resources/firmware/(595/580 only). - Per branch:
ko2kext.shwith the multi-.kostep (§3) —CFBundleExecutable= tier top module; uniqueCFBundleIdentifier;-l org.nextbsd.kext.iographicsand-ffirmware for 595/580 only. - Assemble all six; validate the six match tables are pairwise-disjoint (no id in zero or two lists) before shipping.
- Test single-GPU autoload end-to-end per card; confirm a second branch’s load reports a clean
EEXIST“branch already resident.” - License: carry NVIDIA’s
doc/license.txtin the kext/package and don’t sell it — the same terms FreeBSD shipsnvidia-driverunder (LICENSE_PERMSpkg-mirror no-pkg-sell auto-accept).
9. Per-branch recipe checklist
- Fetch the branch’s unmodified tarball at build; pin the exact version. Never host it in the repo.
- Confirm the module set — do not over/under-wrap: 595/580 = 3 modules; 470/390 = 2 (verify no FreeBSD bridge exists first); 340/304 = 1.
- Compile the shim against the kernel obj + link
nv-kernel.o. For 340/304 treat a build failure against 15.x as “drop this branch,” not a blocker for the others. - Bridge pin (595/580): rebuild
nvidia-drmagainst the exactdrmncommit IOGraphics ships. - Personalities: take this branch’s slice from the global de-overlap; emit exact non-contiguous
0x10deids; stamp descendingIOProbeScore. - Firmware: raw blobs in
Resources/firmware/for 595/580; nothing for the rest. - Wrap: unique
CFBundleIdentifier;CFBundleExecutable= tier top module;-l/-fonly for 595/580. - Properties: set
hw.nvidiadrm.modeset=1(read-only, pre-load) for the KMS branches; documenthw.nvidia.registry.EnableGpuFirmware. - Validate: 595/580 →
/dev/dri/cardN; 470/390 →/dev/nvidia+ modeset, no/dev/dri; 340/304 →/dev/nvidiaonly.
10. Risks & open questions
- Matcher premise (§6) — RESOLVED: the real repo does PCI-id personalities with de-overlap;
gen-nvidia-personalities.shbuilt + validated. No longer a blocker. - Multi-
.kopackaging (§3) —ko2kext.shas written wraps one.kowithCFBundleExecutable=nvidiaand would never load KMS on four of six branches. The multi-module step is net-new. - Licensing — not a blocker: FreeBSD ships
nvidia-driver/nvidia-kmodas binary packages (LICENSE_PERMS pkg-mirror no-pkg-sell auto-accept); NextBSD ships the kexts on the same terms — carry the license file, don’t sell. - Legacy build ceiling — 340/304 may not compile on 15.x; the “6 branches” goal realistically delivers 4–5. 304 is likely dropped.
- De-overlap correctness — must run globally and regenerate on any
supportedchips.htmlchange, or a boundary GPU lands in zero or two tables (nondeterministic autoload). linker.hintscross-branch binding (§5) — sixnvidiaproviders on disk; enforce intra-bundle resolution.- Exact
__FreeBSD_versionfloors unconfirmed for the legacy branches; resolve from each port’s Makefile at build. 595.84 is the confirmed current version; the legacy versions (580.119.02, 470.256.02…) remain examples to pin at build. - Multi-GPU semantics (§5) — ship the “pin one branch covering the most cards” policy + user override, and document the disjoint-case fallback honestly.