FireWolf Pl.

A Place of Freedom

@FireWolf2 months ago

08/23
01:19
macOS Catalina

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 FrequencyPllRatioCdClkPair Table Value (Hex)PllRatioCdClkPair Table Value (Dec)
24.0 MHz0x4D3F64001296000000
19.2 MHz0x4DD1E0001305600000
38.4 MHz0x4DD1E0001305600000

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 FrequencyPLL Ratio
(19.2 MHz)
PLL Ratio
(24 MHz)
PLL Ratio
(38.4 MHz)
PLL Frequency
652.8 MHz68N/A341305.6 MHz = 1305600000 Hz
648.0 MHzN/A54N/A1296.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

  1. Vladimir
    Firefox 80Firefox 80Mac OS X 10.15Mac 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 + 0xc9

    The 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.

    Reply
  2. LireMei
    Safari 13Safari 13Mac OS X 10.15.6Mac 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

    Reply