Skip to content

Commit 888555f

Browse files
committed
gpu detection comparison: add macOS column + section
The original kmodloader plan was modeled on kextd + IOKit personality matching; Phase 1a-c shipped a literal port. Adding macOS to the comparison closes the loop on "why didn't you just do what Apple does?" and grounds the project's design heritage. What's added: - macOS column in the matrix (11 axes); explicit "n/a" for axes that don't translate (scfb/vesa fallback, NVIDIA legacy version selection) - New axis: pre-link/boot speedup — only macOS has it (kextcache fuses kernel + matched kexts into a single boot image) - New §7 explaining the kextd loop, IOPCIMatch + IOProbeScore (the design we shipped and removed), kextcache prelinking, DriverKit (modern userspace drivers), and three reasons we don't do exactly this on FreeBSD - TL;DR bullet acknowledging the heritage - macOS / IOKit references section with apple-oss-distributions pointers Subsequent sections renumbered (§8 too-new, §9 scorecard, §10 xlibre takeaway, §11 recommendations, §12 references). Navigation updated.
1 parent 6c7d99e commit 888555f

1 file changed

Lines changed: 89 additions & 5 deletions

File tree

‎freebsd-gpu-detection-comparison.html‎

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
<a href="#initgfx">NomadBSD initgfx</a>
8484
<a href="#xlibre">xlibre</a>
8585
<a href="#linux">Linux udev</a>
86+
<a href="#macos">macOS kextd</a>
8687
<a href="#toonew">"Too-new" GPU case</a>
8788
<a href="#scorecard">Scorecard</a>
8889
<a href="#refs">References</a>
@@ -107,6 +108,7 @@ <h3>TL;DR</h3>
107108
<li><strong>"Too new" failure mode is benign on FreeBSD.</strong> If <code>i915kms</code> loads but the chip isn't in its supported table, the LinuxKPI shim returns <code>ENXIO</code> from <code>pci_probe</code>. The kmod stays resident (~10MB RAM), no <code>drmn</code> child attaches, <code>vgapci0</code> keeps owning the slot, <code>vt_efifb</code> keeps owning the console, and Xorg falls back to <code>scfb</code> (UEFI) or <code>vesa</code> (BIOS). No crash, no console disruption.</li>
108109
<li><strong>xlibre (and Xorg generally) doesn't help us pick kmods.</strong> Its detection runs <em>after</em> the DRM kmod is loaded; the vendor-switch table maps to userspace X driver names (<code>intel</code>/<code>modesetting</code>/<code>amdgpu</code>), not kmod names. Doesn't tell us which kmods to load or skip.</li>
109110
<li><strong>Linux's udev+modprobe is the right shape</strong> — and FreeBSD has the equivalent (devmatch + linker.hints). The only reason kmodloader needs a GPU PCI scan at all is that drm-kmod doesn't declare <code>MODULE_PNP_INFO</code>. Upstream PR to drm-kmod adding it would let our GPU pass collapse into devmatch.</li>
111+
<li><strong>macOS kextd + IOKit was our original reference architecture.</strong> Phase 1a-c shipped a literal port (.kext bundles, IOPCIMatch arrays, IOProbeScore). We ripped it out in Phase 1d because hand-curated personality plists don't scale to FreeBSD's hardware breadth — Apple's full-time staff curates IOPCIMatch tables for the SoCs Apple ships; we'd be doing the same work for every PC ever made. Apple's <code>kextcache</code> prelink optimization remains an interesting Phase 4+ idea.</li>
110112
</ul>
111113
</section>
112114

@@ -123,6 +125,7 @@ <h2 id="matrix">1. Comparison matrix</h2>
123125
<th>NomadBSD <code>initgfx</code></th>
124126
<th>xlibre</th>
125127
<th>Linux udev+modprobe</th>
128+
<th>macOS kextd / IOKit</th>
126129
</tr>
127130
</thead>
128131
<tbody>
@@ -133,6 +136,7 @@ <h2 id="matrix">1. Comparison matrix</h2>
133136
<td>POSIX shell (467 LOC) + sourced DB (1782 LOC)</td>
134137
<td>C, inside Xorg server</td>
135138
<td>C (libkmod) inside udevd</td>
139+
<td>C++ (kextd, IOKit), launchd-supervised</td>
136140
</tr>
137141
<tr>
138142
<td>Cadence</td>
@@ -141,6 +145,7 @@ <h2 id="matrix">1. Comparison matrix</h2>
141145
<td>Boot via rc.d, but PCI-hash short-circuit if hardware unchanged</td>
142146
<td>Server start; not autoload-related</td>
143147
<td>Hot-plug + boot, event-driven</td>
148+
<td>Boot (kextcache prelink) + hot-plug (IOKit matching notifications)</td>
144149
</tr>
145150
<tr>
146151
<td>Detection input</td>
@@ -149,6 +154,7 @@ <h2 id="matrix">1. Comparison matrix</h2>
149154
<td><code>pciconf -lv</code> awk-parsed + <code>hw.pci.default_vgapci_unit</code></td>
150155
<td>libpciaccess + udev DRM enum + <code>drmGetVersion</code></td>
151156
<td>kernel <code>MODALIAS</code> uevent → pattern match</td>
157+
<td>IOKit IORegistry walk; IOService::probe per matching kext</td>
152158
</tr>
153159
<tr>
154160
<td>(vendor:device) → driver source</td>
@@ -157,6 +163,7 @@ <h2 id="matrix">1. Comparison matrix</h2>
157163
<td>~507 AMD IDs + ~56 legacy Intel IDs in sourced DB; NVIDIA branches by marketing-name string</td>
158164
<td>Inline <code>switch(vendor)</code> in C; modern Intel falls through to <code>modesetting</code></td>
159165
<td><code>MODULE_DEVICE_TABLE</code> in each <code>.ko</code>; <code>depmod</code> emits <code>modules.alias</code></td>
166+
<td><code>IOKitPersonalities</code> dict in each <code>.kext/Info.plist</code> with <code>IOPCIMatch</code> arrays</td>
160167
</tr>
161168
<tr>
162169
<td>Validation that the driver actually works</td>
@@ -165,6 +172,7 @@ <h2 id="matrix">1. Comparison matrix</h2>
165172
<td><span class="yesno yes">Yes</span> &mdash; <code>test_x</code> runs <code>xinit</code> with the proposed config; rollback on failure</td>
166173
<td>n/a (post-kmod)</td>
167174
<td><span class="yesno no">No</span> at module level (PCI probe is the validation)</td>
175+
<td><span class="yesno yes">Yes</span> &mdash; <code>IOService::probe</code> returns <code>IOProbeScore</code>; highest-scoring kext wins; non-matching kexts never attach</td>
168176
</tr>
169177
<tr>
170178
<td>Fallback to scfb/vesa/efifb</td>
@@ -173,6 +181,7 @@ <h2 id="matrix">1. Comparison matrix</h2>
173181
<td>Explicit, picks <code>vesa</code> on BIOS / <code>scfb</code> on EFI from <code>machdep.bootmethod</code></td>
174182
<td>Falls back through <code>fbdev</code>/<code>scfb</code>/<code>vesa</code> in compile-time order</td>
175183
<td>n/a (kmod stays loaded; userspace X handles fallback)</td>
184+
<td>n/a &mdash; Apple ships hardware + drivers as a unit; "no matching driver" effectively can't happen</td>
176185
</tr>
177186
<tr>
178187
<td>"Kmod loaded but didn't attach" cleanup</td>
@@ -181,6 +190,7 @@ <h2 id="matrix">1. Comparison matrix</h2>
181190
<td><span class="yesno partial">Indirect</span> &mdash; <code>test_x</code> failure rolls back next-boot config but doesn't <code>kldunload</code></td>
182191
<td>n/a</td>
183192
<td><span class="yesno no">No</span> &mdash; same shape: probe-fail leaves module resident</td>
193+
<td><span class="yesno yes">Yes</span> &mdash; kexts that don't pass probe never load; kextcache only fuses kexts whose match succeeds</td>
184194
</tr>
185195
<tr>
186196
<td>Hot-plug coverage</td>
@@ -189,6 +199,7 @@ <h2 id="matrix">1. Comparison matrix</h2>
189199
<td><span class="yesno partial">Non-DRM only</span> via <code>dsbdriverd</code> (DRM kmods explicitly excluded)</td>
190200
<td>n/a</td>
191201
<td><span class="yesno yes">Yes</span> &mdash; udev rules trigger libkmod on every uevent</td>
202+
<td><span class="yesno yes">Yes</span> &mdash; IOKit matching notifications fire on every IORegistry change</td>
192203
</tr>
193204
<tr>
194205
<td>Multi-GPU / Optimus</td>
@@ -197,6 +208,7 @@ <h2 id="matrix">1. Comparison matrix</h2>
197208
<td>Walks each vgapci device and tries handlers; default vgapci first</td>
198209
<td>Iterates platform devices, multi-card supported</td>
199210
<td>All matching modules load; kernel binds each to its card</td>
211+
<td>IOService probe scores per device; multiple GPUs each get their best-scoring kext</td>
200212
</tr>
201213
<tr>
202214
<td>NVIDIA driver-version selection</td>
@@ -205,6 +217,7 @@ <h2 id="matrix">1. Comparison matrix</h2>
205217
<td>All four NVIDIA branches pre-staged at image-build time; runtime symlinks the right one</td>
206218
<td>Lists <code>nvidia</code> as one option; selection is xorg.conf-side</td>
207219
<td>Per-distro packaging picks one branch</td>
220+
<td>n/a since 10.13 (NVIDIA support dropped); historically Apple shipped the kext, no version selection</td>
208221
</tr>
209222
<tr>
210223
<td>Scope</td>
@@ -213,6 +226,16 @@ <h2 id="matrix">1. Comparison matrix</h2>
213226
<td>GPU + Xorg config only (non-GPU via separate <code>dsbdriverd</code>)</td>
214227
<td>X server only</td>
215228
<td>All hardware classes</td>
229+
<td>All hardware classes (IOKit owns everything driver-shaped)</td>
230+
</tr>
231+
<tr>
232+
<td>Pre-link / boot speedup</td>
233+
<td><span class="yesno no">No</span> &mdash; load on demand at boot</td>
234+
<td><span class="yesno no">No</span></td>
235+
<td><span class="yesno no">No</span></td>
236+
<td>n/a</td>
237+
<td><span class="yesno no">No</span> at the module-load layer (initramfs is a separate concept)</td>
238+
<td><span class="yesno yes">Yes</span> &mdash; <code>kextcache</code> fuses kernel + matched kexts into a single boot image</td>
216239
</tr>
217240
</tbody>
218241
</table>
@@ -358,7 +381,59 @@ <h3>6.2 The single difference that matters</h3>
358381
<h3>6.3 Linux "too new" handling</h3>
359382
<p>If the .ko loads but the live device ID isn't in the module's <code>pci_ids</code>: the driver's <code>.probe()</code> is never called, the device sits unbound. Modern kernels expose <code>i915.force_probe=</code>, <code>amdgpu.force_probe=</code> etc. for users with too-new hardware. There is no userspace "fall back to a different kernel module" mechanism — udev hands MODALIAS to libkmod, gets a hit or miss, and that's it.</p>
360383

361-
<h2 id="toonew">7. The "too-new GPU" failure case</h2>
384+
<h2 id="macos">7. macOS: kextd + IOKit (the reference architecture)</h2>
385+
386+
<p>The original kmodloader plan was modeled directly on Apple's <code>kextd</code> + IOKit personality matching. Phase 1a-c shipped a literal port of that idea (<code>.kext</code> bundles in <code>/System/Library/Extensions/</code>, <code>IOKitPersonalities</code> dicts, integer probe scores, highest-scoring kext wins). Phase 1d ripped it out in favor of <code>devmatch</code> for FreeBSD-specific reasons. Worth showing the reference architecture both for design provenance and to answer "why don't you just do what Apple does?"</p>
387+
388+
<h3>7.1 The kextd loop</h3>
389+
<p><code>kextd</code> is a launchd-supervised daemon (<code>com.apple.kextd.plist</code>). On startup it:</p>
390+
<ol>
391+
<li>Walks <code>/System/Library/Extensions/*.kext/</code> and <code>/Library/Extensions/*.kext/</code>.</li>
392+
<li>Reads each bundle's <code>Contents/Info.plist</code>, extracts the <code>IOKitPersonalities</code> dict.</li>
393+
<li>Hands all personalities to the kernel via <code>kextload(8)</code>-equivalent.</li>
394+
<li>Listens on a Mach port for <code>kAppleEventLoopMessage</code>-style notifications: when a new device appears in IOKit's IORegistry, kextd is told and re-runs matching for that device.</li>
395+
</ol>
396+
<p>The kernel keeps a personality registry indexed by match category (<code>"PCI"</code>, <code>"USB"</code>, <code>"USBClass"</code>, etc.). When IOKit sees a new device, it walks the registry, calls <code>IOService::probe</code> on every personality whose match dict could apply, and binds the device to the highest-scoring service.</p>
397+
398+
<h3>7.2 IOPCIMatch + IOProbeScore (the design our Phase 1a-c shipped)</h3>
399+
<p>An IOKit personality looks like this (Apple's actual i915 driver, simplified):</p>
400+
401+
<pre class="objc"><code>&lt;dict&gt;
402+
&lt;key&gt;CFBundleIdentifier&lt;/key&gt;
403+
&lt;string&gt;com.apple.driver.AppleIntelKBLGraphicsFramebuffer&lt;/string&gt;
404+
&lt;key&gt;IOClass&lt;/key&gt; &lt;string&gt;AppleIntelKBLFramebuffer&lt;/string&gt;
405+
&lt;key&gt;IOMatchCategory&lt;/key&gt; &lt;string&gt;IOFramebuffer&lt;/string&gt;
406+
&lt;key&gt;IOPCIMatch&lt;/key&gt;
407+
&lt;array&gt;
408+
&lt;string&gt;0x59168086&lt;/string&gt; &lt;!-- HD Graphics 620 --&gt;
409+
&lt;string&gt;0x591B8086&lt;/string&gt; &lt;!-- HD Graphics 630 --&gt;
410+
&lt;!-- ... --&gt;
411+
&lt;/array&gt;
412+
&lt;key&gt;IOProbeScore&lt;/key&gt; &lt;integer&gt;90000&lt;/integer&gt;
413+
&lt;/dict&gt;</code></pre>
414+
415+
<p>The probe call returns either <code>0</code> (don't match, skip) or a positive score. Higher score wins; ties broken by personality registration order. Non-matching personalities never get a probe call &mdash; the match dict is the gate.</p>
416+
417+
<p><strong>This is what kmodloader Phase 1a-c implemented</strong> in <code>kmodloader/personalities/*.kext/Contents/Info.plist</code> with our own probe-score algorithm. Worked correctly for hardware we hand-curated; failed scaling because FreeBSD's drm-kmod doesn't ship Info.plists with IOPCIMatch arrays, so we'd be hand-curating ~2000 device IDs ourselves &mdash; without Apple's full-time staff to maintain them.</p>
418+
419+
<h3>7.3 kextcache prelinking</h3>
420+
<p>Apple's optimization for boot speed: <code>kextcache</code> fuses the kernel binary plus every kext that matched at last-boot into a single signed boot image (<code>/System/Library/PrelinkedKernels/prelinkedkernel</code>). At boot the loader maps the prelinked kernel directly &mdash; no per-boot scan of <code>/System/Library/Extensions/</code>, no per-kext load decisions, no probe loop. The matching work happened once at install time.</p>
421+
<p>Plan §11 Phase 4 flagged this as a future optimization for kmodloader; it remains a Phase-4 idea. The Apple-shape parallel is roughly: at install time, run kmodloader's three-path scan, write the result to a manifest, have <code>/boot/loader</code> consume the manifest and pre-load on next boot. Defers all the "vendor scan + devmatch + vm_guest" work from boot to install.</p>
422+
423+
<h3>7.4 DriverKit (modern Apple userspace drivers)</h3>
424+
<p>Since macOS 10.15 / iOS 14, Apple has been moving drivers out of the kernel and into sandboxed userspace processes called <code>.dext</code> bundles (DriverExtension). Each dext is a launchd-launched signed binary that talks to a small in-kernel "personality" stub via IOKit's <code>DriverKit</code> framework. Match logic stays the same (<code>IOPCIMatch</code> in the dext's Info.plist); the actual driver code runs in userspace.</p>
425+
<p>Genuinely interesting as a long-term direction but not on kmodloader's roadmap. FreeBSD has no equivalent to DriverKit's userspace-driver kernel infrastructure today, and porting one is a project comparable to launchd itself.</p>
426+
427+
<h3>7.5 Why we don't do exactly this on FreeBSD</h3>
428+
<p>We started here (Phase 1a-c) and reached a different place (Phase 1d-1g) for three reasons:</p>
429+
<ol>
430+
<li><strong>Apple ships finite hardware.</strong> A handful of staff curate the IOPCIMatch tables for the SoCs/GPUs Apple ships. FreeBSD targets every PC ever made; hand-curated personality plists don't scale to that hardware breadth.</li>
431+
<li><strong>FreeBSD already has the data, just in a different format.</strong> <code>linker.hints</code> built from <code>MODULE_PNP_INFO</code> macros gives us the same (vendor:device → kld) map for every in-tree driver. <code>devmatch(8)</code> is the canonical reader. We don't need to reinvent the registry; we need to use the one FreeBSD already has.</li>
432+
<li><strong>The narrow gap (drm-kmod) is closeable with a 3-vendor map plus one frozen 699-ID list.</strong> No need for Apple-style full personality plists when the actual missing data is "which of three vendor IDs do I have."</li>
433+
</ol>
434+
<p>The Apple-shape design we kept: launchd supervision, ObjC implementation, plist-based config, the conceptual model of "a daemon that owns kernel-extension loading." The Apple-shape design we dropped: the per-driver IOPCIMatch personality plist as the load-decision data structure.</p>
435+
436+
<h2 id="toonew">8. The "too-new GPU" failure case</h2>
362437

363438
<p>The user's specific concern: <em>what happens in our case when the device is too new for the latest drm-kmods</em>? Detailed answer with FreeBSD source citations.</p>
364439

@@ -404,7 +479,7 @@ <h3>7.4 kmodloader's risk profile</h3>
404479
<strong>Loading drm-kmod / nvidia kmods speculatively is safe.</strong> Probe rejection is the designed-in path. The only cost is ~10MB resident kernel memory per loaded-but-unbound kmod. No panic, no console disruption, no Xorg breakage. The <code>vt_efifb</code> framebuffer is the universal fallback and stays in place exactly because no DRM driver attached to take it over.
405480
</div>
406481

407-
<h2 id="scorecard">8. Scorecard — wins and gaps</h2>
482+
<h2 id="scorecard">9. Scorecard — wins and gaps</h2>
408483

409484
<h3>8.1 Where kmodloader wins</h3>
410485

@@ -450,7 +525,7 @@ <h3>8.2 Where kmodloader has gaps</h3>
450525
<strong>Indirect efifb awareness only.</strong> When DRM kmod fails to bind, <code>vt_efifb</code> keeps the console on UEFI systems — works correctly without us doing anything. But we don't <em>verify</em> this; we don't generate an Xorg fallback config. Acceptable today (Xorg autoconfig handles scfb fallback); becomes a policy question if we ship an X server with strict driver requirements.
451526
</div>
452527

453-
<h2 id="xlibre-takeaway">9. Specific takeaway: xlibre devmatch detection</h2>
528+
<h2 id="xlibre-takeaway">10. Specific takeaway: xlibre devmatch detection</h2>
454529

455530
<p>The user's prompt asked specifically about xlibre's devmatch detection so kmodloader doesn't load too aggressively. Conclusion after deep-reading xlibre source:</p>
456531

@@ -460,7 +535,7 @@ <h2 id="xlibre-takeaway">9. Specific takeaway: xlibre devmatch detection</h2>
460535

461536
<p>The right framing is: <strong>don't expect xlibre to filter our kmod-load decisions</strong>. Filtering belongs to the kmod-load layer (us). What xlibre does provide is a clean fallback through its <code>listPossibleVideoDrivers()</code> chain (modesetting → fbdev/scfb → vesa) that handles "no DRM kmod attached" without panic — confirming that loading drm-kmod kmods speculatively is safe.</p>
462537

463-
<h2 id="recommendations">10. Recommendations</h2>
538+
<h2 id="recommendations">11. Recommendations</h2>
464539

465540
<table>
466541
<thead><tr><th>Priority</th><th>Item</th><th>Effort</th></tr></thead>
@@ -473,7 +548,7 @@ <h2 id="recommendations">10. Recommendations</h2>
473548
</tbody>
474549
</table>
475550

476-
<h2 id="refs">11. References</h2>
551+
<h2 id="refs">12. References</h2>
477552

478553
<h3>kmodloader</h3>
479554
<ul>
@@ -521,5 +596,14 @@ <h3>FreeBSD failure-mode</h3>
521596
<li>FreeBSD Handbook x11 chapter: official Xorg fallback statement (UEFI→scfb, BIOS→vesa)</li>
522597
</ul>
523598

599+
<h3>macOS / IOKit</h3>
600+
<ul>
601+
<li>Apple <code>kextd</code> source: <a href="https://github.com/apple-oss-distributions/IOKitTools">github.com/apple-oss-distributions/IOKitTools</a> (referenced for design — not vendored)</li>
602+
<li>IOKit framework: <a href="https://github.com/apple-oss-distributions/IOKitUser">apple-oss-distributions/IOKitUser</a></li>
603+
<li>IOKit Fundamentals (architecture overview, archived): <a href="https://developer.apple.com/library/archive/documentation/DeviceDrivers/Conceptual/IOKitFundamentals/">developer.apple.com IOKit Fundamentals</a></li>
604+
<li>DriverKit framework reference: <a href="https://developer.apple.com/documentation/driverkit">developer.apple.com/documentation/driverkit</a></li>
605+
<li><code>kextcache(8)</code> man page (prelinked-kernel construction): <a href="https://developer.apple.com/documentation/macos-release-notes">macOS man pages</a> — search <code>kextcache</code></li>
606+
</ul>
607+
524608
</body>
525609
</html>

0 commit comments

Comments
 (0)