Symptoms
This applies if you have a Comet Lake desktop CPU (Core i3/i5/i7/i9 10xxx with UHD 630) on a 500-series board (H510, B560, H570, Z590, Q570, W580). That pairing is common in OEM machines such as the Dell OptiPlex 7090, which shipped with either 10th or 11th gen CPUs.
- The iGPU loads (
AppleIntelCFLGraphicsFramebuffer), System Information shows Metal 3, but no display is ever listed and the monitor shows no signal after the Apple logo. sudo dmesg | grep IGFBshowsHPD is lowfor every framebuffer, even with a monitor plugged in.- Bus-ID hunting, connector type changes,
igfxonln=1and faking the IMEI device-id all change nothing. -igfxvesagives a picture, but unaccelerated and very slow.
Forum threads on B560/H510 with Comet Lake reach the same conclusion: the iGPU works headless, nobody gets display output from it, use a dGPU. This page shows why, and how to get the display working.
Why it happens
Hot-plug detection (HPD) for the display ports is wired through the PCH (the chipset), not the CPU. Apple's Coffee Lake framebuffer only knows the 300/400-series PCH layout (Cannon Point / Comet Lake PCH). On a 500-series Tiger Point PCH, the same registers exist at the same addresses but the bits are arranged differently.
Linux's i915 driver supports exactly this pairing (Gen9 display + TGP PCH, see skl_hpd_pin() → icl_hpd_pin() in intel_ddi.c). That gives the real mapping.
Live hot-plug state, SDEISR (0xC4000)
| Bit | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
|---|---|---|---|---|---|---|---|---|---|---|
| Apple reads (CNP layout) | B | C | D | A | ||||||
| Hardware reports (TGP) | A | B | C | D |
Letters are the DDI ports. DDI C and D come in through the Type-C hot-plug pins (TC1, TC2) on this pairing.
So with a monitor on DDI B, bit 17 goes high while Apple checks bit 21, and the driver concludes nothing is connected. That is the HPD is low message.
The rest of the mismatch
| What | Apple expects (CNP) | Tiger Point (TGP) |
|---|---|---|
| Hot-plug interrupt bits in SDEIIR | 21 / 22 / 23 (B / C / D) | 17 / 24 / 25 |
| Hot-plug status and enables | 0xC4030 only | 0xC4030 for DDI B, 0xC4034 (TC) for C and D |
| HPD enable bits | 0xC4030 bits 4/12/20/28 | 0xC4030 bit 7 (B), 0xC4034 bits 3/7 (C/D) |
GMBUS (DDC) pin, which is Apple's busid | 5 / 4 / 6 | 2 / 9 / 10 |
The last row matters for HDMI monitors and passive DisplayPort-to-HDMI adapters, because the EDID is read over DDC. It is a config fix, not code: set the connector's bus ID to the TGP pin.
What the plugin does
CMLTGPFix is a Lilu plugin that patches AppleIntelCFLGraphicsFramebuffer as it loads. OpenCore's own kernel patches cannot be used for this: on Tahoe the graphics kexts live in SystemKernelExtensions.kc, which OpenCore never sees. WhateverGreen's framebuffer-patchN-find/replace doesn't reach it either, because it only searches the platform table, not code.
1. Live state
AppleIntelFramebufferController::DigPortHDPState reads SDEISR and picks the bit through a small jump table (mov eax, imm32; jmp short per port). The plugin swaps the three masks for DDI B, C and D to 17, 24 and 25. This alone makes the monitor detectable at boot.
2. Plug and unplug interrupts
After Apple's hwEnableInterrupts, the plugin enables and unmasks SDEIIR bits 17, 24 and 25. A wrapper around ProcessInterrupt, which runs in the hardware interrupt filter, then does the following when one of those bits fires:
- Read the TGP status fields (0xC4030 bits 5:4 for DDI B, 0xC4034 for TC1/TC2) and clear them, the way i915 does (write-1-to-clear).
- Turn each one into the event bit Apple's own dispatcher already understands: bits 15–18 for a long pulse (plug or unplug) on port 0–3, bits 19–22 for a short pulse.
- OR those bits into the controller's pending-events field and report the interrupt as handled, so
SWInterruptHandlerrunsinvokeHotplugHandler(port)as it would on a real Mac.
3. Display sleep
The TGP HPD enable bits are set by firmware at boot but lost when the display power domain switches off. Apple re-arms hot-plug using the CNP bit positions, which on TGP are status bits. After display sleep SDEISR stays low, and the monitor looks unplugged on wake. The plugin restores the TGP enables after hwEnableInterrupts and before every HPD check, without touching pending status bits.
4. HDMI and passive DP++ adapters on wake
On wake, getDisplayStatus checks HPD, then tries to power the sink up over DP AUX. A passive DP-to-HDMI adapter never answers AUX, so it returns 0. Apple's own log calls this the likely HDMI/TMDS case, yet the wake path still drops the display. The plugin reports the port as connected when that check fails but the port's TGP HPD line is high. An unplugged monitor still reads as disconnected.
Safety check. The wrappers depend on a few private structure offsets. Before routing anything, the plugin confirms the target functions still use exactly those offsets. If a macOS update changes them, it skips the wrappers and applies only the byte patches, which are harmless when they don't match. Worst case after an update is no display, not a kernel panic.
OpenCore config
This is the iGPU part of DeviceProperties → Add → PciRoot(0x0)/Pci(0x2,0x0) used on the test machine, with the monitor on a passive DP-to-HDMI adapter in the lower rear DisplayPort (DDI B).
AAPL,ig-platform-id Data 07009B3E
device-id Data C89B0000
framebuffer-patch-enable Data 01000000
framebuffer-stolenmem Data 00003001
framebuffer-fbmem Data 00009000
framebuffer-con0-enable Data 01000000
framebuffer-con0-busid Data 02000000 <- TGP GMBUS pin for DDI B (Apple default 05)
framebuffer-con0-type Data 00080000 <- HDMI, for the passive DP-to-HDMI adapterdevice-id C89B0000is the usual Comet Lake spoof (the i7-10700 is 0x9BC5). Use the SMBIOSiMac20,1.framebuffer-con0-type 00080000(HDMI) is for HDMI through a passive adapter. With a native DisplayPort monitor you would keep00040000(DP). That combination has not been tested.- To find which DDI your monitor is on, boot with the plugin and read
SDEISRfrom its status (see checking it works): bit 17 set = DDI B, bit 24 = DDI C, bit 25 = DDI D. - Add
CMLTGPFix.kexttoKernel → Addafter Lilu and WhateverGreen. Boot argument-cmltgpoffdisables it.
Other kexts on the test machine: Lilu, VirtualSMC (+SMCProcessor, SMCSuperIO), WhateverGreen, RestrictEvents, IntelMausi, NVMeFix, USBToolBox with a port map, and -wegnoegpu because an unsupported RX 550 was also installed.
Building it
No Xcode is needed. The Command Line Tools are enough, and they install without a GUI session:
touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress
softwareupdate -l | grep "Label: Command Line Tools"
sudo softwareupdate -i "Command Line Tools for Xcode 26.6-26.6" # use the label listed aboveThen fetch the Lilu headers and MacKernelSDK, save the three files from the source section into CMLTGPFix/, and build:
mkdir -p ~/cmltgp && cd ~/cmltgp
curl -sL https://codeload.github.com/acidanthera/Lilu/tar.gz/refs/tags/1.7.2 | tar -xz
curl -sL https://codeload.github.com/acidanthera/MacKernelSDK/tar.gz/refs/heads/master | tar -xz
mkdir -p CMLTGPFix # put kern_start.cpp, Info.plist and build.sh here
bash CMLTGPFix/build.sh
# result: ~/cmltgp/CMLTGPFix/build/CMLTGPFix.kext -> copy to EFI/OC/KextsCheck the build before you reboot. Make sure CMLTGPFix.kext/Contents/MacOS/CMLTGPFix and Contents/Info.plist both exist and aren't empty. An empty kext bundle listed in your config can stop the machine booting. Keep a rescue USB stick with a plain -igfxvesa config and without this plugin.
Checking it works
The plugin publishes its state once a minute. The first report comes 10 seconds after it starts, which can be before the framebuffer has loaded, so give it a minute after boot.
ioreg -r -c CMLTGPFix -w0
sudo dmesg | grep IGFB | grep -E "HPD|HDMI connect|Display status"
system_profiler SPDisplaysDataType- patch-DDI-B/C/D = 0
- The three HPD mask patches applied.
-1means not attempted yet. - layout-status = 0
- Framebuffer offsets verified, wrappers installed.
1means the driver changed and only the byte patches were applied. - route-status = 0
- All four wrappers routed.
- SDEISR
- Live hot-plug state:
131072(bit 17) means a monitor is on DDI B. - tgp-irq-count
- Increments on every plug or unplug.
- hpd-rearm-count
- Times the HPD enables had to be restored, usually after display sleep.
- status-override-count
- Times a failed DP check was corrected for an HDMI sink.
On the test machine this gave HDMI connect found, 1366 × 768 online with Metal 3 at boot, a picture after unplugging and replugging the cable, and a working wake from display sleep (Resuming external display in the log).
Limitations
- Tested on one machine, one port: DDI B with a passive HDMI adapter. The C and D mappings come straight from i915 but are untested, and it isn't known whether Apple's GMBUS code accepts bus IDs 9 and 10.
- Built against
AppleIntelCFLGraphicsFramebuffer24.5.9 in macOS 26.7. The safety check guards against changed offsets, but a future update may need the offsets re-derived. - Display sleep and full system sleep are both tested: the display comes back on wake.
- This fixes display detection only. Everything else about the iGPU (acceleration, VideoToolbox) already worked.
Other Tahoe gotchas on this machine
These cost time on the way. They aren't specific to this plugin but are likely on similar Dell builds.
| Problem | Fix |
|---|---|
Installer boots then loops at the macOS Installer stage: MSU 1130 … firmware_execute failed, boot_to_original_os | SecureBootModel = Disabled, reset NVRAM, erase the disk and reinstall. |
| No working USB at all in the installer | USBToolBox.kext + UTBDefault.kext, then a proper map with 15 ports or fewer. The 500-series controller exposes USB 3 on ports 17–24. |
| OpenCore picker shows only Windows | The recovery .dmg counts as auxiliary. Set HideAuxiliary false or press Space. |
No audio: Tahoe removed AppleHDA, so AppleALC has nothing to patch | VoodooHDA in /Library/Extensions (Auxiliary KC, since OpenCore can't inject it) with csr-active-config 03000000, then approve it in Privacy & Security. Remove AppleALC. |
csr-active-config changes seem to be ignored | Add it to NVRAM → Delete. Otherwise the value stored on first boot wins. |
| RX 550 "Lexa" (0x699F) spoofed to 0x67FF | Crashes and drops signal. Not viable. Disable it with -wegnoegpu. |
| macOS updates | RestrictEvents + revpatch=sbvmm. Updates drop the Auxiliary KC, so re-approve VoodooHDA afterwards. |
Source code
Three files to build from, plus the licence. Save them into a folder called CMLTGPFix next to the unpacked Lilu-1.7.2 and MacKernelSDK-master. The code is released under the BSD 3-Clause licence, the same as Lilu and WhateverGreen: build it, change it and share it, keeping the copyright notice.
//
// kern_start.cpp
// CMLTGPFix
//
// Copyright (c) 2026, A Star Computers
// SPDX-License-Identifier: BSD-3-Clause (see LICENSE)
//
// Comet Lake (Gen9.5) iGPU on a 500-series (Tiger Point, TGP) PCH, e.g. Dell OptiPlex 7090.
// Apple never shipped this pairing: AppleIntelCFLGraphicsFramebuffer handles the PCH hot-plug
// registers with the Cannon/Sunrise Point layout, so every port reads "HPD is low", no display
// is ever detected, and plug/unplug interrupts are never decoded.
//
// Linux i915 (skl_hpd_pin -> icl_hpd_pin for Gen9 BC + TGP) maps the DDIs to TGP HPD pins:
// DDI B -> HPD_PORT_B -> SDE bit 17, status SHOTPLUG_CTL_DDI[5:4] (Apple: bit 21, 0xC4030[1:0])
// DDI C -> HPD_PORT_TC1 -> SDE bit 24, status SHOTPLUG_CTL_TC[1:0] (Apple: bit 22, 0xC4030[9:8])
// DDI D -> HPD_PORT_TC2 -> SDE bit 25, status SHOTPLUG_CTL_TC[5:4] (Apple: bit 23, 0xC4030[17:16])
//
// 1. Live state: patch the SDEISR masks in AppleIntelFramebufferController::DigPortHDPState.
// 2. Interrupts: enable the TGP hot-plug interrupts after hwEnableInterrupts, and translate them
// in ProcessInterrupt into the CNP event bits Apple's SWInterruptHandler already dispatches.
// 3. Display sleep: restore the TGP HPD detection enables whenever Apple re-arms hot-plug and
// before each live-state check (they are lost when the display power domain goes down).
// 4. Wake with HDMI / passive DP++: treat a failed DP status check as connected while HPD is high.
//
// DDC for HDMI / passive DP++ adapters is config only: Apple's busid is the GMBUS pin, and for
// Gen9 BC + TGP i915 uses DDI B -> pin 2, DDI C -> pin 9, DDI D -> pin 10 (CNP: 5 / 4 / 6).
//
#include <Headers/plugin_start.hpp>
#include <Headers/kern_api.hpp>
#include <Headers/kern_patcher.hpp>
#include <IOKit/IOService.h>
#include <kern/thread_call.h>
#include <kern/clock.h>
#include <libkern/OSAtomic.h>
static const char *pathCFL[] {
"/System/Library/Extensions/AppleIntelCFLGraphicsFramebuffer.kext/Contents/MacOS/AppleIntelCFLGraphicsFramebuffer"
};
static KernelPatcher::KextInfo kextCFL {
"com.apple.driver.AppleIntelCFLGraphicsFramebuffer", pathCFL, arrsize(pathCFL), {true}, {}, KernelPatcher::KextInfo::Unloaded
};
// ---------------------------------------------------------------------------------------------
// 1. Live hot-plug state. DigPortHDPState reads SDEISR (0xC4000) and picks the mask through a
// jump table indexed by displayPath+0x450 (0..3 = DDI A..D): `mov eax, imm32; jmp short`.
// The jmp displacement makes each pattern unique in the binary.
// ---------------------------------------------------------------------------------------------
static const uint8_t findB[] { 0xB8, 0x00, 0x00, 0x20, 0x00, 0xEB, 0x02 }; // DDI B: bit 21
static const uint8_t replaceB[] { 0xB8, 0x00, 0x00, 0x02, 0x00, 0xEB, 0x02 }; // bit 17
static const uint8_t findC[] { 0xB8, 0x00, 0x00, 0x40, 0x00, 0xEB, 0x1E }; // DDI C: bit 22
static const uint8_t replaceC[] { 0xB8, 0x00, 0x00, 0x00, 0x01, 0xEB, 0x1E }; // bit 24
static const uint8_t findD[] { 0xB8, 0x00, 0x00, 0x80, 0x00, 0xEB, 0x10 }; // DDI D: bit 23
static const uint8_t replaceD[] { 0xB8, 0x00, 0x00, 0x00, 0x02, 0xEB, 0x10 }; // bit 25
// ---------------------------------------------------------------------------------------------
// 2. Hot-plug interrupts. Apple's ProcessInterrupt (called from the HW interrupt filter) ORs
// decoded events into controller+0x2C70; SWInterruptHandler then dispatches bits 15..18 (long
// pulse = plug/unplug on port index 0..3) to invokeHotplugHandler(idx) and bits 19..22 (short
// pulse) to invokeShortPulseHandler(idx). TGP status bits are write-1-to-clear.
// ---------------------------------------------------------------------------------------------
static constexpr uint32_t SDEIMR = 0xC4004, SDEIIR = 0xC4008, SDEIER = 0xC400C;
static constexpr uint32_t SHOTPLUG_CTL_DDI = 0xC4030, SHOTPLUG_CTL_TC = 0xC4034;
static constexpr uint32_t TgpHotplugIrqs = (1U << 17) | (1U << 24) | (1U << 25);
static constexpr size_t ControllerMmio = 0x1A08, ControllerPendingEvents = 0x2C70;
static inline uint8_t *controllerMmio(void *controller) {
return *reinterpret_cast<uint8_t **>(reinterpret_cast<uint8_t *>(controller) + ControllerMmio);
}
static inline volatile uint32_t &mmioReg(uint8_t *mmio, uint32_t reg) {
return *reinterpret_cast<volatile uint32_t *>(mmio + reg);
}
// HPD detection enables. Firmware sets these at boot, but they are lost when the display power
// domain goes down (display sleep), and Apple re-arms hot-plug with the CNP layout (0xC4030 bits
// 4/12/20/28), which on TGP are status bits. With the enables off SDEISR reads low, so after a
// display sleep the monitor looked unplugged. Restore them without writing back any status bits
// (write-1-to-clear), as i915's icp_ddi_hpd_detection_setup / icp_tc_hpd_detection_setup do.
static constexpr uint32_t DdiHpdEnables = 0x80; // SHOTPLUG_CTL_DDI_HPD_ENABLE(HPD_PORT_B)
static constexpr uint32_t TcHpdEnables = 0x88; // ICP_TC_HPD_ENABLE(TC1) | ICP_TC_HPD_ENABLE(TC2)
static constexpr uint32_t HpdStatusBits = 0x33333333; // 2-bit status field of every pin nibble
static uint8_t *lastMmio;
static uint32_t hpdRearmCount;
static bool restoreHpdEnables(uint8_t *mmio) {
lastMmio = mmio;
uint32_t ddi = mmioReg(mmio, SHOTPLUG_CTL_DDI);
uint32_t tc = mmioReg(mmio, SHOTPLUG_CTL_TC);
if ((ddi & DdiHpdEnables) == DdiHpdEnables && (tc & TcHpdEnables) == TcHpdEnables)
return false;
mmioReg(mmio, SHOTPLUG_CTL_DDI) = (ddi & ~HpdStatusBits) | DdiHpdEnables;
mmioReg(mmio, SHOTPLUG_CTL_TC) = (tc & ~HpdStatusBits) | TcHpdEnables;
hpdRearmCount++;
return true;
}
using t_hwEnableInterrupts = uint64_t (*)(void *);
static t_hwEnableInterrupts orgHwEnableInterrupts;
static uint32_t irqEnableCount;
static uint64_t wrapHwEnableInterrupts(void *controller) {
auto ret = orgHwEnableInterrupts(controller);
if (auto mmio = controllerMmio(controller)) {
restoreHpdEnables(mmio);
mmioReg(mmio, SDEIER) |= TgpHotplugIrqs;
mmioReg(mmio, SDEIMR) &= ~TgpHotplugIrqs;
irqEnableCount++;
}
return ret;
}
// DigPortHDPState(controller, framebuffer, displayPath): make sure detection is enabled before
// the live state is sampled; after re-enabling, give the HPD filter time to settle.
using t_digPortHDPState = uint8_t (*)(void *, void *, void *);
static t_digPortHDPState orgDigPortHDPState;
static uint8_t wrapDigPortHDPState(void *controller, void *framebuffer, void *path) {
if (auto mmio = controllerMmio(controller))
if (restoreHpdEnables(mmio))
IODelay(5000);
return orgDigPortHDPState(controller, framebuffer, path);
}
// status: the 2-bit SHOTPLUG_CTL field, bit 0 = short pulse, bit 1 = long pulse
static uint32_t cnpEvents(uint32_t status, uint32_t portIndex) {
uint32_t events = 0;
if (status & 2) events |= 1U << (15 + portIndex);
if (status & 1) events |= 1U << (19 + portIndex);
return events ? events : 1U << (15 + portIndex); // interrupt without status: treat as plug/unplug
}
using t_processInterrupt = bool (*)(void *);
static t_processInterrupt orgProcessInterrupt;
static uint32_t tgpIrqCount, tgpLastEvents;
static bool wrapProcessInterrupt(void *controller) {
uint32_t events = 0;
if (auto mmio = controllerMmio(controller)) {
uint32_t iir = mmioReg(mmio, SDEIIR) & TgpHotplugIrqs;
if (iir) {
uint32_t ddi = mmioReg(mmio, SHOTPLUG_CTL_DDI);
uint32_t tc = mmioReg(mmio, SHOTPLUG_CTL_TC);
mmioReg(mmio, SHOTPLUG_CTL_DDI) = ddi;
mmioReg(mmio, SHOTPLUG_CTL_TC) = tc;
mmioReg(mmio, SDEIIR) = iir;
if (iir & (1U << 17)) events |= cnpEvents((ddi >> 4) & 3, 1); // DDI B
if (iir & (1U << 24)) events |= cnpEvents(tc & 3, 2); // DDI C (TC1)
if (iir & (1U << 25)) events |= cnpEvents((tc >> 4) & 3, 3); // DDI D (TC2)
tgpIrqCount++;
}
}
bool ret = orgProcessInterrupt(controller);
if (events) {
OSBitOrAtomic(events, reinterpret_cast<volatile UInt32 *>(reinterpret_cast<uint8_t *>(controller) + ControllerPendingEvents));
tgpLastEvents = events;
ret = true; // make HWInterruptHandler schedule SWInterruptHandler
}
return ret;
}
// ---------------------------------------------------------------------------------------------
// 4. HDMI / passive DP++ on wake. AppleIntelFramebuffer::getDisplayStatus(path) checks HPD, then
// powers the sink up over DP AUX; with a passive DP-to-HDMI adapter AUX never answers, so it
// returns 0 ("Setting DP power failed ... likely HDMI/TMDS case"). At boot the HDMI path still
// finds the monitor, but Transition_wake trusts this status and drops the display ("display
// removed in lower powerstate"). If the status is 0 while the port's TGP HPD line is high,
// report it connected; an unplugged monitor (HPD low) still reads as disconnected.
// ---------------------------------------------------------------------------------------------
static constexpr size_t FramebufferController = 0x1D0, DisplayPathPortIndex = 0x450;
static constexpr uint32_t TgpHpdLiveBit[4] { 1U << 16, 1U << 17, 1U << 24, 1U << 25 }; // DDI A..D
using t_getDisplayStatus = uint32_t (*)(void *, void *);
static t_getDisplayStatus orgGetDisplayStatus;
static uint32_t statusOverrideCount;
static uint32_t wrapGetDisplayStatus(void *framebuffer, void *path) {
uint32_t status = orgGetDisplayStatus(framebuffer, path);
if (status == 0 && path) {
auto controller = *reinterpret_cast<void **>(reinterpret_cast<uint8_t *>(framebuffer) + FramebufferController);
uint8_t idx = *(reinterpret_cast<uint8_t *>(path) + DisplayPathPortIndex);
if (controller && idx < 4)
if (auto mmio = controllerMmio(controller))
if (mmioReg(mmio, 0xC4000) & TgpHpdLiveBit[idx]) {
status = 1;
statusOverrideCount++;
}
}
return status;
}
// ---------------------------------------------------------------------------------------------
// Status, published once a minute on our IOService (ioreg -r -c CMLTGPFix): patch and route
// results (0 = ok, otherwise KernelPatcher::Error, -1 = not attempted) and interrupt counters.
// ---------------------------------------------------------------------------------------------
static int patchStatus[3] { -1, -1, -1 };
static int routeStatus = -1;
static int layoutStatus = -1; // 0 = offsets verified, 1 = mismatch (wrappers skipped)
static thread_call_t statusCall;
static void scheduleStatus(uint32_t seconds) {
uint64_t deadline;
clock_interval_to_deadline(seconds, kSecondScale, &deadline);
thread_call_enter_delayed(statusCall, deadline);
}
static void publishStatus(thread_call_param_t, thread_call_param_t) {
if (auto self = ADDPR(selfInstance)) {
self->setProperty("patch-DDI-B", static_cast<unsigned long long>(patchStatus[0]), 32);
self->setProperty("patch-DDI-C", static_cast<unsigned long long>(patchStatus[1]), 32);
self->setProperty("patch-DDI-D", static_cast<unsigned long long>(patchStatus[2]), 32);
self->setProperty("route-status", static_cast<unsigned long long>(routeStatus), 32);
self->setProperty("layout-status", static_cast<unsigned long long>(layoutStatus), 32);
self->setProperty("irq-enable-count", irqEnableCount, 32);
self->setProperty("tgp-irq-count", tgpIrqCount, 32);
self->setProperty("tgp-last-events", tgpLastEvents, 32);
self->setProperty("hpd-rearm-count", hpdRearmCount, 32);
self->setProperty("status-override-count", statusOverrideCount, 32);
if (lastMmio) {
self->setProperty("SDEISR", mmioReg(lastMmio, 0xC4000), 32);
self->setProperty("SHOTPLUG_CTL_DDI", mmioReg(lastMmio, SHOTPLUG_CTL_DDI), 32);
self->setProperty("SHOTPLUG_CTL_TC", mmioReg(lastMmio, SHOTPLUG_CTL_TC), 32);
}
}
scheduleStatus(60);
}
// The wrappers rely on private structure offsets (controller+0x1A08 MMIO, +0x2C70 pending events,
// framebuffer+0x1D0 controller, displayPath+0x450 port index). A macOS update can change them, so
// before routing we check that the functions still use exactly these displacements; otherwise we
// skip the wrappers (worst case: no display) rather than risk a panic.
static bool codeUsesDisplacement(mach_vm_address_t function, size_t length, uint32_t displacement) {
if (!function)
return false;
auto code = reinterpret_cast<const uint8_t *>(function);
for (size_t i = 0; i + sizeof(displacement) <= length; i++)
if (code[i] == (displacement & 0xFF) && code[i + 1] == ((displacement >> 8) & 0xFF) &&
code[i + 2] == ((displacement >> 16) & 0xFF) && code[i + 3] == ((displacement >> 24) & 0xFF))
return true;
return false;
}
static bool layoutMatches(KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) {
auto dig = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController15DigPortHDPStateEP21AppleIntelFramebufferP21AppleIntelDisplayPath", address, size);
auto proc = patcher.solveSymbol(index, "__ZN31AppleIntelFramebufferController16ProcessInterruptEv", address, size);
auto gds = patcher.solveSymbol(index, "__ZN21AppleIntelFramebuffer16getDisplayStatusEP21AppleIntelDisplayPath", address, size);
patcher.clearError();
return codeUsesDisplacement(dig, 0x100, ControllerMmio) &&
codeUsesDisplacement(dig, 0x100, DisplayPathPortIndex) &&
codeUsesDisplacement(proc, 0x1800, ControllerPendingEvents) &&
codeUsesDisplacement(gds, 0x200, FramebufferController);
}
static void processKext(void *, KernelPatcher &patcher, size_t index, mach_vm_address_t address, size_t size) {
if (index != kextCFL.loadIndex)
return;
layoutStatus = layoutMatches(patcher, index, address, size) ? 0 : 1;
if (layoutStatus != 0) {
SYSLOG("cmltgp", "framebuffer layout differs from the version this was built for, skipping wrappers");
goto patchMasks;
}
{
KernelPatcher::RouteRequest requests[] {
{ "__ZN31AppleIntelFramebufferController15DigPortHDPStateEP21AppleIntelFramebufferP21AppleIntelDisplayPath",
wrapDigPortHDPState, orgDigPortHDPState },
{ "__ZN31AppleIntelFramebufferController18hwEnableInterruptsEv", wrapHwEnableInterrupts, orgHwEnableInterrupts },
{ "__ZN31AppleIntelFramebufferController16ProcessInterruptEv", wrapProcessInterrupt, orgProcessInterrupt },
{ "__ZN21AppleIntelFramebuffer16getDisplayStatusEP21AppleIntelDisplayPath", wrapGetDisplayStatus, orgGetDisplayStatus },
};
patcher.routeMultiple(index, requests, arrsize(requests), address, size);
routeStatus = static_cast<int>(patcher.getError());
if (patcher.getError() != KernelPatcher::Error::NoError)
SYSLOG("cmltgp", "interrupt routing FAILED (%d)", patcher.getError());
patcher.clearError();
}
patchMasks:
// The mask patches are safe on any version: a pattern that no longer matches is simply not applied.
const KernelPatcher::LookupPatch patches[] {
{ &kextCFL, findB, replaceB, sizeof(findB), 1 },
{ &kextCFL, findC, replaceC, sizeof(findC), 1 },
{ &kextCFL, findD, replaceD, sizeof(findD), 1 },
};
const char *names[] { "DDI B", "DDI C", "DDI D" };
for (size_t i = 0; i < arrsize(patches); i++) {
patcher.applyLookupPatch(&patches[i]);
patchStatus[i] = static_cast<int>(patcher.getError());
if (patcher.getError() != KernelPatcher::Error::NoError)
SYSLOG("cmltgp", "HPD mask patch %s FAILED (%d)", names[i], patcher.getError());
patcher.clearError();
}
}
static void pluginStart() {
lilu.onKextLoadForce(&kextCFL, 1, processKext);
statusCall = thread_call_allocate(publishStatus, nullptr);
if (statusCall)
scheduleStatus(10);
}
static const char *bootargOff[] { "-cmltgpoff" };
static const char *bootargDebug[] { "-cmltgpdbg" };
static const char *bootargBeta[] { "-cmltgpbeta" };
PluginConfiguration ADDPR(config) {
xStringify(PRODUCT_NAME),
parseModuleVersion(xStringify(MODULE_VERSION)),
LiluAPI::AllowNormal | LiluAPI::AllowInstallerRecovery | LiluAPI::AllowSafeMode,
bootargOff, arrsize(bootargOff),
bootargDebug, arrsize(bootargDebug),
bootargBeta, arrsize(bootargBeta),
KernelVersion::BigSur,
KernelVersion::Tahoe,
pluginStart
};#!/bin/bash
# Copyright (c) 2026, A Star Computers. SPDX-License-Identifier: BSD-3-Clause (see LICENSE)
# Builds CMLTGPFix.kext without Xcode (Command Line Tools only). Run on the Mac from this folder,
# with Lilu-1.7.2/ and MacKernelSDK-master/ unpacked one level up.
set -e
cd "$(dirname "$0")"
SDK=../MacKernelSDK-master
LILU=../Lilu-1.7.2/Lilu
OUT=build/CMLTGPFix.kext
rm -rf build && mkdir -p "$OUT/Contents/MacOS"
cat > build/kmod_info.c <<'EOF'
#include <mach/mach_types.h>
extern kern_return_t _start(kmod_info_t *ki, void *data);
extern kern_return_t _stop(kmod_info_t *ki, void *data);
__attribute__((visibility("default"))) KMOD_EXPLICIT_DECL(org.cmltgpfix.CMLTGPFix, "1.3.1", _start, _stop)
extern kern_return_t CMLTGPFix_kern_start(kmod_info_t *, void *);
extern kern_return_t CMLTGPFix_kern_stop(kmod_info_t *, void *);
__private_extern__ kmod_start_func_t *_realmain = CMLTGPFix_kern_start;
__private_extern__ kmod_stop_func_t *_antimain = CMLTGPFix_kern_stop;
__private_extern__ int _kext_apple_cc = __APPLE_CC__;
EOF
COMMON="-arch x86_64 -mmacosx-version-min=11.0 -mkernel -nostdinc -fno-builtin -fno-common -fno-stack-protector
-D KERNEL -D KERNEL_PRIVATE -D DRIVER_PRIVATE -D APPLE -D NeXT
-D PRODUCT_NAME=CMLTGPFix -D MODULE_VERSION=1.3.1
-I $SDK/Headers -I $LILU -O2 -Wno-deprecated-declarations"
CXXFLAGS="$COMMON -x c++ -std=c++17 -fapple-kext -fno-exceptions -fno-rtti"
clang $COMMON -x c -c build/kmod_info.c -o build/kmod_info.o
clang $CXXFLAGS -c kern_start.cpp -o build/kern_start.o
clang $CXXFLAGS -c $LILU/Library/plugin_start.cpp -o build/plugin_start.o
CCKEXT=$(ls /Library/Developer/CommandLineTools/usr/lib/clang/*/lib/darwin/libclang_rt.cc_kext.a 2>/dev/null | head -1)
clang -arch x86_64 -mmacosx-version-min=11.0 -nostdlib -Xlinker -kext -Xlinker -no_data_const \
build/kmod_info.o build/kern_start.o build/plugin_start.o \
-L$SDK/Library/x86_64 -lkmod ${CCKEXT:+$CCKEXT} -o "$OUT/Contents/MacOS/CMLTGPFix"
cp Info.plist "$OUT/Contents/Info.plist"
echo "built $OUT"
nm -u "$OUT/Contents/MacOS/CMLTGPFix" | head -30<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>CMLTGPFix</string>
<key>CFBundleIdentifier</key>
<string>org.cmltgpfix.CMLTGPFix</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>CMLTGPFix</string>
<key>CFBundlePackageType</key>
<string>KEXT</string>
<key>CFBundleShortVersionString</key>
<string>1.3.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.3.1</string>
<key>IOKitPersonalities</key>
<dict>
<key>org.cmltgpfix.CMLTGPFix</key>
<dict>
<key>CFBundleIdentifier</key>
<string>org.cmltgpfix.CMLTGPFix</string>
<key>IOClass</key>
<string>CMLTGPFix</string>
<key>IOMatchCategory</key>
<string>CMLTGPFix</string>
<key>IOProviderClass</key>
<string>IOResources</string>
<key>IOResourceMatch</key>
<string>IOKit</string>
</dict>
</dict>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2026 A Star Computers. BSD-3-Clause.</string>
<key>OSBundleCompatibleVersion</key>
<string>1.0</string>
<key>OSBundleLibraries</key>
<dict>
<key>as.vit9696.Lilu</key>
<string>1.3.1</string>
<key>com.apple.kpi.bsd</key>
<string>12.0.0</string>
<key>com.apple.kpi.dsep</key>
<string>12.0.0</string>
<key>com.apple.kpi.iokit</key>
<string>12.0.0</string>
<key>com.apple.kpi.libkern</key>
<string>12.0.0</string>
<key>com.apple.kpi.mach</key>
<string>12.0.0</string>
<key>com.apple.kpi.unsupported</key>
<string>12.0.0</string>
</dict>
<key>OSBundleRequired</key>
<string>Root</string>
</dict>
</plist>BSD 3-Clause License
Copyright (c) 2026, A Star Computers
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.Credits
- The Linux i915 developers, whose Gen9 + TGP support (
intel_ddi.c,intel_hotplug_irq.c,intel_bios.c,intel_display_regs.h) documents every register used here. - acidanthera for OpenCore, Lilu, WhateverGreen, RestrictEvents and MacKernelSDK.
- USBToolBox, Dortania's guides and VoodooHDA.
- vladII19's Latitude 7390 Tahoe audio write-up, which documents the VoodooHDA route.