@FireWolf2 years ago
Ice Lake Intel Iris Plus Graphics on macOS Catalina: A solution to the kernel panic due to unsupported core display clock frequencies in the framebuffer driver
Hi folks! I haven’t updated my blog for a long time, but today I have something new about the graphics driver for Ice Lake platforms on macOS Catalina 10.15.6 to share with you. It’s time to write a new blog post.
If this page looks strange due to font or layout issues, you could find a pretty-printed version at here.
>> Introduction
It has been quite a while since Apple released the graphics driver for Intel Ice Lake platforms. While we expect that it should not be difficult to make the integrated graphics card on an Ice Lake-based laptop work under macOS Catalina, a large number of people has encountered a kernel panic due to an unsupported Core Display Clock frequency. Core Display Clock (CDCLK
) is one of the primary clocks used by the display engine to do its work. Apple’s graphics driver expects that the EFI firmware has already set the clock frequency to either 652.8 MHz or 648 MHz, but quite a few laptops set it to a much lower value (e.g. 172.8 MHz), and hence a kernel panic is triggered. In the following sections, I will focus on how the graphics driver verifies and configures the Core Display Clock and how we add support for these valid yet unsupported frequencies.
>> Analyze the framebuffer driver
We need to locate the kernel panic first, so we obtain a panic report as follows. We could observe that the kernel panic is triggered inside the function AppleIntelFramebufferController::probeCDClockFrequency()
while the driver tries to start the framebuffer controller.
// Return addresses are omitted panic(): "[IGFB][PANIC][DISPLAY ] " "Unsupported CD clock decimal frequency 0x158\n" @/Library/Caches/com.apple.xbs/Sources/GPUDriversIntel/GPUDriversIntel-14.7.8/IONDRV/ICLLP/AppleIntelFramebuffer/AppleIntelClocks.cpp:348 Backtrace (CPU 2), Frame : Return Address mach_kernel : _handle_debugger_trap + 0x49d mach_kernel : _kdp_i386_trap + 0x155 mach_kernel : _kernel_trap + 0x4ee as.vit9696.VirtualSMC : __ZN18VirtualSMCProvider10kernelTrapI22x86_saved_state_1010_tEEvPT_Pm + 0x46d mach_kernel : _return_from_trap + 0xe0 mach_kernel : _DebuggerTrapWithState + 0x17 mach_kernel : _panic_trap_to_debugger + 0x227 mach_kernel : _panic + 0x54 AppleIntelICLLPGraphicsFramebuffer : __ZN31AppleIntelFramebufferController21probeCDClockFrequencyEv.cold.2 AppleIntelICLLPGraphicsFramebuffer : __ZN31AppleIntelFramebufferController21probeCDClockFrequencyEv + 0xe5 AppleIntelICLLPGraphicsFramebuffer : __ZN31AppleIntelFramebufferController11initCDClockEv + 0x87 AppleIntelICLLPGraphicsFramebuffer : __ZN31AppleIntelFramebufferController5startEP9IOService + 0xb85 mach_kernel : __ZN9IOService14startCandidateEPS_ + 0xf6 mach_kernel : __ZN9IOService15probeCandidatesEP12OSOrderedSet + 0xad1 mach_kernel : __ZN9IOService14doServiceMatchEj + 0x2de mach_kernel : __ZN15_IOConfigThread4mainEPvi + 0x186 mach_kernel : _call_continuation + 0x2e Kernel Extensions in backtrace: as.vit9696.VirtualSMC(1.1.5)@0xffffff7f8628c000->0xffffff7f862b2fff dependency: as.vit9696.Lilu(1.4.6)@0xffffff7f86206000 dependency: com.apple.iokit.IOACPIFamily(1.4)@0xffffff7f8456e000 com.apple.driver.AppleIntelICLLPGraphicsFramebuffer(14.0.7)0xffffff7f87e13000->0xffffff7f880b0fff dependency: com.apple.iokit.IOPCIFamily(2.9)@0xffffff7f83f31000 dependency: com.apple.iokit.IOACPIFamily(1.4)@0xffffff7f8456e000 dependency: com.apple.iokit.IOAcceleratorFamily2(438.7.3)@0xffffff7f87d4f000 dependency: com.apple.iokit.IOReportFamily(47)@0xffffff7f844ae000 dependency: com.apple.AppleGraphicsDeviceControl(5.2.6)@0xffffff7f84b4a000 dependency: com.apple.iokit.IOGraphicsFamily(576.1)@0xffffff7f84813000
The probe()
function is invoked by initCDClock()
, so we disassemble the graphics driver to find what they are doing. Fortunately, these functions are relatively small compared to the ones I analyzed before, so let’s take a look at my translated pseudocode directly.
/// /// Probe the Core Display Clock frequency and calculate the PLL VCO frequency /// /// @param this The hidden implicit `this` pointer /// @return The PLL VCO frequency derived from the current Core Display Clock frequency. /// @note Function Signature: AppleIntelFramebufferController::probeCDClockFrequency() /// @author Assembly code dumped from macOS 10.15.6 (19G2021) and translated by FireWolf. /// int AppleIntelFramebufferController::probeCDClockFrequency(AppleIntelFramebufferController* this) { // %rdi stores the implicit `this` pointer // Read from the CDCLK_PLL_ENABLE register SInt32 retVal = AppleIntelFramebufferController::ReadRegister32(this, 0x46070); // Bit 31 of the CDCLK_PLL_ENABLE register is set when PLL is enabled // PLL must be enabled before the driver probes the CD clock // testl %eax, %eax // jns loc_71474 if (retVal >= 0) { // loc_71474: // Error: PLL is not enabled // Print the error message and trigger the kernel panic panic("ERROR"); } // Read from the CDCLK_CTL register UInt32 retVal = AppleIntelFramebufferController::ReadRegister32(this, 0x46000); // Retrieve low 11 bits // i.e. retVal now stores the Core Display Frequency decimal // andl $0x7FF, %eax retVal &= 0x7FF; // Check the current Core Display Clock frequency // Valid values are // - 0x518: 652.8 MHz // - 0x50E: 648 MHz // - 0x458: 556.8 MHz // - 0x44E: 552 MHz // - 0x26E: 312 MHz // - 0x264: 307.2 MHz // - 0x17E: 192 MHz // - 0x166: 180 MHz // - 0x158: 172.8 MHz switch (retVal) { // loc_7143c case 0x50E: // 648 MHz case 0x518: // 652.8 MHz // OK: goto loc_7144a case 0x264: case 0x26E: case 0x44E: // goto loc_71410 panic("Wrong CD clock set by EFI"); default: // goto loc_71482 panic("Unsupported CD clock"); } // loc_7144a: // %rbx now stores the `this` pointer // Read the instance field stored at 0xE60 // This field stores the reference frequency (See initCDClock()) // movl 0xe60(%rbx), %eax rax = this->field_0xe60; // Multiplied by 9 // leaq (%rax, %rax, 8), %rax rax = rax + rax * 8; // Load the address of the global variable rcx = PllRatioCdClkPair; // Read the "%rax"-th element in the array // Array starts at (0x20 + PllRatioCdClkPair) // movl 0x20(%rcx, %rax, 4), %eax // i.e. rax = ((UInt32*) (rcx + 0x20))[rax]; rax = 0x20 + (rcx + rax * 4); ^^^^^^^^^^^ ~~~ Base Address Index return *rax; }
The probe()
function first reads a 32-bit integer from the register at 0x46070
and checks whether the value is signed or not. According to Intel’s graphics driver developer manual, the highest bit of the register CDCLK_PLL_ENABLE
(at 0x46070
) is set when PLL is enabled, and hence the driver expects that PLL has already been enabled before it probes the Core Display Clock frequency. Subsequently, the driver retrieves the current frequency from the CDCLK_CTL
register (at 0x46000
) and triggers a kernel panic if the value is not one of 0x50E
and 0x518
. Finally, the driver uses the field stored at 0xE60
in the framebuffer controller as an index to retrieve some value from a global table PllRatioCdClkPair
and returns it back to the caller.
So far, we know that a Core Display Clock frequency other than 652.8 MHz or 648 MHz would trigger a kernel panic, but the meaning of the instance field at 0xE60
and contents of the global table are still mysterious. As such, we need to analyze the caller function initCDClock()
to see if we could have a further insight. We could observe that there is an instruction that writes to the instance field at 0xE60
in this function, so if we can find its concrete value, we will be able to read from the table PllRatioCdClkPair
. Let’s now take a look at the translated version of initCDClock()
.
/// /// Initialize the Core Display Clock /// /// @param The hidden implicit `this` pointer /// @note Function Signature: AppleIntelFramebufferController::initCDClock() /// @author Assembly code dumped from macOS 10.15.6 (19G2021) and translated by FireWolf. /// @note This function probes and stores the hardware reference frequency, /// as well as the current and the new Core Display Clock PLL VCO frequency. /// void AppleIntelFramebufferController::initCDClock(AppleIntelFramebufferController* this) { // %rdi, %r14 both stores a reference to the `this` pointer // Read from DSSM register to fetch the hardware reference frequency UInt32 retVal = AppleIntelFramebufferController::ReadRegister32(this, 0x51004); // Guard: Reference frequency must be valid // Bits 29-31 store the reference frequency value // Valid values are: // - 0x0: 24 MHz // - 0x1: 19.2 MHz // - 0x2: 38.4 MHz // Equivalent to checking whether the highest 3 bits is greater than 0b011. if (retVal >= 0x60000000) { // Error: Invalid value panic("Invalid reference frequency."); } // %ebx now stores the reference frequency // movl %eax, %ebx // shrl $0x1D, %ebx UInt32 ebx = retVal >> 0x1D; // Store the reference frequency to the field at 0xe60 // movl %ebx, 0xe60(%r14) this->field_0xe60 = referenceFrequency; // %rax stores the return value retVal = AppleIntelFramebufferController::probeCDClockFrequency(this); this->field_0xe50 = retVal; // A fallback that does not even work // The current `probe()` function never returns 0 // If it returns 0, `field_0xe50` is 0 and a divide-by-zero error will occur later if (retVal == 0) { // The following operations are the same as the end of probe() rax = this->field_0xe60; rax = rax + rax * 8; // Global Variable @ 0x9B6C0 rcx = PllRatioCdClkPair; // Base = 0x9B6E0; Index = RefFreq * 9 // Final Address = 0x9B6E0 + 36 * RefFreq // 24.0 MHz: Value @ 0x9B6E0 = 0x4D3F6400 (1296000000) // 19.2 MHz: Value @ 0x9B704 = 0x4DD1E000 (1305600000) // 38.4 MHz: Value @ 0x9B728 = 0x4DD1E000 (1305600000) // [email protected][0/9/18] rax = *(UInt32*) (0x20 + rcx + rax * 4); } this->field_0xe58 = rax; }
The initCDClock()
function first fetches the hardware reference frequency from the DSSM
register (at 0x51004
). There are only three valid values, 24 MHz (0x0
), 19.2 MHz (0x1
), and 38.4 MHz (0x2
), and a kernel panic will occur if the register value is invalid. The driver then stores the hardware reference frequency to the field at 0xE60
and calls probeCDClockFrequency()
. When the probe()
function returns, the return value is stored to the field at 0xE50
. If the return value is nonzero, it is also stored to the field at 0xE58
. Otherwise, the driver stores the value read from the table PllRatioCdClkPair
instead as a “fallback”.
Now that we have three possible concrete values of the field at 0xE60
, we could retrieve and try to demystify the value from the table.
Hardware Reference Frequency | PllRatioCdClkPair Table Value (Hex) | PllRatioCdClkPair Table Value (Dec) |
---|---|---|
24.0 MHz | 0x4D3F6400 | 1296000000 |
19.2 MHz | 0x4DD1E000 | 1305600000 |
38.4 MHz | 0x4DD1E000 | 1305600000 |
Intel mentions in its developer manual that the Core Display Clock PLL is the main source for Core Display Clock, and it must be programmed by the graphics driver to enable or disable a display, so we hypothesize that both 0x4D3F6400
and 0x4DD1E000
represent the PLL frequency. The PLL frequency is the product of the PLL ratio for a Core Display Clock frequency and the hardware reference frequency. The manual lists all possible pairs, and indeed our hypothesis is correct. It is also worth noting that we can find the same calculation in Intel Graphics Drivers for Linux.
Core Display Clock Frequency | PLL Ratio (19.2 MHz) | PLL Ratio (24 MHz) | PLL Ratio (38.4 MHz) | PLL Frequency |
---|---|---|---|---|
652.8 MHz | 68 | N/A | 34 | 1305.6 MHz = 1305600000 Hz |
648.0 MHz | N/A | 54 | N/A | 1296.0 MHz = 1296000000 Hz |
>> Simple but not optimal solutions
So far, we know that the probeCDClockFrequency()
function assumes that the BIOS has set the Core Display Clock frequency to one of supported values and finds the PLL frequency that corresponds to the current hardware reference frequency. Its caller function initCDClock()
just stores the PLL frequency to the controller and finishes the initialization sequence. Some Ice Lake-based laptops, however, do not satisfy these preconditions, and hence a kernel panic occurs. We have several ways to patch the framebuffer driver to support other valid Core Display Clock frequencies, and we always want to keep the number of modifications as minimal as possible.
First Attempt: Use the fallback mechanism
Further investigations have revealed that the field at 0xE50
stores the current Core Display Clock PLL frequency while the field at 0xE58
stores the new value pending to be changed to. The graphics driver has code to check the inequality of these two fields and then switches to the new frequency in other functions. Since initCDClock()
has a fallback mechanism, we could patch the probe()
function to return 0 if it finds an unsupported Core Display Clock frequency. As such, the graphics driver will reconfigure the Core Display Clock to a supported value based on the actual hardware reference frequency. Unfortunately, it does not work as expected, because the current frequency value is used as a divider later and a divide-by-zero error will occur. We deduce that the fallback mechanism is totally useless, and we should ignore it from now on.
Second Attempt: Remove the frequency check
Even though a frequency of 172.8 MHz
is low, it is still capable of driving the built-in full HD display. Consequently, we should be able to light up the eDP panel if we remove the frequency check from the probeCDClockFrequency()
function. Indeed, our experiments have shown that kernel panic is no longer triggered but the display now blinks or presents garbled images frequently. As a result, we conclude that a low Core Display Clock frequency might not be enough to deliver the best user experience even though it is valid in theory.
>> An optimal solution
Since the graphics driver expects that the Core Display Clock frequency is set to the highest one and there are quite a few locations that rely on this assumption as well, we should reprogram the clock and switch its frequency to the one “recommended” by Apple. We inject code into the probeCDClockFrequency()
function to check whether the current frequency is natively supported. If not, we read the reference frequency to find the appropriate PLL frequency for the hardware and then sets the new clock. Since the display engine is not started yet, the sequence of setting a new clock can be summarized as follows.
- Disable the PLL by clearing the highest bit of the CDCLK_PLL_ENABLE register.
- Inform the power controller that the clock frequency will be changed.
- Enable the PLL by setting the highest bit and writing the new PLL ratio to the CDCLK_PLL_ENABLE register.
- Write the new value (Divider, Pipe, SSA Precharge, CD Frequency Decimal) to the CDCLK_CTL register.
- Inform the power controller of the new Core Display Clock voltage level.
- Wait for the hardware to complete the request.
Fortunately, we don’t need to manually implement a function to perform aforementioned operations. Apple has already provided two convenient helper functions, disableCDClock()
to disable the PLL and setCDClockFrequency()
to set a new PLL frequency. As such, our new probeCDClockFrequency()
supports non-native Core Display Clock frequencies as follows. Once the patch is enabled, the built-in display no longer constantly blinks or presents garbled images, and we can read from the register again to ensure that the new Core Display Clock frequency is indeed effective.
/// /// [Wrapper] Probe and adjust the Core Display Clock frequency if necessary /// /// @param this The hidden implicit `this` pointer /// @return The PLL VCO frequency derived from the current Core Display Clock frequency. /// @note Function Signature: AppleIntelFramebufferController::wrapProbeCDClockFrequency() /// @author Support valid yet non-native Core Display Clock frequencies by FireWolf. /// @note This function presents the logic to support other frequencies in pseudocode. /// The complete version can be found in the WhateverGreen repository on Github. /// int AppleIntelFramebufferController::wrapProbeCDClockFrequency(AppleIntelFramebufferController* this) { // Read the current Core Display Clock frequency UInt32 cdclk = AppleIntelFramebufferController::ReadRegister32(this, CDCLK_CTL); // Adjust the Core Display Clock frequency if necessary if (cdclk is not supported) { UInt32 referenceFrequency = AppleIntelFramebufferController::ReadRegister32(this, DSSM); UInt32 newPLLFrequency = 1296 MHz if referenceFrequency is 24 MHz, otherwise 1305.6 MHz; AppleIntelFramebufferController::disableCDClock(); AppleIntelFramebufferController::setCDClockFrequency(newPLLFrequency); // CDCLK has been switched to a supported frequency // Read and check the new value from the register CheckFrequency(cdclk, AppleIntelFramebufferController::ReadRegister32(this, CDCLK_CTL)); } // Call the original function return AppleIntelFramebufferController::orgProbeCDClockFrequency(this); }
>> Conclusion
We have discovered that a kernel panic saying “Unsupported CD clock decimal frequency” occurs because Apple only supports the highest frequencies. We have identified several assumptions and preconditions specified by the graphics driver, and we have analyzed functions related to configuring the Core Display Clock frequency. We add support for those “unsupported” yet valid frequencies to the graphics driver and reprogram the clock to deliver the best user experience. The fix will be integrated into WhateverGreen
, and I hope that this fix would help you make progress toward configuring the integrated graphics card to fully work on your Ice Lake-based laptops.
That’s the end of the story. Take care and stay healthy. See you in the next post.
>> Relevant Notes, Additional Resources and References
– Intel Graphics Developer Manual for Ice Lake Platforms, Volume 2c: Command and Register References [PDF]
You could find detailed descriptions of registers mentioned in this article.
– Intel Graphics Developer Manual for Ice Lake Platforms, Volume 12: Display Engine [PDF]
You could find the Core Display Clock and PLL programming guide.
– Intel Graphics Driver for Linux (Linux Kernel 5.8.3)
The intel_cdclk.c file can be considered as a reference implementation to configure the Core Display Clock. The Linux driver “sanitizes” the current Core Display Clock configurations to avoid any invalid values set by the BIOS or other operating systems. The driver then sets the frequency that is appropriate for the hardware at the end of the initialization sequence. In comparison, Apple assumes that everything is set probably because its firmware has already configured the clock appropriately. However, those assumptions might not be true on some Ice Lake-based laptops, and hence the driver triggers a kernel panic to indicate the error. The following functions might be interesting to see how the graphics driver on Linux initializes the Core Display Clock. The indentation represents the caller and the callee.
/// /// Call sequence of initializing Core Display Clock on Linux /// /// Linux Kernel: 5.8.3 /// File: /src/drivers/gpu/drm/i915/display/intel_cdclk.c /// Summarized by FireWolf /// // Entry Point: Platform-independent init() intel_cdclk_init_hw() // Redirect to platform-dependent init() bxt_cdclk_init_hw() // Sanitize the CDCLK // The CDCLK value set by the previous OS might not be appropriate or valid. bxt_sanitize_cdclk() // Find a CDCLK that is greater than the MIN_CDCLK and matches the RefFreq bxt_calc_cdclk() // Use the new CDCLK value to calculate the PLL VCO bxt_calc_cdclk_pll_vco() // Use the new CDCLK value to calculate the voltage level display.calc_voltage_level() // Set the new CDCLK frequency bxt_set_cdclk()
>> Update Logs
Revision 0: Initial Release
>> License
This article is licensed under Attribution-NonCommercial 4.0 International License.
Copyright (C) 2020 FireWolf @ FireWolf Pl. All Rights Reserved.
Ice Lake Intel Iris Plus Graphics on macOS Catalina: A solution to the kernel panic due to unsupported core display clock frequencies in the framebuffer driver
-
Google Chrome 84
Windows
-
Firefox 80
Mac OS X 10.15
Hi there,
First of all, fantastic work! Thank you from all of us Ice Lake owners.
Second, after trying to compile and run current WhateverGreen HEAD, I no longer get the CD clock frequency panic, but I still get the (new) panic (page fault) a little later during the boot. Specifically, it seems, at
0xffffff82d62db990 : 0xffffff7f880d24ed com.apple.driver.AppleIntelICLGraphics : __ZN25IGHardwareGlobalPageTable15initWithOptionsEP16IntelAcceleratorRK14IGAddressRangePvyj + 0xc9The guys at WhateverGreen support thread on insanelymac suggested I contact you on this one.
If by chance you can help, I am happy to provide you with all the details, of course.
-
Safari 13
Mac OS X 10.15.6
Amazing work!
If you don’t mind me asking: is the code in the “An optimal solution” section from a .dsl SSDT patch? can I compile it as an .aml file and make graphics acceleration work under my i51035G1 iGPU laptop? Where will you be releasing said patched WhateverGreen kext? Could you possibly contact your friend and ask him for his EFI folder? I saw your post on insanelymac and I really wish I could contact you and your friend somehow to try this on my system. Again, sorry for bothering.
Again, thanks for sharing this information.
Take care and have a nice day!
t. Lilian
that`s great work! Thanks for contributing!