watchthelight labs

How the NullMoth NVIDIA driver works

This page follows a frame from a Metal app down to the GPU, then covers how the driver is installed, removed, and recovered. Paths are the ones on an installed Mac; the source folder for each piece is in brackets.

The driver is new. It is tested on an RTX 5060 under macOS 15.7.x, and it may not work on every card, board or OpenCore setup. If your Mac does not boot macOS with OpenCore yet, set that up first with the OpenCore Install Guide, then install the driver.

1. The layers

Metal app / WindowServer / Core Image / MPS / OpenGL (Apple's GL-on-Metal)
        |
        v
NVMTLDriver.bundle  (Metal driver plugin)                         [plugin/]
        |  shaders: Apple AIR  -> SPIR-V                            [translator/]
        v
NVK  (Mesa's Vulkan driver for NVIDIA, with the NAK compiler)      [nvk/nvk-macos.patch]
        |  SPIR-V -> NVIDIA machine code, command buffers
        v
NVRM.kext  (NVIDIA's open GPU kernel modules, r610, ported to XNU) [kexts/NVRM]
        |  talks to the GPU's GSP firmware
        v
GeForce RTX GPU  (GSP firmware from /Users/Shared/nvfw/nvidia/610.57.04)

Display and compositing run beside that path:

WindowServer -> IOFramebuffer = NVRMFB.kext            (NVKMS heads, modes, vblank)       [kexts/NVRMFB]
             -> IOAccelerator = NVAccel.kext           (surfaces WindowServer composites)
             -> AppleGraphicsDeviceControl policy = NVRMAGDC.kext                           [kexts/NVRMAGDC]

2. Kernel side

NVRM.kext. NVIDIA's open-source resource manager, built for macOS with an XNU OS layer (os-xnu*.cpp) in place of Linux's. At start it:

  1. claims the PCI device, maps its BARs, and places BAR1 (the GPU-visible window into video memory) outside the firmware console;
  2. holds the I/O registry busy so WindowServer does not start on the firmware screen;
  3. on its own thread, loads the GSP firmware from /Users/Shared/nvfw/nvidia/610.57.04/ and boots the GPU with rm_init_adapter();
  4. brings up NVKMS (NVIDIA's mode-setting) and releases the hold once the display is armed.

The boot argument -nvoff makes NVRM leave the card alone. The recovery path below uses it.

NVRM is also a power child of the GPU's PCI device: on system sleep it suspends NVKMS and puts RM in standby with all of video memory preserved, and on wake it restores the PCI state and resumes both. NVRMFB then does the wake work IOGraphics expects. SLEEP-WAKE.md has the details.

NVRMFB.kext is the macOS framebuffer (an IOFramebuffer). It publishes each NVKMS head as a display, reads real mode timings and EDIDs, and drives vblank from the GPU. NVAccel.kext is the IOAccelerator that WindowServer and IOSurface use for the shared surfaces the plugin renders into. NVRMAGDC.kext answers Apple's graphics-device-control policy queries for the card.

All four kexts install into /Library/Extensions and load from the Auxiliary Kernel Collection that install.sh rebuilds with kmutil. They are not injected by OpenCore: the display kexts link against Apple frameworks (IOGraphicsFamily, IOAcceleratorFamily2) that only exist in macOS's own kernel collections.

3. User space

NVMTLDriver.bundle (/Library/GPUBundles/) is the Metal driver Apple's Metal.framework loads for the card. It implements Metal's device, queues, command buffers, encoders, resources, argument buffers, ray tracing and mesh pipelines on top of Vulkan (nvmtl_vk.c), with contract notes for Apple-specific behavior (sample positions, sampler descriptors, read-only metadata, physical bounds) in plugin/nvmtl_*.h.

Shader translation (libnvmtl_translate.dylib, translator/) converts Apple AIR, the bitcode Metal shaders compile to, into SPIR-V that NVK can compile. It is based on metal2vulkan (LGPL-3.0).

NVK (/Library/GPUBundles/nvmtl/) is Mesa's Vulkan driver for NVIDIA plus the NAK shader compiler, patched to run on macOS against NVRM instead of the Linux DRM interface (nvk/nvk-macos.patch on Mesa 17ca6174).

NVIDIAShared.bundle carries NVIDIA's NVVM compiler libraries and air2nvvm.py, the AIR to NVVM path. The plugin's MPS convolution fast path is plugin/nvconv.metal.

4. OpenCore settings the driver needs

Setting Value Why
NVRAM > Add > 7C436110-…-FE41995C9F82 > boot-args nvfb=1 nvaccel=1 nvfbheads=4 -nvkmsnosmooth amfi_get_out_of_my_way=0x1 amfi=0x80 ipc_control_port_options=0 turns on the framebuffer and accelerator, 4 display heads; the AMFI arguments let WindowServer load the driver bundle
NVRAM > Add > … > csr-active-config <430A0000> the SIP value the driver was tested with (its kexts are not Apple-signed)
Misc > Security > SecureBootModel Disabled Apple Secure Boot refuses kexts Apple did not sign
UEFI > Quirks > ResizeGpuBars 13 8 GB BAR1 for full memory bandwidth; NVRM then moves BAR1 away from the boot screen (tested on the RTX 5060)
Booter > Quirks > ResizeAppleGpuBars -1 macOS sees the full BAR
Kernel > Block com.apple.iokit.IONDRVSupport, Exclude otherwise the firmware framebuffer takes display index 0 from NVRMFB
BIOS Above 4G Decoding on, CSM off the card maps memory above 4 GB

Optional boot arguments, for troubleshooting: -nvrmnoflip keeps NVAccel on the copy path (no zero-copy scan-out of the composited surface) and is the first thing to try when the desktop flashes or tears while the cursor moves; -nvrmnogo leaves the GPU idle until nvrmctl go <pass>; -nvrmnobootscreen skips the boot-screen snapshot.

Installing macOS itself needs different values. The macOS installer has no NVIDIA driver, so it runs on the firmware's screen, which only survives macOS's PCI setup with a small BAR: ResizeAppleGpuBars = 0, ResizeGpuBars = -1, and IONDRVSupport not excluded. 1401 builds the installer that way. The 1401 Mac app switches to the table above when it installs the driver.

5. The 1401 Mac app (installs the driver)

1401.app lives in the 1401 repository (mac/, Swift + WebKit; the window is HTML/JS in mac/Resources/). It installs a driver package with that package's own install.sh. Every change to the system is made by one root script, nullmoth-setup.sh, which the app runs through macOS's administrator prompt.

Finding OpenCore. The script reads OpenCore's boot-path NVRAM variable, which names the partition OpenCore started from, and uses that partition. If the variable is missing, it searches every EFI and FAT32 partition and keeps only a config whose SMBIOS model matches this Mac, so a rescue stick or another machine's EFI is never edited; if none matches it stops and asks for the boot disk or stick to be plugged in. OpenCore may live in EFI/OC or, when OpenCore.efi is the firmware's BOOTx64.efi, in EFI/BOOT; both are found. If more than one OpenCore is found, the app asks which one.

Install (--dry lists the same changes and makes none):

  1. checks the driver package's SHA-256;
  2. test-builds the Auxiliary Kernel Collection before changing anything, so a Mac that cannot take the driver is left as it was;
  3. backs up config.plist next to it, then makes only the changes in section 4 that are missing, and lints the result (any failure restores the backup);
  4. adds 1401: Remove NVIDIA driver to the boot picker (Misc > Tools, the NullMothSafe.efi tool);
  5. runs the package's install.sh: copies the files and builds the Auxiliary Kernel Collection (if it fails, the config backup is put back);
  6. writes an install record to /Library/NullMoth/state and installs the recovery daemon.

If SIP is still fully on in the running system, the first run sets only SIP, Secure Boot, the boot arguments and the boot picker entry, and asks for a restart; the second run installs the driver and switches the BAR.

Remove. If the config is unchanged since the install, the backup is copied back. Otherwise only the driver's own edits are undone. Then uninstall.sh removes the files and rebuilds the kernel collection.

The way back if the driver stops macOS starting. Choosing 1401: Remove NVIDIA driver in the boot picker runs NullMothSafe.efi (1401's mac/efi-safe/, Rust UEFI). It adds -nvoff to the boot arguments for the next start and sets the NVRAM flag nullmoth-remove=1. macOS then starts without the driver, the LaunchDaemon com.nullmoth.recover sees the flag, runs nullmoth-setup.sh --remove, and restarts.

Verbose startup. Turn verbose on/off adds or removes the boot argument -v in the OpenCore config and in NVRAM, so the next start shows macOS's startup text instead of the Apple logo.

USB map. The app watches the USB ports while you plug a device into each one, then writes UTBMap.kext (for USBToolBox) into the OpenCore Kexts folder.

Crash reports. If the driver panics the Mac, the app offers at the next login to write a text report to the Desktop: hardware, macOS version, and what failed, with names, serial numbers, network addresses and paths removed. Nothing is sent; you can upload the file on the site yourself.

6. The package

package/install.sh and uninstall.sh work without the app (README, Install — prebuilt). The tarball holds pkgroot/ with every file at its install path plus SHA256SUMS.

7. Building

See the README, Build from source. The kexts need NVIDIA's open-gpu-kernel-modules at tag 610.57.04; NVK needs Mesa 17ca6174 with nvk/nvk-macos.patch.